From 9c34114c52617e8d433f87c3fdcea52b03577ef3 Mon Sep 17 00:00:00 2001 From: Guangyang Li Date: Wed, 16 Sep 2026 22:07:17 -0400 Subject: [PATCH] Add a release process note Nothing wrote down how a release is cut. The steps lived in release.sh's closing echo, a comment at the top of publish.yml, and nowhere else, which is enough to follow once and not enough to follow again a year later. RELEASING.md covers the one-time PyPI trusted-publishing setup, the checks worth doing by hand before tagging, the tag and release steps, and what to verify afterwards. Two things it records that are easy to learn the hard way: a version can never be replaced on PyPI, so the artifacts are exercised from a clean environment before the tag is pushed rather than after; and the suite imports the working tree, so it cannot catch a packaging mistake, which is why the built artifacts are installed into throwaway venvs in both install modes. Written without version numbers so it does not go stale. --- RELEASING.md | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 RELEASING.md diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..261e340 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,125 @@ +# Releasing + +How a release is cut. Developer-facing; users do not need any of this. + +Publishing is done by CI, not from a laptop. Creating a GitHub release triggers +`.github/workflows/publish.yml`, which builds the artifacts and uploads them to PyPI using +[trusted publishing](https://docs.pypi.org/trusted-publishers/). No API token is stored anywhere. + +Throughout, `X.Y.Z` stands for the version being released. + +## One-time setup + +Only needed once per project, or if the PyPI configuration is ever reset. + +1. **Configure the PyPI trusted publisher.** On PyPI, open the project's + *Settings -> Publishing* page and add a GitHub publisher with the owner and repository of this + project, workflow filename `publish.yml`, and environment name `pypi`. +2. **Create the `pypi` environment** in the repository's *Settings -> Environments*. The publish + job declares `environment: pypi`, so the job will not start without it. + +Without both, the workflow runs and fails at the upload step. Everything before that point still +works, so a misconfiguration is visible but not destructive. + +## Before releasing + +`./release.sh` runs the core checks: the test suite, the example generator, a build, and +`twine check`. CI runs considerably more on every pull request, so a green `master` already covers +most of this. + +Two things are worth doing by hand anyway, because neither is covered by simply running the suite. + +**Check the built artifacts actually work when installed.** The suite imports the working tree, so +it cannot catch a packaging mistake such as a missing file or a wrong dependency. Install each +artifact into a throwaway environment and draw a chart: + +```bash +python -m build +for artifact in dist/*.whl dist/*.tar.gz; do + for extra in "" "[icons]"; do + rm -rf /tmp/relcheck && python -m venv /tmp/relcheck + /tmp/relcheck/bin/pip install -q "${artifact}${extra}" + MPLBACKEND=Agg /tmp/relcheck/bin/python -c " +import matplotlib; matplotlib.use('Agg') +import matplotlib.pyplot as plt, pywaffle +from pywaffle import Waffle +fig = plt.figure(FigureClass=Waffle, rows=5, columns=10, values=[30, 20]) +assert len(fig.axes[0].patches) == 50 +print(pywaffle.__version__, 'ok')" + done +done +``` + +Both install modes matter. Font Awesome is an optional extra, so a plain install must draw +rectangles and must refuse `icons=` with a message naming the extra, while an `[icons]` install must +draw icons. A release that gets this backwards is only visible from a clean environment. + +**Check the changelog against reality.** Entries are written while the work is fresh and can drift +before the release goes out. Confirm that anything the changelog claims to add is actually exported, +and that any constant it quotes still holds that value. This has caught a stale number more than +once. + +## Releasing + +1. **Make sure `master` is green and has no open pull requests** that belong in the release. + +2. **Set the version.** Edit `__version__` in `pywaffle/_version.py`. Everything else reads it from + there: `pyproject.toml` declares it dynamic, and `pywaffle.__version__` re-exports it, so this is + the only place it is written. + +3. **Date the changelog.** The top heading is kept as `vX.Y.Z (unreleased)` while work accumulates. + Change it to `vX.Y.Z (YYYY-MM-DD)`. Check that every user-visible change since the last release + has an entry, and mark anything breaking under its own `Breaking` heading. + + Steps 2 and 3 go in their own pull request, so the release commit is reviewed like any other. + +4. **Tag the merge commit and push the tag.** + + ```bash + git checkout master && git pull + python -c "import pywaffle; print(pywaffle.__version__)" # confirm it matches + head -1 CHANGELOG.md # confirm the date is set + git tag vX.Y.Z + git push origin vX.Y.Z + ``` + +5. **Create the GitHub release.** This is the step that publishes. + + ```bash + gh release create vX.Y.Z --title vX.Y.Z --notes "..." + ``` + + Use the changelog's top section as the notes. Lead with anything breaking: release notes are + skimmed, and an upgrade that stops working is what people need to see first. + +6. **Watch the publish run.** + + ```bash + gh run watch "$(gh run list --workflow=publish.yml --limit 1 --json databaseId --jq '.[0].databaseId')" + ``` + +## After + +```bash +pip download --no-deps -d /tmp/pypi-check pywaffle==X.Y.Z +``` + +Downloading from PyPI confirms the upload rather than assuming it. Then open the project page and +check the rendered description, which comes from `README_pypi.rst` and is rendered by PyPI rather +than by GitHub, so it can fail there and nowhere else. `twine check` catches most of this earlier. + +Finally, add a new `vNEXT (unreleased)` heading to the changelog so the next change has somewhere to +go. + +## Notes + +**A version cannot be replaced on PyPI.** A file uploaded under a version number is permanent, even +if deleted. A mistake means releasing the next patch version, not re-uploading. This is why the +artifacts are exercised from a clean environment before the tag is pushed rather than after. + +**The tag is what people trust, so tag the merge commit.** Tagging a branch tip that never reached +`master` produces a release whose source nobody can find. + +**`workflow_dispatch` is enabled on the publish workflow** for the case where a release was created +but the run failed for an unrelated reason, such as a transient network error. It builds from +whatever `master` currently holds, so only use it when `master` is still at the tagged commit.