Releasing pyroid¶
Owner-only instructions. Everything here needs repository-admin or PyPI-owner rights, and no automation does
it for you. Do the one-time setup (sections 1–4 and 8) once, before the first 1.0 push to main.
.github/workflows/release.yml runs release-please on every
push to main. release-please keeps a release PR open (chore: release X.Y.Z) that bumps the version in
Cargo.toml/Cargo.lock and updates CHANGELOG.md from Conventional Commits. Merging that PR tags vX.Y.Z
and creates the GitHub Release. The same workflow run then builds 24 wheels and the sdist, tests every wheel in
a clean venv and attests build provenance. It waits for your approval in the pypi environment, then
publishes with Trusted Publishing: no API tokens, and PEP 740 attestations are uploaded with the files.
1. GitHub repository settings¶
- Settings → Actions → General
- Workflow permissions: Read repository contents and packages permissions (the workflow asks for more per job).
- Tick Allow GitHub Actions to create and approve pull requests. release-please needs this to open
the release PR with the default
GITHUB_TOKEN. - Settings → Secrets and variables → Actions: delete the old
PYPI_API_TOKENsecret (0.7.x used it), and revoke that token on PyPI (Account settings → API tokens). Nothing in the new workflow uses a token. - Labels used by the issue forms: create
parity(thebugandenhancementlabels exist by default). - Discussions (optional): the issue chooser links "Ask a question" to Discussions. Either enable
Discussions (Settings → General → Features) or remove that link from
.github/ISSUE_TEMPLATE/config.yml.
2. Environments¶
Settings → Environments → New environment, twice:
| Environment | Required reviewers | Deployment branches and tags | Used by |
|---|---|---|---|
pypi |
you | Selected branches and tags: main only |
job publish to PyPI |
testpypi |
you | Selected branches and tags: main (add a branch pattern if you want dry runs from branches) |
job publish to TestPyPI |
For both, leave Allow administrators to bypass configured protection rules unticked, and don't add any secrets. "Prevent self-review" must stay off if you are the only reviewer.
3. Trusted publishers¶
The publisher must match the workflow exactly: owner ao, repository pyroid, workflow release.yml, and the
environment name.
- PyPI:
pyroidalready exists there (0.7.x), so this is a normal publisher, not a pending one. https://pypi.org/manage/project/pyroid/settings/publishing/ → Add a new publisher → GitHub Actions: ownerao, repositorypyroid, workflow namerelease.yml, environment namepypi. - TestPyPI:
pyroiddoesn't exist on TestPyPI yet (checked 2026-11-05), so register a pending publisher. https://test.pypi.org/manage/account/publishing/ → Add a new pending publisher: PyPI project namepyroid, ownerao, repositorypyroid, workflow namerelease.yml, environment nametestpypi. The first successful upload creates the project. A TestPyPI account is separate from your PyPI account.
Docs: adding a publisher, pending publishers.
4. Private vulnerability reporting¶
Settings → Advanced Security (called "Code security" on some accounts) → Private vulnerability reporting
→ Enable. SECURITY.md and the issue chooser send reporters to
https://github.com/ao/pyroid/security/advisories/new, which only works once this is on.
GitHub docs.
Also confirm the Code of Conduct contact (CODE_OF_CONDUCT.md, "Reporting an Issue"): it currently names
support@ataiva.com, the address in the package metadata. Change it if conduct reports should go elsewhere.
5. First release dry run (TestPyPI)¶
After next is merged into main and pushed:
- Actions → Release → Run workflow, branch
main, tick testpypi. - Wait for the build, test and
check distributionsjobs. The run summary lists the 25 files. - Approve the
testpypideployment when the run asks for it. - Check the result from a clean venv (versions come from
Cargo.toml;1.0.0-rc.1is1.0.0rc1on TestPyPI, andpyroid.__version__is that PEP 440 string too):
uv venv /tmp/pyroid-tp && uv pip install --python /tmp/pyroid-tp --index-url https://test.pypi.org/simple/ pyroid==1.0.0rc1
/tmp/pyroid-tp/bin/python -c "import pyroid; print(pyroid.__version__)" # 1.0.0rc1
On https://test.pypi.org/project/pyroid/#files, each file should show a "Verified" provenance badge
(PEP 740).
5. TestPyPI never accepts the same filename twice, even after you delete a release. To dry-run again, change the
pre-release version in Cargo.toml first (e.g. 1.0.0-dev.1, which is 1.0.0.dev1; then
cargo update --workspace) in a regular PR. The version must be one PEP 440 can read (-alpha.N,
-beta.N, -rc.N, -dev.N): maturin refuses to build any other.
6. Releasing 1.0.0¶
- On the first push to
main, release-please reads the commits since thev0.7.0tag and openschore(main): release 1.0.0.release-please-config.jsonpins"release-as": "1.0.0", so the first release is 1.0.0 however the history is merged. The breakingchore!:commit would also give 1.0.0, but a squash merge could drop its!. - Review the PR: the root
Cargo.toml([workspace.package] version) and thepyroid-core/pyroid-pyentries inCargo.locksay1.0.0,.release-please-manifest.jsonsays1.0.0, andCHANGELOG.mdhas the generated 1.0.0 section. The hand-written## 1.0.0 (unreleased)section, the summary of what 1.0 contains, ends up just below the generated one. Move its text under the generated heading and delete its own heading, in a normal commit onmain(release-please refreshes the PR), or straight after the release. - CI doesn't run on the release PR itself, because PRs opened with
GITHUB_TOKENdon't trigger workflows. That's acceptable: the PR only changes versions and the changelog, and the same build and test matrix runs again after the merge, before anything is published. To get checks on the PR anyway, close and reopen it. - Merge the release PR. The
Releaserun onmaintagsv1.0.0, creates the GitHub Release, builds and tests everything, attests provenance, and then waits at publish to PyPI. Approve thepypideployment. - Afterwards:
- Remove
"release-as": "1.0.0"fromrelease-please-config.json(commitchore: stop pinning release-as). It is sticky: while it is there, every later release PR proposes 1.0.0 again (verified offline with release-please 17.6.0). - Check https://pypi.org/project/pyroid/1.0.0/#files: 25 files, each with provenance.
- Optionally verify a file's GitHub attestation:
gh attestation verify pyroid-1.0.0-cp311-abi3-*.whl --repo ao/pyroid.
7. Later releases¶
Merge PRs with Conventional Commit titles (see CONTRIBUTING.md). feat gives a minor bump, fix/perf a
patch, and !/BREAKING CHANGE a major. Merge the release PR when you want to ship, then approve pypi.
For a pre-release, add a Release-As: 1.1.0-rc.1 footer to a commit on main (the wheels become
1.1.0rc1), and use Release-As: 1.1.0 for the final release.
If a job fails after the tag was created and it was flaky, use Re-run failed jobs on the same run. That
keeps the release-please outputs, so it can still publish. If the code is broken, fix it on main with a
fix: commit and ship the next patch release. Don't move or reuse the tag.
Never publish from a laptop, and don't add an API token as a fallback.
The wheels are built with profile-guided optimization (PGO). To build a run without it, start the workflow
by hand with pgo: false, or set the repository variable PYROID_PGO=off (this also covers release runs
on push). A platform whose matrix entry sets no_pgo: true always builds plain wheels. windows-arm64 does,
because llvm-profdata could not read the profiles its instrumented wheels wrote ("symbol name is empty").
Remove no_pgo there once that is fixed.
How the version is bumped: release-please-config.json uses release-type: simple with TOML extra-files.
The rust release type can't handle a virtual workspace whose crates use version.workspace = true: it throws,
and no release PR appears. The extra files update [workspace.package].version in Cargo.toml and the
pyroid-core and pyroid-py entries in Cargo.lock. If you rename a crate or add a workspace crate, add it to
the Cargo.lock filter, or --locked builds fail on the release commit. The log line
file version.txt did not exist is expected and harmless.
8. Docs site (GitHub Pages)¶
.github/workflows/docs.yml builds the site (mkdocs.yml, sources in docs/) on every PR and push, with
zensical build --strict and a mkdocs build --strict link check. It deploys only on a push to main, and
only if the github-pages environment exists. A first job looks the environment up and, if it is missing,
skips the deploy with a notice instead of creating it. Nothing is published until you do this once:
- Settings → Pages → Build and deployment → Source: GitHub Actions. This creates the
github-pagesenvironment. - Settings → Environments →
github-pages: check that Deployment branches and tags allowsmainonly (GitHub's default for this environment). No reviewers or secrets are needed. - Push to
main(or re-run the latestdocsrun onmain). Thedeploy to GitHub Pagesjob publishes https://ao.github.io/pyroid/. The README's Docs, Benchmarks and Compatibility links point there, and 404 until then.
To stop publishing, set the Pages source back to "Deploy from a branch" (or delete the environment). The workflow then goes back to build-only.
The README hero (docs/assets/hero-{light,dark}.svg) is a copy of charts/hero-{light,dark}.svg from the run
folder that bench/consolidate.py wrote; it currently comes from the AWS EC2 c7i.4xlarge dev-slice run of
2026-11-06. README.md embeds it by absolute raw.githubusercontent.com URL, so it also renders on PyPI.