Releasing cafaye-py
Releasing cafaye-py
Section titled “Releasing cafaye-py”A v* tag is a release. Nothing else is: no branch cut, no manual upload,
no second command that has to be remembered.
The shape of a release
Section titled “The shape of a release”- Move
Unreleasedinto a version section first. Onmaster, under## [Unreleased]inCHANGELOG.md, the pending entries are already written (every packet prepends its own). Rename the section they belong to## [X.Y.Z] — YYYY-MM-DDwith today’s date, leaving a fresh empty## [Unreleased]on top, and add the compare link for the new tag at the bottom of the file next to the existing ones. - Bump the version in
pyproject.toml.[project] versionis the single source — there is no__version__anywhere in the tree, and the build (hatchling) reads the version from that one field. The release workflow refuses to ship a tag that disagrees with it. - Gate green, then tag.
./bin/primeon the merge commit. Then the manager cuts the release frommasterwith:git tag vX.Y.Z && git push origin vX.Y.Z. Tags are never created in a worktree and never ahead of the merge.
What the tag does
Section titled “What the tag does”.github/workflows/release.yml, on a v* tag push, in one job:
- The gate —
./bin/prime, the same command CI runs. - A version check: the tag (
vstripped) must equal[project] version. A mismatch fails the job before anything ships. python -m build— the wheel and the sdist, intodist/. The distribution name iscafaye-py; the import package inside iscafaye, so the artifacts arecafaye_py-X.Y.Z-*.whlandcafaye_py-X.Y.Z.tar.gz(hatchling normalises the dash).builditself comes out of the frozen lockfile (a dev dependency), not an unpinned fetch.- A GitHub Release named for the tag, with both artifacts attached and
notes taken byte-for-byte from the
CHANGELOG.mdsection for that version. A tag with no CHANGELOG section fails the job rather than shipping empty notes. - The PyPI publish step — see below. It never fails the release when its precondition is absent; it skips with a sentence.
The job builds once, on Python 3.14: the newest end of CI’s 3.11–3.14
matrix, and the development interpreter mise.toml pins. Same toolchain,
one artifact set.
The one-time registry step
Section titled “The one-time registry step”PyPI publishing is credential-gated on a repository secret named
PYPI_API_TOKEN. It is not configured yet, and that is a supported state:
without it the workflow still cuts the GitHub Release and logs that PyPI
was skipped. To turn publishing on, someone with PyPI access mints an API
token and adds it as that secret, then re-runs the release job. The first
upload of the cafaye-py name registers the project (measured 2026-10-15:
it does not exist on PyPI yet), so the token needs account scope for that
first release and can be scoped to the project afterwards.
The named future is PyPI Trusted Publishing (OIDC), which needs no secret:
register this repository and the release.yml workflow as a pending
publisher on PyPI, then swap the publish step for
pypa/gh-action-pypi-publish with no password input, add
id-token: write to the job’s permissions, and delete the secret. Until
that registration exists an OIDC step would fail the job rather than skip,
which is why the workflow ships the secret-gated form first.