ADR 0035: Publish each release to PyPI from the release workflow, with trusted publishing¶
- Status: Accepted
- Date: 2026-10-23
Context¶
v0.1.0 is the first release meant for users: the scanner and the GitHub Action are done and evaluated (scanner evaluation). Until now a release only made a tag and a GitHub release (ADR 0005). People who want the command-line tool should be able to run pip install modelbump or uvx modelbump, and the version on PyPI must be the one that was tagged.
Decision¶
- Publishing is part of
release-please.yml. Two jobs run only when release-please reportsrelease_created, that is, when the release PR is merged:- build checks out the new tag, checks that the
pyproject.tomlversion equals the tag without itsv, runsuv build, installs the wheel in a clean environment and scans the demo app (tests/fixtures/action-demo/), which must fail with--fail-within 30because it uses retired models. This proves the wheel carries the registry data, not just the code. - publish uploads that same
dist/withpypa/gh-action-pypi-publish.
- build checks out the new tag, checks that the
- Trusted publishing, not an API token. PyPI is told to trust this repository's
release-please.ymlrunning in the GitHub environmentpypi. The job gets a short-lived token through OpenID Connect (id-token: write, only on that job), so no PyPI secret is stored anywhere. The project is created on PyPI by a "pending publisher" on the first upload. - The package description says what exists now (find model IDs and warn before retirement), not the whole plan, and the README links use full URLs so they work on PyPI.
- The Action keeps installing from its own tag (ADR 0026), not from PyPI, so the two cannot drift apart.
- The GitHub Marketplace listing is a one-time manual step on the v0.1.0 release; later releases keep it.
Alternatives considered¶
- A separate workflow triggered by
release: published: releases made with the release-please token do trigger it, but the version check and the tag would then live in two files. One workflow keeps the whole release in one place. - A PyPI API token in a repository secret: works, but it is a long-lived secret that can leak and has to be rotated. PyPI recommends trusted publishing.
- Publishing by hand with
uv publish: easy to forget, and the uploaded files would not be built from the tag by CI. - TestPyPI first: useful for a first try, but the smoke test already installs the built wheel, and a second publisher doubles the setup. It can be added if an upload ever goes wrong.
Consequences¶
- Merging the release PR is the only step to release: tag, GitHub release, changelog and PyPI upload all follow from it.
- A wrong version, a broken wheel or missing registry data stops the release before anything reaches PyPI. A PyPI version can never be uploaded twice, so a failed publish is fixed with a new patch release.
- If the workflow file or the environment is renamed, the trusted publisher on PyPI must be updated too.