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

  1. Land all changes on main. Releases are cut from a clean main. Commit messages should follow Conventional Commits so git-cliff can classify them in the changelog.

  2. 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 prerelease
    

    make release runs typecheck, builds the docs, runs the full nox matrix across every supported Python (this requires the dev VM with Vagrant hosts up), regenerates CHANGELOG.md at the new version, bumps the version in pyproject.toml and uv.lock, commits, tags vX.Y.Z, and builds dist/.

  3. 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 pypi GitHub 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.