Bitspark constellation
accepted source ↗

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.0 tag-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:

  1. 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.
  2. 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.
  3. The BOM is ground truth. substrate/bom.json records each component's source.commit and its captured per-lane coordinates; the live tag is checked against the BOM, not against main'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.0 incident. A recon pass (atlas#465) saw the v0.4.0 tag pointing to a commit not reachable from main and 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: 70f19687 is the genuine release commit, recorded in the immutable 2026.06.06 BOM with real wasm digests. The BOM was right; the recon's assumption was wrong. The tag was restored to 70f19687. The lesson: for anything touching release tags, the BOM is the source of truth, not main'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-time substrate capture step 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 from main is not a release property.

How it is enforced (belt, suspenders, written model)

  1. Belt — the action is made impossible. An org tag-protection ruleset (release-tags-immutable, target tag, patterns refs/tags/v* + refs/tags/**/v*, rules: block deletion + 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.
  2. Suspenders — drift is caught from any cause. atlas substrate verify-publisher --bom-tags asserts, for every channel-current release, that each component's recorded source.tag still resolves live to the BOM-recorded source.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).
  3. Captured coordinates. SUBSTRATE_CAPTURE_ARM is armed fleet-wide, so the publish-time substrate capture (#321/#260) overwrites the template placeholders with real published coordinates, fail-loud — the BOM never records a placeholder for an armed lane.
  4. The written model. This ADR and RELEASING.md state 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-tags leg checks source.commit (the real release commit), not the per-lane resolvedCommit, 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.

The Bitspark constellation — how the systems are built and relate.

GitHub