Release checklist¶
The procedure for cutting a release of openauc-io. It is written down so a release is a repeatable act rather than a remembered one.
Status — 0.1.0a1 is released
v0.1.0a1 was tagged and published as a GitHub pre-release, and
openauc 0.1.0a1 is on
PyPI. The machinery below has
now been run to completion, not only in dry-run form.
Publication went through Trusted Publishing: publish.yml mints a
short-lived OIDC token per run, and the protected pypi GitHub environment
is the only place that token is issued from. There is no PyPI API token.
This note records a point in time, not a permanent property of the project. Update it when the next release is made — the "After the release" steps below say so, and no test pins it.
What is automated and what is not¶
| Step | Automated |
|---|---|
| Lint, format, types, tests, strict docs build | scripts/release_check.py, and CI on every pull request |
| Coverage floor | fail_under in pyproject.toml, enforced by every pytest run |
| Build, metadata check, artifact verification, clean-environment smoke test | .github/workflows/release.yml (Release dry run) |
| Tagging | Manual |
| GitHub Release creation and publication | Manual |
| Rebuild + verification of the tagged source | .github/workflows/publish.yml, after the Release is published |
| PyPI upload | .github/workflows/publish.yml, after the Release is published |
The dry-run workflow holds permissions: contents: read and contains no upload
step, so it cannot publish even if invoked by mistake. It remains publish-free
deliberately, and tests pin that.
The two release workflows¶
Release dry run (release.yml) |
Publish to PyPI (publish.yml) |
|
|---|---|---|
| Trigger | workflow_dispatch, pull_request |
release: published only |
| Publishes | Never | Yes, to PyPI |
| OIDC identity | None | id-token: write, publish job only |
| GitHub environment | None | pypi |
publish.yml has two jobs, and the split is the security boundary:
build-and-verifychecks outgithub.sha— the immutable commit the Release was published from, rather than re-resolving the tag name — runs the full release check suite, builds from a cleandist/, runstwine check --strict, runsscripts/verify_artifacts.py— which on areleaseevent also asserts that the tag isv<version>, derived from the sources rather than hard-coded — smoke-tests the real wheel in a fresh virtualenv, and uploads the distributions as an Actions artifact. It holds no publishing identity.publish-to-pypidownloads that artifact and uploads it withpypa/gh-action-pypi-publish. It does not check out the repository, install dependencies or run any project code. Its only privilege isid-token: write.
Every external action in publish.yml — not just the publishing one — is
pinned to a full commit SHA with the release named in a comment beside it, and
a test fails the suite if any pin is reverted to a mutable tag.
There is no workflow_dispatch on publish.yml: a published GitHub Release is
the only way to start a publication. GitHub's ordinary re-run of that run, or
of its failed jobs, is still available and keeps the original event's commit and
tag, so it republishes exactly the same thing. Re-running is the right move when
publication failed before any distribution was accepted — a mistyped Trusted
Publisher or a missing pypi environment, say. That is exactly what happened on
the first attempt at 0.1.0a1: the OIDC exchange was rejected with
invalid-publisher ("valid token, but no corresponding publisher"), which
fails before any upload, so correcting the publisher configuration and
re-running the failed job of the same run was sufficient. If publication partly succeeded,
or the version already exists on PyPI, investigate the PyPI state and follow
release recovery instead; skip-existing is not set, deliberately, so that
condition fails loudly rather than passing in silence.
Publication identity¶
Both of these are human configuration steps performed in GitHub and PyPI settings. No repository code performs them. Both now exist; what follows is what to verify before each release, not what to create again.
1. The pypi GitHub environment¶
An environment named exactly pypi under Settings → Environments, carrying
the protection rules available for the repository — required reviewers, and a
deployment-branch/tag rule limiting it to release tags — so that publication
has a human gate in addition to the Release gate. Publication of 0.1.0a1
paused at that gate until a human approved it.
2. The PyPI Trusted Publisher¶
The openauc project on PyPI has an active Trusted Publisher under
Manage project → Publishing. Confirm it still reads exactly:
| Field | Value |
|---|---|
| PyPI project name | openauc |
| Owner | ronfinn |
| Repository name | openauc-io |
| Workflow name | publish.yml |
| Environment name | pypi |
Any mismatch — including an empty environment field — makes the OIDC exchange
fail with invalid-publisher. Verify it; do not add a second publisher, and do
not create a pending publisher: pending publishers exist only to create a
project that does not yet exist on PyPI, which was how openauc was first
created and is no longer the applicable procedure for this repository.
Trusted Publishing uses a short-lived OIDC token minted per run. There is no PyPI API token, username or password anywhere in this repository or its secrets, and none should ever be added.
Before the release¶
- Working tree clean, on a release branch cut from current
main. - Confirm the version in
src/openauc/__init__.py— hatch reads it from there — and thatCITATION.cffdeclares the same version. A test enforces this. CHANGELOG.mdhas a section for the version, with its entries written and the release date filled in.- Run every gate:
- Build and verify the artifacts:
$ rm -rf dist
$ uv build
$ uv run python scripts/verify_artifacts.py
$ uvx twine check --strict dist/*
- Install the wheel alone into a throwaway environment and exercise it:
$ python -m venv /tmp/openauc-smoke
$ /tmp/openauc-smoke/bin/pip install dist/openauc-*.whl
$ /tmp/openauc-smoke/bin/openauc version
$ /tmp/openauc-smoke/bin/openauc formats
- Run the Release dry run workflow from the Actions tab and confirm it is green.
- Confirm the
pypiGitHub environment exists and is protected. - Confirm the project's active PyPI Trusted Publisher is configured exactly as tabulated above.
The release itself¶
Only once every step above passes.
Human, in order:
- Create the tag
v<version>on the release commit and push it. Pushing the tag publishes nothing — no workflow reacts to it. - Create the GitHub Release from that tag, with the
CHANGELOG.mdsection as its body. - Mark it as a pre-release for any
aN/bN/rcNversion — so, forv0.1.0a1, yes. - Publish the Release. A draft Release triggers nothing; publishing is the act that authorises the upload.
Automated, and only now:
publish.ymlrebuilds and re-verifies the tagged source, confirming the tag matches the packaged version.- It publishes those verified distributions to PyPI over Trusted Publishing OIDC, with attestations left at their default.
After the release¶
Verify, by hand:
- The project and version exist on PyPI.
pip install openauc==<version>succeeds in a fresh environment — for a pre-release, with--preor the exact version.- The installed version is the expected one, and
openauc versionandopenauc formatswork. - The publication carries the provenance/attestation information PyPI shows for Trusted Publishing uploads.
Then update the repository:
- Replace
- unreleasedin theCHANGELOG.mdsection heading with the release date, and open the next## [Unreleased]section. - Update the status note at the top of this page, the version line on the
project index, and the README installation instructions, so none
still says the version is unpublished.
0.1.0a1is the worked example. - Record the release in
docs/project/roadmap.mdand the development log. - Bump the version in
src/openauc/__init__.pyandCITATION.cff, and updateCITATION.cff's publication metadata, when work on the next version begins.
None of these are enforced by tests, deliberately: the tests pin what the machinery may do, not which point in the release cycle the repository currently occupies. Those durable boundaries are:
- the Release dry run may build and verify, and may never publish;
- only
publish.ymlmay publish to PyPI, and only on a published Release; - no automation manufactures or pushes a release tag;
- no automation creates or publishes a GitHub Release.
No test depends on whether v0.1.0a1 currently exists — a clone that has
fetched the tag must still pass the whole suite.