feat(release): cut release candidates before the final 1.0.0 (ADR-1201) - #1353
Merged
Conversation
lusoris
force-pushed
the
feat/release-candidates
branch
4 times, most recently
from
September 7, 2026 10:16
6b05db9 to
d4ebadb
Compare
The fork has never released, and the plan is to go straight from that to a
1.0.0 final. For a fork shipping four GPU backends, an FFmpeg patch stack, an
MCP server and a tiny-AI surface, the first cut is the risky one and the
pipeline has never cut anything. Maintainer direction: cut 4-8 release
candidates first.
The pipeline could not. SIX places refused or mishandled a prerelease, none of
them a policy statement -- each a guard written when a prerelease could only be
a mistake:
1. verify-release-version.sh accepted only ^v<major>.<minor>.<patch>$
2. ...and its marker extractor matched [0-9]+\.[0-9]+\.[0-9]+, reading
1.0.0-rc.1 as "1.0.0" and then reporting the marker as disagreeing with
the tag it actually matched
3. supply-chain.yml exited 1 on prerelease == true
4. ...and separately required the release to have prerelease == false
5. docker-publish-production.yml had its own copy of the rejection
6. docker-publish-operator-node.yml had a third copy
Plus a latent trap worth spelling out. The ADR-1151 contract gate allows the
one-shot release-as / bootstrap-sha fields "until the manifest reaches 1.0.0"
and decides that with sort -V. But
printf '1.0.0\n1.0.0-rc.1\n' | sort -V | head -1 -> 1.0.0
so the gate would have concluded the RC line had already reached 1.0.0 and
failed EVERY RC build while release-as was legitimately still present. The
prerelease suffix is now tested before that comparison.
Each blanket rejection becomes a CONSISTENCY check: an -rc.N tag must be
published as a prerelease and a final tag must not. That is the property worth
enforcing -- the dangerous states are the mismatched ones. An RC published as
stable becomes `latest` for every consumer; a final published as a prerelease
silently never does.
A release candidate can never take the `latest` image tag. GitHub's
/releases/latest already excludes prereleases so the existing resolution could
not have picked one, but the failure is severe and silent for anyone pulling
`latest`, so the suffix is refused explicitly rather than trusting one
endpoint's semantics to stay that way.
The accepted shape is deliberately narrow: rc only, dotted integer, no leading
zero. v1.0.0-rc.1 and v1.0.0-rc.12 pass; -beta, bare -rc, -rc.01, -rc.1.2 and
-RC.1 are refused. SemVer permits all of those; this fork ships exactly one
prerelease channel and every extra accepted shape is another way to mis-tag.
Cutting the final release is then two edits: prerelease: false and
release-as: 1.0.0. The contract gate starts enforcing one-shot-field removal
the moment the manifest loses its suffix.
scripts/release/tests/test-verify-release-version.sh: 25 passed, 0 failed
(was 18) -- five new cases pin the accepted and rejected tag shapes, and they
caught the marker-extractor bug above.
no digest needed: trivial
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lusoris
force-pushed
the
feat/release-candidates
branch
from
September 7, 2026 12:16
d4ebadb to
8c7e22c
Compare
lusoris
marked this pull request as ready for review
September 7, 2026 12:47
lusoris
enabled auto-merge (squash)
September 7, 2026 12:47
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The plan was to go from "never released" straight to
1.0.0final. For a fork shipping four GPU backends, an FFmpeg patch stack, an MCP server and a tiny-AI surface, the first cut is the risky one — and the pipeline has never cut anything. Per maintainer direction, 4–8 release candidates come first.The pipeline could not do that. Six places refused or mishandled a prerelease. None was a policy statement; each was a guard written when a prerelease could only be a mistake:
verify-release-version.sh^v<major>.<minor>.<patch>$[0-9]+\.[0-9]+\.[0-9]+, reading1.0.0-rc.1as1.0.0and then reporting the marker as disagreeing with the tag it actually matchedsupply-chain.ymlexit 1onprerelease == truesupply-chain.ymlprerelease == falsedocker-publish-production.ymldocker-publish-operator-node.ymlThe latent trap worth flagging
The ADR-1151 contract gate allows the one-shot
release-as/bootstrap-shafields "until the manifest reaches 1.0.0", decided withsort -V. But:So the gate would have concluded the RC line had already reached 1.0.0 and failed every RC build while
release-aswas legitimately still present. The prerelease suffix is now tested before that comparison, and the rebase note marks that ordering as load-bearing.What replaces the rejections
Each blanket rejection becomes a consistency check: an
-rc.Ntag must be published as a prerelease, and a final tag must not. That is the property worth enforcing — the dangerous states are the mismatched ones. An RC published as stable becomeslatestfor every consumer; a final published as a prerelease silently never does.A release candidate can never take the
latestimage tag. GitHub's/releases/latestalready excludes prereleases, so the existing resolution could not have picked one — but the failure is severe and silent for anyone pullinglatest, so the suffix is refused explicitly rather than trusting one endpoint's semantics to stay that way.The accepted shape is deliberately narrow:
rconly, dotted integer, no leading zero.v1.0.0-rc.1,v1.0.0-rc.12v1.0.0-beta,v1.0.0-rc,v1.0.0-rc.01,v1.0.0-rc.1.2,v1.0.0-RC.1SemVer permits all of the refused ones. This fork ships exactly one prerelease channel, and every extra accepted shape is another way to mis-tag a release.
Cutting the final release is then two edits —
prerelease: false,release-as: 1.0.0— and the contract gate starts enforcing one-shot-field removal the moment the manifest loses its suffix.Type
feat— new featurebuild/ci— tooling / infraChecklist
make format && make lintgreen locally —pre-commitclean on all 14 files;actionlintclean on all four workflows;check-aggregator-names.shOK (35 required checks).scripts/release/tests/test-verify-release-version.sh: 25 passed, 0 failed (was 18).vX.Y.Ztag behaves exactly as before; the new shape is additive.docs/adr/_index_fragments/with the slug in_order.txt;README.mdregenerated and--checked.Bug-status hygiene (ADR-0165)
docs/state.md— no state delta: release policy and pipeline capability, not a tracked runtime defect.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score.Deep-dive deliverables (ADR-0108)
## Alternatives consideredweighs five options, including0.9.xbetas and draft-only RCs (which rehearse nothing, since a draft never firesrelease: published).AGENTS.mdinvariant note — indocs/rebase-notes.md: the narrow pattern, consistency-not-absence in the publishing workflows, and thesort -Vordering trap.changelog.d/added/release-candidates.md.feat/release-candidates — rc.N prereleases before 1.0.0 (2026-09-06).Reproducer
Known follow-ups
prerelease: false,release-as: 1.0.0). Someone can forget them; the contract gate catches the second the moment the manifest loses its suffix, anddocs/development/release.mdshould gain a checklist line when the first RC is actually cut.pipwill not install them without--pre.