Maintained repository documentationView source on GitHub

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); continuous integration (CI); digital object identifier (DOI); Extensible Markup Language (XML); hash-based message authentication code (HMAC); identifier (ID); Java unit test report format (JUnit); line feed (LF); Python Package Index (PyPI); 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 next source version may be prepared 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.

  1. Merge the versioned release change through the public pull-request workflow. Require the protected repository-check aggregate to pass on the exact candidate commit. Record the numeric workflow run identifier for the successful repository-checks push run on the protected default branch; a pull-request run or synthetic merge is not a release evidence coordinate.
  2. From a clean checkout of that commit, run:
   python -m pip install ".[test]"
   python -u -m pytest -vv -s --durations=10 --timeout=60
   python -u -m compileall src
  1. Build a pre-tag candidate from the full commit SHA, not a working directory:
   VERSION="$(PYTHONPATH=src python -c 'import verifier; print(verifier.__version__)')"
   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, the exact $VERSION, 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 Linux, Windows, and macOS and compares every byte. Do not prepare a tag unless that cross-platform comparison passed on the exact candidate commit.

  1. Run twine check on 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.
  2. 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.
  3. Confirm python scripts/check_time_status.py passes, 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="$(PYTHONPATH=src python -c 'import verifier; print(verifier.__version__)')"
   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, the maintainer MAY instead create an unsigned annotated tag locally with git tag -a "v$VERSION" FULL_PUBLIC_COMMIT_SHA. The workflow requires that existing tag and never creates one. Publication MUST still run through .github/workflows/release.yml, which records the GitHub tag-object verification result and reason in the release notes and creates 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.

  1. Run the verifier independently before upload:
   VERSION="$(PYTHONPATH=src python -c 'import verifier; print(verifier.__version__)')"
   python scripts/release_artifacts.py verify \
     "dist/tagged/verifier-standard-$VERSION.manifest.json"
   python scripts/release_artifacts.py compare-retagged dist/candidate dist/tagged

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.

  1. Push the tag only after all preceding checks pass. Re-run the owner-authenticated immutable-release API preflight, then dispatch .github/workflows/release.yml from the protected default branch with the exact existing tag, repository_checks_run_id=<RECORDED_NUMERIC_RUN_ID>, and immutable_releases_preflight=true. The owner-dispatched release workflow rechecks signed-tag identity, protected-main ancestry, package version, the selected run's workflow identity, successful push event, exact commit and branch, successful conformance-gate, 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 also downloads the four run- and attempt-qualified component reports and their raw environment/JUnit artifacts. A fail-closed preparation step reconstructs every report, rejects coordinate substitution, malformed or contradictory JUnit summaries, unknown or duplicate environment fields, changed raw environment bytes, and changed report bytes. It builds a deterministic ZIP whose internal manifest binds the release, repository, commit, workflow run, member digests, and byte lengths. It then boundary-scans and attests that observational evidence bundle alongside the tested source ZIP, wheel, source distribution, SBOM, and external release manifest. The scanner checks raw and decoded XML report text against the prohibited patterns, rejects malformed XML and document declarations, and never echoes a matched secret. Rejection leaves the digest-bound evidence bytes unchanged. Repository checks apply the same boundary gate before uploading raw environment and test artifacts, including when the test command fails. The platform bundle has verification_effect = NONE; its existence does not turn the mapped tests into universal platform support. 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 protected pypi environment, and publishes them through the configured PyPI Trusted Publisher. Existing tags and release assets remain untouched; corrections are additive.
  2. 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.

  1. Let Zenodo archive the GitHub release, then record the issued DOI additively.
  2. Confirm that the PyPI page at https://pypi.org/project/verifier-standard/VERSION/ for the exact release version 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 name verifier.