ADR 0020 — release tags are off-main, immutable, and BOM-anchored
- Status: accepted
- Date: 2026-06-14
- Relates: 0008 (the substrate release model this makes explicit), 0004 (the BOM), 0006 (channels), 0019 (the registry/projection model)
- Motivating incident: the logos
v0.4.0tag-drift (2026-06-13) — see below
Three things about how a release relates to its source were true but never written down, so a reconnaissance pass filled the gap with a wrong assumption and moved a shipped release tag. They are now stated, and enforced:
- A release tag is cut OFF-MAIN. The release commit need not be reachable from
main. A tag pointing off-main is normal, not an orphan.- A release tag is the only ref, and it is IMMUTABLE. Once a release is cut, its tag must never move or be deleted. A re-cut is a new tag/release, never a moved one.
- The BOM is ground truth.
substrate/bom.jsonrecords each component'ssource.commitand its captured per-lane coordinates; the live tag is checked against the BOM, not againstmain's reachability.
Why this ADR exists
ADR 0008 is the complete substrate release model — component/constellation releases,
signed channel pointers, atomic advertisement, the verdict. What it left implicit is
the relationship between a release and its git history: that the tag is cut off-main, is
immutable, and that the immutable BOM — not main — is the authority on what a tag should
point at. Because that was never written, two failures became possible:
The logos
v0.4.0incident. A recon pass (atlas#465) saw thev0.4.0tag pointing to a commit not reachable frommainand concluded it was an orphan, then "fixed" it by moving the tag onto a mainline squash (31e5501, the ontos-v0.2.0-migration PR). It was not an orphan:70f19687is the genuine release commit, recorded in the immutable2026.06.06BOM with real wasm digests. The BOM was right; the recon's assumption was wrong. The tag was restored to70f19687. The lesson: for anything touching release tags, the BOM is the source of truth, notmain's reachability.Placeholder coordinates (#321 capture). A release-facts template ships placeholder volatile coordinates (
go.sum h1:AAAA…,rs/ts commit 0000…0001) that the publish-timesubstrate capturestep overwrites with real ones. With capture unarmed, a release could reach the BOM carrying placeholders — half the "BOM is ground truth" guarantee silently unmet.
What the model forbids
- Never move a shipped release tag. If a release was cut wrong, cut a new release (a new tag, a new BOM row — the ledger is append-only; bad rows are yanked, never deleted; ADR 0004/0008). Moving a tag breaks every consumer that pinned it and every BOM row that recorded it.
- Never judge a release tag by
main. Off-main is the normal shape (releases are cut on a release ref, tagged, BOM-recorded). Reachability frommainis not a release property.
How it is enforced (belt, suspenders, written model)
- Belt — the action is made impossible. An org tag-protection ruleset
(
release-tags-immutable, targettag, patternsrefs/tags/v*+refs/tags/**/v*, rules: blockdeletion+non_fast_forward, no bypass) rejects a force-push or delete of any release tag outright — including by an agent or admin. This is the primary control: it removes the ability to do the wrong thing. - Suspenders — drift is caught from any cause.
atlas substrate verify-publisher --bom-tagsasserts, for every channel-current release, that each component's recordedsource.tagstill resolves live to the BOM-recordedsource.commit. It catches a moved tag, a deleted/orphan tag, and a botched re-cut, all against the immutable BOM — the correct replacement for the removed "tag must be on main" guard (atlas#543/#568). - Captured coordinates.
SUBSTRATE_CAPTURE_ARMis armed fleet-wide, so the publish-timesubstrate capture(#321/#260) overwrites the template placeholders with real published coordinates, fail-loud — the BOM never records a placeholder for an armed lane. - The written model. This ADR and
RELEASING.mdstate the model so a future recon pass (or contributor) checks against it rather than a generic assumption.
Consequences
- A draft/private member registered before it self-declares (ADR 0019) does not yet cut releases; this ADR governs members that ship substrate releases.
- The
verify-publisher --bom-tagsleg checkssource.commit(the real release commit), not the per-laneresolvedCommit, so it stays green while a lane's coordinates are pending capture and reds only on a genuine tag drift. - An accidental tag now requires deliberately editing the ruleset to remove — friction by design: the wrong action is no longer one careless push away.