The Python project and Compiled project distinguish their latest merged source commit from current released-package validation. Publication must preserve both identities rather than treating a tag or package lookup as execution proof.
Prerequisites
- the operational release sequence in Making a Release
- maintainer authority for repository settings and the PyPI project when changing a control
Version rules
Semantic versioning defines the bump semantics. This policy adds one rule with no exceptions: a version that has ever been tagged or uploaded is burned. PyPI permanently rejects any filename it has ever accepted, even after the file is deleted, and deleted releases disappear from PyPI's JSON API, so a reused version fails in the least visible way possible. Never reuse a version; always bump past it. The 6.0.0 and 7.0.0 filenames withdrawn under #814 remain burned even though their releases are absent from the public PyPI JSON API.
Publish-path gates
The publish path is gated as strongly as the merge path. Every control is mechanical:
| Control | Mechanism |
|---|---|
merge to main |
required status checks on the Protect-Main ruleset, including dependency vulnerability checking inside pr_checks_lint |
| tag immutability | Protect-Release-Tags blocks updates and deletion of v* tags without bypass; creation follows repository write permissions |
| manual release source | before loading the triggering source, the publisher uses trusted main code to require the successful canonical manual workflow, the same repository, and a commit reachable from origin/main; a dispatch tag must bind that commit and its package version |
| tag shape | scripts/create_release.py computes the tag from pyproject.toml and validates it against TAG_RE; pr_publish_pypi.yml re-validates the resolved tag and cross-checks it against the built version |
| PyPI upload | trusted publishing (OIDC) only, from the publish_to_pypi job running in the pypi GitHub environment; the PyPI trusted-publisher configuration names that environment |
| filename availability | a pre-build guard queries PyPI and fails loud, with the burned filenames named, before anything is built or uploaded |
Cut stable v<version> tags through Automated Release. A source-binding tag has the separate form v<version>-dispatch-<full merged commit>: it fixes the workflow revision before dispatch and is excluded from previous-release selection. Verify the live tag ruleset before using it. Tag creation alone establishes neither an admitted release source nor permission to publish a package.
Model authority boundary
The release model composes prose only: the release title and the notes body. It has no authority over identifiers. The tag derives from the pyproject.toml version, must match TAG_RE (^v\d+\.\d+\.\d+$), and any model deviation on tag or version aborts the release before a git command runs. Traceability is appended mechanically after the model returns: the merged pull requests since the previous tag, the compare link, and the changelog anchor by scripts/create_release.py, and the artifact SHA-256 digests by the publish workflow once the distributions exist. tests/test_release_contract.py pins this boundary.
Release deliverables
Every GitHub release carries:
- the wheel and sdist that PyPI serves, attached as release assets
sbom.json(CycloneDX), generated from the released wheel environmentprovenance.intoto.jsonl, the Sigstore bundle of the build-provenance attestations, attached as a release asset- SHA-256 digests of every asset, appended to the release body
- GitHub build-provenance attestations for the distributions, served by the GitHub attestation API
- a mechanical traceability section: merged pull requests, compare link, changelog anchor
Assets attach only after the PyPI publish succeeds, so a release with assets is a release that shipped.
Verifying release artifacts
Any consumer can verify that a distribution is exactly what CI built from this repository:
| Step | Command |
|---|---|
| 1 | gh attestation verify vaquum_limen-*.whl --repo Vaquum/Limen |
| 2 | gh attestation verify vaquum_limen-*.whl --repo Vaquum/Limen --bundle provenance.intoto.jsonl |
| 3 | sha256sum vaquum_limen-*.whl |
The first command checks the artifact against the attestation the GitHub API serves. The second performs the same check offline against the downloaded provenance.intoto.jsonl release asset. The third must reproduce the digest recorded in the release body's Artifact SHA-256 block. PyPI serves the same files with PEP 740 attestations through its integrity API.
Recovery from a partial release is asymmetric: when the PyPI publish succeeded but assets are missing, re-run publish_release_assets alone; when the PyPI upload itself failed partway, the version is burned — bump past it. Never re-run the full workflow for a version PyPI already serves; the filename guard rejects it by design.
Dependency vulnerability gate
pr_checks_lint runs governance/check_dependency_vulnerabilities.py: pip-audit over [project.dependencies]. A finding blocks merge unless .github/vuln_exceptions.json carries an entry with id, reason, and expiry; an expired exception fails again. Dependabot (pip and github-actions ecosystems, weekly) proposes the upgrades that keep the gate green.
Workflow hardening invariants
- every workflow declares least-privilege top-level
permissions:; write scopes are job-level only - every action reference is pinned to a full commit SHA; no mutable tags
- every checkout sets
persist-credentials: falseexcept the release tag push - CI dependency installs verify artifact hashes (
--require-hashesagainst the pip-compile-generated sets inrequirements/ci/); the project itself installs--no-deps, andrequirements/constraints.txtremains the human-readable envelope the sets are compiled from - release tag names reach shell steps through
env:indirection, never inline interpolation
tests/test_packaging_surface.py::test_supply_chain_surfaces fails any PR that regresses these invariants; repository settings state is verified by API read and recorded in the delivering PR.