Releasing¶
A release is a tag on main. .github/workflows/release.yml does the rest, in an order
that leaves nothing half-done:
check -> image (amd64, arm64) + Python package -> multi-arch tags -> PyPI -> GitHub release
PyPI is last because it cannot be undone: a file, once uploaded, is there for good. An image tag can be moved.
Making a release¶
- Pick the version.
X.Y.Z, orX.Y.ZaN,X.Y.ZbN,X.Y.ZrcNfor a prerelease. - Prepare it in a pull request:
Rename
for p in trimset trimset-stats trimset-server; do uv version --package $p X.Y.Z; done## [Unreleased]in CHANGELOG.md to## [X.Y.Z] - YYYY-MM-DD, add a fresh empty[Unreleased]above it, and update the README's status and "not yet" lines. The three packages carry one version. Merge it once CI is green. - Tag the merge commit on
mainand push the tag:git switch main && git pull git tag vX.Y.Z && git push origin vX.Y.Z - Watch the run. If the
pypijob has a required reviewer, approve it when the images and the package have built.
What the workflow checks before it publishes anything (tools/release_check.py): the tag
is v plus the packages' version, the packages agree, the version is a plain
X.Y.Z[aN|bN|rcN], CHANGELOG.md has a dated section for it, the tagged commit is on
main, and CI was green on that commit. The GitHub release's notes are the changelog
section.
An alpha, beta or release candidate is marked as a prerelease on GitHub and never takes
the latest image tag.
What gets published¶
| What | Where | How |
|---|---|---|
Container image, linux/amd64 and linux/arm64 |
ghcr.io/trimset/trimset:X.Y.Z (and :latest for a final release) |
Each architecture builds on its own runner, is started once to check it runs, then the two are joined under one tag. |
| The Python SDK and CLI | pypi.org/project/trimset |
uv publish with PyPI trusted publishing: no token exists anywhere. Files already on PyPI are skipped, so a rerun is safe. |
| Release notes | GitHub Releases | gh release create from the changelog. |
trimset-stats and trimset-server are inside the image and are not published to PyPI.
Rehearsal¶
The same workflow runs without publishing, by hand (gh workflow run release.yml) and on
every pull request that touches Dockerfile, a lockfile, a pyproject.toml, the release
script or the workflow. It builds both images, starts each, builds the wheel and sdist,
runs twine check --strict, installs the wheel on Python 3.11 and runs the CLI. A
dependency bump that breaks the arm64 image shows up in that pull request, not on the day
of a release.
One-time setup (the maintainer, in the browser)¶
- PyPI. Sign in, go to Your projects → Publishing and add a pending publisher
for the project
trimset: ownertrimset, repositorytrimset, workflowrelease.yml, environmentpypi. The name is free at the time of writing; the pending publisher reserves it. - GitHub environment. Repository Settings → Environments → New environment:
pypi. Optionally add yourself as a required reviewer, so that every upload waits for your click. - GitHub Container Registry. The first push creates the package
trimsetunder the organisation, private. Open Packages → trimset → Package settings and set the visibility to public (once). The image'sorg.opencontainers.image.sourcelabel links the package to this repository. - Rulesets. If a ruleset restricts tag creation, allow the maintainer to create
v*tags.
Nothing in the repository holds a secret for any of this.
If a release goes wrong¶
- The run fails before PyPI. Nothing irreversible happened. Fix, delete the tag
(
git push origin :refs/tags/vX.Y.Z), and tag again. Image tags are overwritten. - PyPI succeeded, a later step failed. Rerun the failed job. Uploading the same files again is skipped, not an error.
- A bad release is out. Yank it on PyPI (it stays installable by exact version but is
no longer chosen), and release
X.Y.Z+1. Never reuse a version.