How to cut a release of ftrio to PyPI. Publishing is automated by
.github/workflows/publish.yml using PyPI Trusted Publishing (OIDC): a
published GitHub Release builds the distributions and uploads them, with no API
tokens or secrets.
These are configured once and reused for every release. Verify they are in place before the first publish:
| Side | Setting | Value |
|---|---|---|
| PyPI pending/trusted publisher | PyPI Project Name | ftrio |
| PyPI pending/trusted publisher | Owner | FtrOnOff (the GitHub owner, must match the repo) |
| PyPI pending/trusted publisher | Repository name | ftrio-python |
| PyPI pending/trusted publisher | Workflow name | publish.yml |
| PyPI pending/trusted publisher | Environment name | pypi |
| GitHub repo | Environment | pypi exists (Settings -> Environments) |
GitHub pypi environment |
Required reviewers | a release approver (e.g. yourself) |
GitHub pypi environment |
Deployment tags | restricted to v* |
Notes:
- The PyPI publisher Owner is a GitHub identity and must equal the owner of the repo the workflow runs from; it is not the PyPI organization name, even if they happen to share a value.
- To have the project owned by a PyPI organization, create the publisher under the organization (once approved) or publish under a personal account and transfer the project to the organization afterwards.
Two rulesets (Settings -> Rules -> Rulesets) protect the release path. Add Repository admin to each ruleset's bypass list so a solo maintainer keeps an escape hatch and can still create release tags.
protect-main(branch ruleset, targets the default branch): require the CI status checks (test (3.11),test (3.12),test (3.13)) to pass and be up to date, require a pull request (0 approvals for a solo maintainer), block force pushes, and restrict deletions.protect-release-tags(tag ruleset, targetsv*): restrict updates and deletions so a published release tag can never be moved or removed, and optionally restrict creations to maintainers.
These are independent of, and complementary to, the pypi environment's v*
deployment rule: the rulesets govern who can change branches and tags, the
environment rule governs which tags may publish.
-
Confirm
mainis green. The CI workflow must pass (ruff, mypy, pytest).ruff check ftrio playground tests && mypy ftrio && pytest -
Bump the version in one place:
pyproject.toml->[project] version.That is the single source of truth.
ftrio.__version__is derived from it at runtime via the installed package metadata (importlib.metadata), so there is nothing else to edit. The built sdist/wheel always carry the bumped version.Follow SemVer: patch for fixes, minor for backward- compatible features, major for breaking changes.
Local note: an editable install caches the version in its metadata at install time, so after bumping
pyproject.tomlre-runpip install -e .if you wantftrio.__version__to reflect the new value in your dev checkout. This does not affect releases, which build the distributions fresh frompyproject.toml. -
Update
CHANGELOG.md. Move the items under## [Unreleased]into a new## [X.Y.Z] - YYYY-MM-DDsection (use the real release date), and refresh the comparison links at the bottom. -
Commit the version bump and changelog on
main(via PR if the branch is protected).git commit -am "Release vX.Y.Z" -
Tag and push. The tag must start with
vto satisfy the environment'sv*deployment rule.git tag vX.Y.Z git push origin main --tags
-
Create the GitHub Release. From the tag (Releases -> Draft a new release -> choose
vX.Y.Z). Paste the changelog section as the notes and publish it. This is what triggerspublish.yml. -
Approve the deployment. The
buildjob runs, then thepublishjob pauses at thepypienvironment for required-reviewer approval. Open the workflow run in the Actions tab and approve it. The upload to PyPI then proceeds. -
Verify. Confirm the new version is live and installable:
pip index versions ftrio # shows the published versions pip install "ftrio==X.Y.Z"
Use the workflow's manual trigger to build and twine check the distributions
without uploading: Actions -> Publish to PyPI -> Run workflow. The publish
job is skipped on a manual run (it only runs on a real Release event), so this
validates the build in isolation.
- Upload rejected / identity mismatch: the PyPI publisher Owner / Repository / Workflow / Environment must exactly match the run. The most common cause is the Owner not matching the GitHub repo owner.
- Deployment never starts: check the tag matches the
v*environment rule and that the Release event fired the workflow. - Name already taken:
ftriois a global PyPI name; if it is claimed by someone else, rename the distribution inpyproject.toml.