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. No API token is stored anywhere.
Throughout, X.Y.Z stands for the version being released.
Only needed once per project, or if the PyPI configuration is ever reset.
- 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 namepypi. - Create the
pypienvironment in the repository's Settings -> Environments. The publish job declaresenvironment: 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.
./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:
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
doneBoth 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.
-
Make sure
masteris green and has no open pull requests that belong in the release. -
Set the version. Edit
__version__inpywaffle/_version.py. Everything else reads it from there:pyproject.tomldeclares it dynamic, andpywaffle.__version__re-exports it, so this is the only place it is written. -
Date the changelog. The top heading is kept as
vX.Y.Z (unreleased)while work accumulates. Change it tovX.Y.Z (YYYY-MM-DD). Check that every user-visible change since the last release has an entry, and mark anything breaking under its ownBreakingheading.Steps 2 and 3 go in their own pull request, so the release commit is reviewed like any other.
-
Tag the merge commit and push the tag.
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
-
Create the GitHub release. This is the step that publishes.
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.
-
Watch the publish run.
gh run watch "$(gh run list --workflow=publish.yml --limit 1 --json databaseId --jq '.[0].databaseId')"
pip download --no-deps -d /tmp/pypi-check pywaffle==X.Y.ZDownloading 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.
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.