Release Pipeline
How a new version of vfairness gets from a commit to PyPI, what a maintainer does, and how quality is guaranteed at each step.
Releases are automated, tag-triggered, and rehearsed on TestPyPI before they
touch PyPI, with a required human approval for the production publish. The
workflow lives at .github/workflows/release.yml.
Every artifact this pipeline releases to PyPI is built, verified, and
published by CI from a tagged commit, using short-lived OIDC credentials
rather than a stored token. The first pipeline publish, the
0.1.0 beta, has not happened yet: the 0.0.1
upload that has sat on PyPI since March 2026 is a pre-pipeline name
reservation and carries none of these guarantees. See the
Quality
& Hardening page for the gates that run before a release can even
be cut.
At a Glance
- Tag-triggered. Pushing a
vX.Y.Ztag runs the release workflow; normal pushes never publish. - TestPyPI first, then PyPI, with a required human approval before the production publish.
- Trusted Publishing (OIDC). No long-lived PyPI API token is ever stored; the upload uses short-lived, workflow-scoped credentials.
- Every build is verified (
twine check,check-wheel-contents), gets a CycloneDX SBOM, and is installed into a clean environment and smoke-tested before any publish. - Build provenance attestations are attached to every pipeline upload to PyPI automatically.
How a Release Flows
release.yml runs four jobs in order; each runs only if the
previous ones passed, so a failure anywhere stops the release before it
reaches PyPI.
1. Build and verify distribution
Builds the sdist and wheel with python -m build, gates on twine check (metadata and README render) and check-wheel-contents (nothing missing or stray in the wheel), and generates a CycloneDX SBOM. The artifacts are uploaded for the later jobs.
2. Clean-room install smoke test VB-REL-3
Installs the freshly built wheel into a fresh environment and exercises the public API from outside the source tree, so a missing data file, wrong entry point, unshipped subpackage, or missing dependency blocks the release. This catches packaging defects the source-tree test suite cannot see.
3. Publish to TestPyPI
Uploads to TestPyPI via Trusted Publishing as a dress rehearsal. A re-run is idempotent (skip-existing).
4. Publish to PyPI
Runs only after a maintainer approves the pypi environment, then uploads to PyPI via Trusted Publishing with provenance attestations.
Cutting a Release
For a maintainer, a release is four small steps; the pipeline does the rest.
- Confirm the quality gates are green on
main(see Quality & Hardening). - Bump the version. There is a single source of truth in
src/vfairness/__init__.py(__version__); the build reads it from there. - Update the changelog. Move the
[Unreleased]entries under a new[X.Y.Z]heading with the date. - Tag and push an annotated tag:
git tag -a v0.1.0 -m "vfairness 0.1.0"
git push origin v0.1.0
Build, verify, smoke, and TestPyPI run automatically. When the run reaches
the pypi environment it pauses for approval; approve it to
publish to PyPI.
The beta ships as a normal SemVer 0.x version with the
Development Status :: 4 - Beta classifier, so
pip install vfairness finds it without the --pre
flag (a pre-release suffix would hide the package from every default
resolve, the wrong trade for a library seeking testers). SemVer already
reserves API stability for 1.0.0. PEP 440 pre-release
suffixes such as 0.9.0b1 remain available for rehearsing a
specific future release; those do require
pip install --pre vfairness.
One-Time Setup (Prerequisite)
Before the first release, two things must exist. They are configured once by a maintainer with PyPI and repository-admin access.
- A Trusted Publisher for project "vfairness" on both PyPI and TestPyPI, bound to the owner, this repository, and workflow
release.yml. - GitHub Environments
testpypiandpypi, with a required reviewer onpypiso a human approves the production publish.
The publish jobs fail closed; the build, verify, and smoke jobs still run, so the pipeline is exercised end to end without publishing.
How Quality Is Guaranteed
Quality is enforced in two layers, so a release can only ship code that has already cleared every gate.
Before a change can reach a release
Every push and pull request that touches the library must pass the standing gates: ruff lint and format, mypy (with a strict island on the core), the full test suite on Python 3.11 / 3.12 / 3.13, branch coverage plus a PR-only diff-cover gate, security scanning (Bandit and pip-audit), and, when the relevant paths change, cross-library metric parity and the Pulse output contract. Mutation testing runs weekly and on demand as an informational report, not a blocking gate. CodeQL and an OpenSSF Scorecard are configured but stay skipped while the repository is private. The full inventory is on the Quality & Hardening page.
At release time
The pipeline runs additional checks on the exact artifact that will ship: twine check, check-wheel-contents, a CycloneDX SBOM, and the clean-room install smoke test. TestPyPI is the dry run before PyPI.
Security of the Pipeline
- No stored PyPI token. Publishing uses OIDC short-lived credentials via PyPI Trusted Publishing, so there is no long-lived secret to leak or rotate.
- Provenance attestations are attached to every pipeline upload, so consumers can verify such an artifact was built by this workflow. The pre-pipeline
0.0.1upload from March 2026 carries no attestations; the first attested artifact will be the0.1.0beta publish. - A CycloneDX SBOM records the dependency set of each release.
- TestPyPI-first and a required human approval stand between a tag and a production publish.
- Least-privilege tokens. The workflow runs with
contents: read, andid-token: writeis granted only on the publish jobs that need it.
Versioning and Stability
- Semantic Versioning, with a single source of version truth in
src/vfairness/__init__.py. - The beta line is plain SemVer 0.x (
0.1.0,0.2.0, ...) carrying the Beta trove classifier; PEP 440 pre-release suffixes (0.9.0b1) stay available for rehearsals and install withpip install --pre. - The frozen public surface and deprecation policy govern what a version bump may change (see
API_STABILITY.md); every change is announced in the changelog.
If Something Goes Wrong
A PyPI version cannot be overwritten. To correct a bad release, publish a new patch version and, if needed, yank the bad one on PyPI (yank hides it from new resolves without breaking existing pins). TestPyPI is the rehearsal: a failure there stops the pipeline before it touches PyPI.