Rendered from RELEASING.md at build time without changing its status. The repository source controls if this presentation differs.
Release procedure#
Acronyms: application programming interface (API); carriage return and line feed (CRLF); digital object identifier (DOI); hash-based message authentication code (HMAC); line feed (LF); Secure Hash Algorithm 256-bit (SHA-256); Software Bill of Materials (SBOM); Coordinated Universal Time (UTC); ZIP archive format (ZIP).
Public releases are built only from a commit already present in the public repository. The release manifest is published beside the source ZIP rather than tracked inside the source tree. This avoids a self-referential commit field and lets the manifest bind an exact, publicly resolvable commit.
Development branches may record precise contradictions with TIME.md set to Status: OPEN; normal pull-request checks do not prohibit that state. Publication is different: the owner-dispatched release workflow runs python scripts/check_time_status.py against the exact tagged checkout and fails unless it contains exactly one Status: CLEAR line. There is no subjective override.
The source version may be prepared as 1.2.0 while the release does not exist. During that period, CHANGELOG.md says UNRELEASED, CITATION.cff identifies a release candidate and has no date-released, and install instructions distinguish a source checkout from the latest published package. Before tagging, land an explicit release-finalization change that uses the actual publication date consistently in the changelog and citation metadata; do not fabricate or backdate it. The release workflow enforces this with python scripts/check_release_metadata.py --version <version> and also refuses release-candidate Zenodo metadata.
- Merge the versioned release change through the public pull-request workflow. Require the protected repository-check aggregate to pass on the exact candidate commit.
- From a clean checkout of that commit, run:
python -m pytest -q
python -m compileall -q src
- Build a pre-tag candidate from the full commit SHA, not a working directory:
VERSION=1.2.0
python scripts/release_artifacts.py build \
--ref FULL_PUBLIC_COMMIT_SHA --release "$VERSION" --output-dir dist/candidate
The builder uses git archive, records exact Git-blob member bytes, and rewrites the source ZIP with a UTC commit timestamp, stable Unix modes, sorted members, and stored compression. It builds both the wheel and standard source distribution twice from separate source extractions with SOURCE_DATE_EPOCH set to the commit timestamp. Generated packaging text is normalized to LF; wheel RECORD is rebuilt after normalization; ZIP metadata, tar metadata, gzip metadata, ownership, modes, and member order are canonical. The build fails unless each pair is byte-identical and both distributions declare verifier-standard, version 1.2.0, import package verifier, and the frozen three console scripts. It also emits a deterministic CycloneDX 1.6 SBOM whose components bind the source ZIP, wheel, and source distribution by SHA-256 and byte size. The external manifest binds the SBOM digest; the SBOM does not list itself, avoiding self-reference.
The protected repository-check aggregate separately builds this complete artifact set on Windows and Linux and compares every byte. Do not prepare a tag unless that cross-platform comparison passed on the exact candidate commit.
- Run
twine checkon the candidate wheel and source distribution. Install the candidate wheel with--no-deps. Run the generic receipt lifecycle,vstd hardware list --json, and the deterministic virtual probe/verification lifecycle. Test each optional dependency profile independently; never place a test HMAC key in a committed fixture. - Require a zero-match boundary scan of the candidate source archive, wheel, source distribution, external manifest, and SBOM for private project names, proprietary model identifiers, local or home-directory paths, credentials, and personal email addresses.
- Confirm
python scripts/check_time_status.pypasses, release-candidate metadata has been finalized with the actual intended publication date, and then create the release tag locally at the exact tested commit. Before creating the tag, require GitHub release immutability to be enabled:
gh api -H "X-GitHub-Api-Version: 2026-03-10" \
repos/TimeLordRaps/verifier/immutable-releases
The response MUST contain "enabled": true. GitHub release immutability is a repository setting that applies only to future releases; GitHub documents both the repository setting and the versioned API. GitHub documents this endpoint as requiring repository Administration read permission. The default Actions token cannot request that permission, so the release workflow must not present its token as an independent administrative recheck. Instead, the repository owner performs this authenticated preflight immediately before dispatching the existing release.yml workflow and supplies its required immutable_releases_preflight=true control input. The workflow requires the dispatcher to equal the repository owner and verifies the published release's immutable field as a postcondition. Immutability locks the published tag and attached assets and generates a GitHub release attestation; it does not correct false metadata. Corrections, revocations, and superseding releases remain additive. Enable the setting only after the draft-first workflow is present on the protected release commit.
Prefer a cryptographically signed annotated tag when the maintainer's signing key is registered and available. Rebuild using the tag coordinate. The source ZIP, wheel, and source distribution MUST be byte-identical to the commit-coordinate candidate. The SBOM also remains byte-identical because it binds the resolved commit rather than the ref spelling. The external manifest MUST differ only where its source.ref changes from the full commit SHA to the tag ref, plus the manifest's own resulting digest:
VERSION=1.2.0
git tag -s "v$VERSION" FULL_PUBLIC_COMMIT_SHA
python scripts/release_artifacts.py build \
--ref "refs/tags/v$VERSION" --release "$VERSION" --output-dir dist/tagged
If tag signing is unavailable, an unsigned annotated tag is permitted only through .github/workflows/release.yml. That workflow records the GitHub tag-object verification result and reason in the release notes and MUST create GitHub/Sigstore artifact attestations for the source ZIP, wheel, source distribution, SBOM, and external manifest. An artifact attestation is not described as a tag signature.
- Run the verifier independently before upload:
VERSION=1.2.0
python scripts/release_artifacts.py verify \
"dist/tagged/verifier-standard-$VERSION.manifest.json"
The manifest's source ref MUST resolve to its recorded public commit. The source ZIP file set and every member byte MUST match that commit. CRLF/LF equivalence is not accepted as byte identity.
- Push the tag only after all preceding checks pass. Re-run the owner-authenticated immutable-release API preflight, then dispatch
.github/workflows/release.ymlfrom the protected default branch with the exact existing tag andimmutable_releases_preflight=true. The owner-dispatched release workflow rechecks signed-tag identity, protected-main ancestry, package version, the successful protected repository-check aggregate (theconformance-gatestatus context), the full test suite, final metadata, deterministic build, installed wheel, and artifact manifest. The administrative preflight is owner-observed rather than Actions-token-observed; the workflow stops unless that fact is explicitly supplied and refuses a non-owner dispatch. It then attests the tested source ZIP, wheel, source distribution, SBOM, and external release manifest. The workflow creates a draft, attaches the complete set, and only then publishes it. A second job can access only the wheel and source distribution, requires approval in the protectedpypienvironment, and publishes them through the configured PyPI Trusted Publisher. Existing tags and release assets remain untouched; corrections are additive. - Verify each downloaded asset with both the external manifest and:
gh attestation verify PATH_TO_ASSET --repo TimeLordRaps/verifier
An attestation and SBOM complement but do not replace the release manifest, and neither turns an unsigned tag into a signed tag.
- Let Zenodo archive the GitHub release, then record the issued DOI additively.
- Confirm that
https://pypi.org/project/verifier-standard/1.2.0/lists the same wheel and source-distribution SHA-256 values as the GitHub release and external manifest. PyPI ownership establishes control of the distribution coordinate only; it does not establish adoption, consensus, certification, or exclusive control of the Python import nameverifier.