Release Process¶
This is the runbook for cutting an otto release. The mechanics are driven by
make release locally and the release.yml workflow on GitHub — you should
not need to edit version strings or upload artifacts by hand.
Overview¶
Otto uses a static version in pyproject.toml, bumped by
bump-my-version, and a
CHANGELOG.md regenerated from Conventional Commits
history by git-cliff. Publishing to PyPI happens in
GitHub Actions via PyPI trusted publishing (OIDC) — there are no long-lived
tokens in the repository.
The distribution name is otto-sh; the installed CLI command is otto.
Cutting a release¶
Land all changes on
main. Releases are cut from a cleanmain. Commit messages should follow Conventional Commits so git-cliff can classify them in the changelog.Run
make release. This validates and prepares everything locally:make release # patch bump (default) make release BUMP=minor # or minor / major make release NEW_VERSION=0.4.0rc1 # explicit version, e.g. a prereleasemake releaserunstypecheck, builds the docs, runs the full nox matrix across every supported Python (this requires the dev VM with Vagrant hosts up), regeneratesCHANGELOG.mdat the new version, bumps the version inpyproject.tomlanduv.lock, commits, tagsvX.Y.Z, and buildsdist/.Push the tag:
git push --follow-tags
What the tag push triggers¶
Pushing a v* tag fires .github/workflows/release.yml:
A guard job inspects the tag. Only final-version tags (
vX.Y.Z) proceed to PyPI. Prerelease tags (v0.4.0rc1,a2,b1,.dev0) are skipped here — those are for the TestPyPI dry-run workflow instead.build produces the sdist and wheel with
uv build.publish uploads to PyPI via OIDC, gated by the
pypiGitHub Environment.github-release creates the GitHub Release, attaching the artifacts and release notes generated by git-cliff.
TestPyPI dry-run¶
To rehearse a release without touching production PyPI, dispatch
release-testpypi.yml
from the Actions tab (workflow_dispatch). It builds and uploads to TestPyPI
via the testpypi environment. Prerelease versions (e.g. 0.4.0rc1) are the
natural fit for this path.
Manual fallbacks¶
If the workflow is unavailable, dist/ can be uploaded by hand with a
UV_PUBLISH_TOKEN in the environment:
make publish-test # upload dist/ to TestPyPI
make publish # upload dist/ to PyPI — permanent
Prefer the tag-driven workflow; the manual targets exist only as a fallback.