Releases are tagged, built reproducibly, accompanied by an SBOM, and carry a Sigstore-signed build-provenance attestation so a downloader can verify a release artifact was built from this source by this repository's CI. The release tag itself is signed too, going forward (git tag signature, verified by a CI guard before anything builds) — see the one-time setup below. These are two different signatures: one over the build (always present since v0.2.0), one over the tag identity (new; existing v0.1.0/v0.2.0 tags predate it and are not signed).
Repository ruleset 18815834 protects v* tags from update or deletion and
requires signed tags. The reviewable export is
.github/rulesets/release-tags.json. This protects tag identity, but it does not
replace the workflow's independent signature, version, ancestry, and exact-commit
checks.
- Ensure
mainis green:make verify, thea11ygate, and CodeQL all pass. - Update
CHANGELOG.md(move[Unreleased]→ the new version) and bumpversioninpyproject.toml.__version__is derived from the installed distribution (importlib.metadata.version), so there is nothing to hand-edit insrc/habitable/__init__.py— that is the point (REL-02/03: no second place for the version to drift). - From a reviewed commit already on
main, create a signed, annotated tag and push it (see one-time setup below):$ git tag -s vX.Y.Z -m "vX.Y.Z" && git push origin vX.Y.Z - Dispatch the release workflow from trusted
main, naming that existing tag:$ gh workflow run release.yml --ref main -f tag=vX.Y.Z - The
releaseworkflow (.github/workflows/release.yml) first runs a read-only verification/build job. Before building anything, the workflow proves it was dispatched from currentmain; the requested tag is resolved, required to be an ancestor of that branch, and checked out by exact commit; the tag must have a valid signature per.github/allowed_signers, and its version at that commit must matchpyproject.toml. It then re-runsmake verifyat that exact tagged commit — a red, branch-drifted, or unmerged commit cannot ship. Only then does it:- build the wheel + sdist twice, from two independent clean copies of the
tracked source, and verify the two builds are byte-identical
(
make repro→scripts/check_reproducible_build.py) — a non-reproducible build fails the release rather than shipping; - build the linux/amd64 relay twice without cache as OCI archives under the
tagged commit's fixed source epoch, rewrite layer timestamps, and require the
complete archives to be byte-identical (
make relay-repro); this claim is scoped to the pinned base, platform, Dockerfile, BuildKit invocation, and Debian archive state, and the comparison uses a clean archive of tracked source so test caches cannot perturb the image. Archive state is part of the scope because the image applies Debian security updates over the pinned digest — the digest alone leaves published HIGH CVEs unpatched whenever upstream has not rebuilt the base. Two builds seconds apart see the same archive; a rebuild after a later Debian security upload is expected to differ, and that difference is the patch, not a regression; - install the wheel into a clean environment and serve the packaged local app;
- generate a runtime SBOM (CycloneDX) into
dist/sbom.cdx.json; - transfer only those verified assets to a checkout-free publication job;
- re-read the live Git tag object through the GitHub API and require it to match the exact annotated-tag object verified by the read-only job;
- produce a signed build-provenance attestation for the downloaded exact
artifacts (
actions/attest-build-provenance, Sigstore), then create/update the GitHub release and uploaddist/*; - transfer those exact wheel/sdist bytes as a short-lived workflow artifact to
a separate
pypi-publishjob (scoped tocontents: readplusid-token: write), which validates the distribution set and publishes the same files to PyPI via Trusted Publishing (pypa/gh-action-pypi-publish, OIDC — no stored token). The publish action also attaches PEP 740 provenance to those PyPI artifacts. No build tool or source checkout runs in the OIDC-enabled publish job.
- build the wheel + sdist twice, from two independent clean copies of the
tracked source, and verify the two builds are byte-identical
(
The release workflow verifies tag signatures against .github/allowed_signers
using git's SSH signing format. The committed ED25519 public key has fingerprint
SHA256:Kz1JPRtDNVmRa1tD/buR0/iOGDSwEa4P4iu3DN+bElk; its private key remains
maintainer-held. An unsigned tag or a tag signed by any other key fails before
the build begins.
Configure the local checkout to use the matching maintainer-held key:
$ git config gpg.format ssh
$ git config user.signingkey ~/.ssh/github-release-signing.pubKey rotation requires a reviewed update to .github/allowed_signers before a
tag is created; never weaken or skip the signature guard to recover a release.
Trusted Publishing needs a pending publisher registered on PyPI once, which CI cannot do for itself:
- project
habitable, ownerChelseaKR, repositoryhabitable, workflowrelease.yml, environmentpypi; - a matching GitHub Environment named
pypion this repo.
Restrict that environment to the intended release-tag pattern and require any maintainer approval the project chooses. Repository workflow checks cannot replace the external environment policy.
After that, every vX.Y.Z tag publishes with no API token.
Anyone can confirm an artifact came from this repo's CI:
$ gh attestation verify habitable-X.Y.Z-py3-none-any.whl --repo ChelseaKR/habitableThe SBOM (sbom.cdx.json) lists the runtime dependency set for that release.
Beyond the provenance attestation (which proves this repo's CI built the artifact), anyone can independently rebuild a tagged release from source, verify that two clean rebuilds are byte-identical, and then compare those hashes with the published artifacts:
$ git checkout vX.Y.Z
$ make repro
$ shasum -a 256 dist/*This builds the wheel and sdist twice, from two independent clean copies of
the git-tracked source, with a normalized SOURCE_DATE_EPOCH (the tagged
commit's timestamp) and PYTHONHASHSEED, and fails loudly — naming the
differing file(s) — if the two builds don't match byte for byte. On success the
verified artifacts land in dist/, so make repro is a drop-in replacement
for make build that also proves determinism. The release workflow runs this
same check as part of every release; a non-reproducible build blocks the
release rather than shipping.
SemVer for the package. The packet format and verification protocol are
versioned independently and older packets must keep verifying — enforced by the
golden-packet corpus and the version-contract test (see
evidence-method.md and tests/test_golden.py), not by
prose.
The GitHub release and PyPI receive the exact wheel/sdist pair emitted by the
successful make repro run. The read-only job verifies and smoke-tests that pair.
A separate checkout-free job rechecks the live annotated-tag object, attaches
Sigstore build provenance, and publishes the GitHub release; only after that
succeeds may the isolated OIDC job publish the same bytes to PyPI, which adds its
PEP 740 attestations. Dispatch is always from trusted main, so the default
branch cannot be published under an older tag, and the tag commit must already
belong to current default-branch history.