ADR-0009 Stage Release Publication
Context and scope¶
Needlr publishes from a version tag. The tag workflow first confirms that the exact
tagged commit already completed a successful main CI run, which restores, builds,
runs the test suite, validates packages, and compiles both Native AOT samples. That
verification proves the commit is releasable.
Until this decision, the tag workflow then discarded the value of that proof. A single
publication job repeated restore, build, the full test suite, coverage generation,
packaging, and documentation generation, and only afterwards performed the irreversible
operations: pushing to NuGet.org, pushing to GitHub Packages, deploying documentation,
and creating the GitHub Release. The job declared no timeout. Release v0.0.3-alpha.3
demonstrated the cost: main CI run 30402851026 succeeded for the exact commit, then
release run 30406601340 began the entire validation sequence again, and a stalled
publication worker meant the only recovery was cancelling and repeating all of it.
This decision governs the staging, artifact contract, and retry model of
.github/workflows/release.yml. It does not change what a release contains, which
packages ship, how versions are validated, or the local gates in scripts/release.ps1.
Decision drivers¶
- A transient failure in an irreversible publication step must not force recompilation or re-execution of tests that already passed for the same commit.
- Every reversible operation must complete before the first irreversible operation.
- Each publication destination must be independently retryable and idempotent.
- Anything published must be provably the artifact produced from the validated commit.
- Storage cost matters. Retaining artifacts between workflow runs must never become a requirement of the release contract.
- The published package set, release notes, documentation output, provenance, and permissions must not regress.
Decision¶
release.yml is staged into one verification job, one reversible preparation job, and
four publication jobs.
verify-main-ci waits for the successful same-commit main CI run. prepare validates
the tag against that verified run, builds, packs, produces a digest manifest, extracts
release notes, builds the documentation site, and uploads two artifacts scoped to the
release run. publish-nuget, publish-github-packages, deploy-documentation, and
create-release each download the prepared artifacts, re-verify them, and perform
exactly one irreversible operation. No publication job restores, builds, or tests.
The release path no longer repeats the test suite or coverage generation. verify-main-ci
is the gate that proves the commit passed those checks, and duplicating them only widened
the window in which a runner failure destroyed validated work.
A release candidate is defined by release-manifest.json, written by
scripts/write-release-manifest.ps1. The manifest records the schema version, release
version, package version, source commit SHA, producing run identifier, producing workflow,
the identifier of the validated CI run, and a SHA-256 digest and size for every packaged
file. scripts/verify-release-manifest.ps1 recomputes the digests and rejects a candidate
that is incomplete, tampered with, carries unlisted packages, or does not match the
version, commit, producing run, and validated CI run the publishing job expects.
scripts/pack-release-packages.ps1 owns the published project selection so the package set
cannot drift between producers, and scripts/test-release-artifacts.ps1 exercises the whole
contract during CI preflight.
Artifacts remain scoped to the release run. Retrying a destination replays only that destination against artifacts the same run already produced.
Alternatives considered¶
Keep one monolithic publication job¶
The status quo needs no artifact contract and keeps the whole release readable in one place. It was rejected because it makes every irreversible step depend on a fresh, full, redundant validation sequence, so any late failure costs the entire release again. It also leaves no way to retry a single destination.
Promote a release candidate produced by main CI¶
Main CI could pack a candidate, upload it, and let the tag workflow download and publish
that exact bundle, giving the strongest provenance and the fastest possible tag workflow.
It was rejected as the default because it makes cross-run artifact retention a hard
requirement of the release contract and imposes storage cost on every main build, which is
an unacceptable steady-state expense for this repository. The saving is also smaller than
it appears: scripts/generate-api-docs.sh requires the Release build's XML documentation
output, so the release run must build regardless, and promoting packages alone would only
avoid dotnet pack.
The manifest format is deliberately producer-agnostic. It records the producing workflow and the validated CI run separately, so an opt-in accelerator that promotes a candidate from another run can be added later without changing the verification contract or making retention mandatory.
Split preparation and publication only for retries, keeping tests in the release path¶
Retaining the release-path test run would preserve the previous belt-and-braces validation
while still gaining restartability. It was rejected because the tests add no information
that verify-main-ci has not already established for the identical commit, while
lengthening the pre-publication window that the change is meant to shorten.
Consequences¶
Positive¶
- A failed destination is retried on its own; publication never rebuilds or retests.
- All validation completes before the first irreversible operation.
- Published artifacts are digest-bound to the validated commit and run.
- Every job has an explicit timeout and least-privilege permissions.
- Release duration drops by the length of the duplicated test and coverage run.
Negative¶
- The release workflow is longer and has more moving parts than one job.
- Prepared artifacts occupy run-scoped storage for their retention window, and a retry attempted after expiry requires a full workflow re-run.
- The
releaseenvironment now gates four jobs instead of one, so required reviewers, if ever configured, would prompt per destination. - Release-path confidence now depends on
verify-main-cibeing correct about the commit.
Neutral¶
- Documentation deployment still follows package publication, as in the previous sequential job, so documentation never announces a release whose packages failed.
- Coverage output is no longer produced by the release path. The documentation ownership
contract already assigns
/coverage/*toci.yml, andkeep_filespreserves the copy published by the same commit's CI run.
Confirmation¶
scripts/test-release-artifacts.ps1 runs in CI preflight and asserts the published project
selection, the pack invocation contract, manifest creation, and every rejection case:
digest tampering, missing packages, unlisted packages, mismatched version, mismatched
commit, mismatched producing run, mismatched validated CI run, unsupported schema, and
expectations supplied without a value.
The workflow structure is confirmed by inspection and by actionlint: no publication job
contains a restore, build, or test step, and every publication job verifies the manifest
before its irreversible step.
Retry behavior against live registries cannot be confirmed from the repository. It is observed on the first release that exercises a failed destination.
References¶
- Issue "Release workflow rebuilds an already validated commit instead of promoting its artifacts" reports the duplicated validation and the resulting failed release attempt.
docs/releasing.mddocuments the staged jobs, the candidate contract, and the retry procedure for maintainers..github/instructions/docs.instructions.mddefines thegh-pagespath ownership split that keepsci.ymlresponsible for/api/dev/*and/coverage/*whilerelease.ymlowns/api/stable/*and/api/v<version>/*.