Release evidence and package promotion¶
The release pipeline is package-first and fail-closed. A release candidate and every final or post release is built reproducibly from a protected tag, recorded in the Gitea Package Registry, validated through TestPyPI, and only then eligible for public PyPI and Docker Hub promotion.
Required sequence¶
- Record requirements, design impact, verification scope, coverage, known defects, operational impact, and approvals in the private lifecycle record.
- Create a PEP 440 release-candidate version such as
X.Y.Zrc1and push the matching protectedvX.Y.Zrc1tag. The Gitea package workflow builds the wheel and sdist twice under oneSOURCE_DATE_EPOCH, requires identical SHA256 values, runs the full test suite, writes the canonical cross-system distribution manifest, a separate source/run/workflow-bound Gitea provenance document, andSHA256SUMS, then uploads one deterministic tar. Theprepare-packagejob runs onci-untrusted-python312with read-only contents and explicitpackages: noneand receives no package credential. Candidate provenance records the canonical public server identityhttps://git.nmulti.cloud; it does not consume the runner-facinggithub.server_url, which can legitimately contain the server's internal configured origin. - The dependent
verify-and-sealjob anonymously fetches canonicalmainand the exact annotated tag fromhttps://git.nmulti.cloud, proves that the tag peels to the immutable event SHA and remains in canonicalmain, validates the workflow digest and every candidate attestation field, and rebuilds the source in two independent Git worktrees. It requires byte equality between both rebuilds and the candidate, writes a canonical seal manifest that binds every sealed file to the source SHA, run ID, run attempt, tag, and version, and exports the manifest SHA256 as a job output before uploading the bounded seal. This credential-free job runs only on the dedicatedrelease-builderlane and never on aci-untrusted-*runner. - The
publish-candidatejob anonymously fetches the exact tag from the canonicalhttps://git.nmulti.cloud/emersonfelipesp/proxmox-sdk.gitorigin into an isolated source tree, checks that the tag peels to${{ github.sha }}and that the checked-out SHA matches the verifier output, downloads only the sealed set, recomputes and compares the verifier-exported seal manifest SHA256, and restores private read-only modes. It then uses the locked release tools and the same reproducible-build procedure asprepare-packageto build the exact tag source twice on therelease-publisherrunner. Both rebuilt distributions must match the sealed wheel and sdist byte for byte before the package token is exposed. This job must run only on the dedicatedrelease-publisherlane and never on aci-untrusted-*runner. Only its five-minutePublish sealed package and verify served bytesstep receives the repository secretPACKAGE_WRITE_TOKEN. The helper validates the token as bounded printable ASCII before constructing a registry client, uses the pinnedhttps://git.nmulti.cloudorigin, and uploads each missing artifact through one no-redirect multipart request with bounded connect/read timeouts, response size, and a shared global deadline. An exact remote subset is resumable only when every present artifact's metadata, served bytes, and repository association match the seal; every upload is followed by a fresh inspection. Redirected, oversized, timed-out, extra, mismatched, or wrongly associated states fail closed. An ambiguous upload or link response is accepted only when an independent bounded GET proves exact progress. A final credential-free step retainspublication.jsonunder a source/run/attempt- qualified artifact name. That evidence records the verified seal digest, canonical server, owner, and repository, observed repository association, result, and the final filename, size, and SHA256 inventory. - Verify the package record and both served file identities with:
nms git packages latest --type pypi --name proxmox-sdk --owner emersonfelipesp
nms git packages detail --type pypi --name proxmox-sdk --version <version> --owner emersonfelipesp
nms git packages files --type pypi --name proxmox-sdk --version <version> --owner emersonfelipesp
Archive these responses with the candidate provenance and distribution
manifest. Do not promote a tag whose package record, repository association,
filenames, sizes, or SHA256 values differ from the sealed evidence.
6. Promote the RC tag to GitHub. The v*rc* trigger publishes the same pair to
TestPyPI and validates the bytes served by TestPyPI across Python 3.11, 3.12,
and 3.13 and all supported schema fixtures. Iterate with rcN until clean.
7. Create the final protected tag on Gitea and require its final package record
to pass the same byte-level verification.
8. Copy .github/RELEASE_EVIDENCE_TEMPLATE.md into the public GitHub Release
body, set the exact version, copy distribution_manifest_sha256 from the
Gitea provenance in the verified seal, complete every evidence item, and
remove all private tracker references. The public workflow rebuilds the
manifest and rejects a digest mismatch, missing/unchecked evidence, a
version mismatch, or internal evidence.
9. Publish the non-prerelease GitHub Release. PyPI publication runs in a
protected, artifact-only job. A later job downloads the project wheel back
from PyPI, hashes the served bytes, and makes that exact wheel the only
project payload accepted by service-image builds.
10. Core and service images build without registry credentials, run on amd64 and
arm64 under a digest-pinned QEMU helper where needed, and emit CycloneDX
inventories. BuildKit is likewise selected by a reviewed multi-architecture
digest. Protected jobs load the tested Docker archives, verify the requested
platform plus source/version/wheel labels, bind each archive and SBOM hash to
its registry manifest digest, and stage candidate manifests. Every candidate
is pulled by digest and smoked on both platforms before one final fan-in job
verifies all per-architecture evidence and writes version, latest, and
commit-traceability aliases.
11. Retain the distribution, served-PyPI, candidate-manifest, SBOM, and final
promotion evidence artifacts. The post-release workflow rechecks alias
digests and runs all seven image identities on both architectures.
RC tags never enter the public PyPI or stable Docker paths. A manual public
workflow dispatch is TestPyPI-only and must execute from protected main.
Manual Docker dispatches build and test only.
Required external controls¶
Repository YAML cannot create environment reviewers, deployment branch rules, protected-tag rules, runner isolation, or environment-scoped secrets. Operators must configure all of the following before enabling publication:
| Environment | Credential | Required deployment policy |
|---|---|---|
testpypi |
TEST_PYPI_TOKEN |
Required reviewer; protected main and protected v*rc*/prerelease tags only |
pypi |
PYPI_TOKEN |
Required reviewer; protected final/post release tags only |
dockerhub-candidate |
DOCKERHUB_USERNAME, DOCKERHUB_TOKEN |
Protected main/testing and protected release tags only |
dockerhub-development |
Docker Hub credentials | Protected main/testing only |
dockerhub-release |
Docker Hub credentials | Required reviewer; protected final/post release tags only |
| Gitea package registry | Repository secret PACKAGE_WRITE_TOKEN |
Repository-scoped write:package; exact protected annotated v* tag workflow only |
Remove the corresponding GitHub repository-scoped publisher secrets after the
GitHub environment secrets are configured. Provision the dedicated runner
ci-deploy-emersonfelipesp-246 with the labels mirror-host,
release-builder, and release-publisher, and confirm that both release lanes
select only that runner and that the runner executes no pull-request workloads.
Do not configure PACKAGE_WRITE_TOKEN until the release-builder and
release-publisher assignments and the absence of pull-request workloads have
been confirmed. Then provision it as the only Gitea package-publisher secret. It
must be a dedicated,
repository-scoped write:package credential, must not grant repository-content
write or administrative access, and must not be copied into a runner profile,
command argument, log, artifact, or committed configuration. The built-in job
token remains package-read-only. The prepare-package job remains on disposable
ci-untrusted-python312 workers; verify-and-seal is isolated on
release-builder, and publish-candidate is isolated on release-publisher.
If the secret is absent, the credentialed step exits non-zero with an explicit
message.
The systemd verifier and publisher units remain available as a compatibility
path for already provisioned hosts, but they are no longer required for the
normal release sequence. Their credential-file and root-seal controls remain
unchanged. Protect tag creation, require annotated tags and the exact v*
protection, restrict repository-secret administration, and require the public
release reviewer to confirm the Gitea package record and retained evidence.
Reproducibility and digest terminology¶
The Python wheel and sdist are required to reproduce byte for byte under the tag commit timestamp. Container inputs are constrained by the reviewed Python/Alpine multi-architecture index digest, exact direct APK versions, the locked Python graph, checksummed mkcert downloads, source revision labels, and per-image CycloneDX inventories.
Container binary reproducibility is not claimed: Alpine repositories and
transitive build behavior can change independently of this repository. The OCI
manifest digest is the immutable image identity. Tags named sha-<commit> are
commit traceability tags, not immutable objects; all promotion and verification
therefore reads and compares manifest digests.
Within a workflow run, distribution, Docker archive, SBOM, and provenance artifact names include the validated source SHA, GitHub run ID, and run attempt. Downloads are restricted to those exact names, so a rerun cannot consume an earlier attempt's artifact. Candidate registry tags carry the same run token to prevent a same-commit CI/release race. Candidate evidence records the local image ID, requested platform, OCI revision/version/wheel labels, archive SHA256, SBOM SHA256, and the matching digest returned by both the push and registry inspection. The final promotion evidence hashes the candidate-evidence documents themselves so a later artifact substitution cannot silently change the approved identity chain.
Docker Hub does not provide a transaction that atomically updates all aliases. The workflow gates all candidates before the first stable write and promotes only from validated digests, but an external registry failure can still stop a multi-image alias update partway through. A rerun revalidates the candidates and idempotently re-points every alias to the recorded digests; never rebuild or delete a released artifact to repair a partial alias update.
Stable Docker promotion is serialized across releases and rechecks that the
triggering GitHub Release is still the repository's current latest release
immediately before alias writes. An older rerun may verify its immutable version
artifacts, but it cannot move latest* aliases backward.
Lifecycle evidence retained¶
The evidence set covers NASA NPR 7150.2D Chapter 4 lifecycle expectations:
- requirements and acceptance-criteria traceability;
- architecture/design impact and reused-component decisions;
- coding standards, static analysis, tool and dependency versions;
- unit, integration, package, schema-matrix, container, and post-release tests;
- measured coverage or an approved rationale for gaps;
- security checks, known-defect disposition, and reviewer approval;
- delivery manifests containing source, distribution, served-wheel, and OCI digest identities;
- operator documentation, rollback, maintenance, archive, and retirement assessment.
The workflows retain release evidence artifacts for 90 days. The private issue and pull-request record remains the long-term lifecycle archive. Public Release notes contain only the product-facing checklist and must not include private URLs, issue/PR numbers, branch names, or internal commit references.