docs(adr): ADR-0004 push-trigger + manifest-state + rolling-PR release flow (refs #52) #57

Merged
alex merged 2 commits from i/52-adr-0004 into main 2026-06-25 02:24:39 +02:00

Decision record for the v0.4.0 release flow

Records what the design sketch (PR #55, merged 01946c7) converged on, as the formal ADR. Filed as a focused PR before slice 1 so the narrows-not-supersedes-ADR-0003 framing — flagged by Surveyor d296 as the load-bearing thing to get right — gets independent review, not bundled with implementation.

The narrows-not-supersedes relationship (load-bearing)

ADR-0003 §Decision establishes four operator-review gates as the load-bearing structure. Its §Reasoning argues for gate count + gate function, not for workflow_dispatch-as-Gate-1 specifically.

ADR-0004 changes the mechanism of Gate 1 (explicit dispatch → rolling-PR merge) while preserving the substance (operator explicitly decides when to release). Gates 2-4 stay verbatim per ADR-0003 — its §Reasoning on each survives unchanged.

A supersession would invalidate ADR-0003. This is not that. Both ADRs stay in force. Future readers reference both together when reasoning about the release flow's shape.

The decision in one sentence

v0.4.0 uses push:main + workflow_dispatch fast-path + .release-toolkit-manifest.json committed state + shell git log <last_sha>..HEAD walk via release-decide.sh — with zero ${{ github.event.* }} / ${{ forgejo.event.* }} accesses on the load-bearing path — and operator's merge of the rolling release PR replaces Gate 1's workflow_dispatch click.

Trade-offs surfaced

The ADR enumerates four:

  1. Every push triggers a workflow run (mitigated by noop-fast-exit)
  2. Manifest desync = new load-bearing silent-failure surface (#56 tracks the guard)
  3. Gate 1 mechanism is opinionated about merge-flow style (matches ADR-0003 audience intent)
  4. v0.3.x → v0.4.0 migration must be all-or-nothing per consumer (coexistence period for legacy refs)

What this PR does NOT do

  • Not implementation. No _release.yml, no release-decide.sh, no manifest schema enforcement.
  • Not consumer-side adoption. The toolkit's own consumer-side workflows still consume v0.3.x refs.
  • Not a supersession of ADR-0003. ADR-0003 stays in force; this ADR narrows Gate 1's mechanism only.

Implementation follows

Slice 1 (_release.yml + release-decide.sh + manifest schema enforcement + bats unit tests for the decision tree) opens as its own PR after this ADR merges. Surveyor d296 has two substrate-checks queued for slice 1: manifest-desync guard (#56) traced through the manifest-read path + the semver -rc.1 parse-verify (substrate-confirmed in the design sketch §Bootstrap mechanic; contract holds, no semver.sh changes needed).

Refs

## Decision record for the v0.4.0 release flow Records what the design sketch (PR #55, merged 01946c7) converged on, as the formal ADR. Filed as a focused PR before slice 1 so the **narrows-not-supersedes-ADR-0003** framing — flagged by Surveyor d296 as the load-bearing thing to get right — gets independent review, not bundled with implementation. ## The narrows-not-supersedes relationship (load-bearing) ADR-0003 §Decision establishes **four operator-review gates** as the load-bearing structure. Its §Reasoning argues for gate count + gate function, not for `workflow_dispatch`-as-Gate-1 specifically. ADR-0004 changes the **mechanism** of Gate 1 (explicit dispatch → rolling-PR merge) while preserving the **substance** (operator explicitly decides when to release). Gates 2-4 stay verbatim per ADR-0003 — its §Reasoning on each survives unchanged. A *supersession* would invalidate ADR-0003. This is not that. Both ADRs stay in force. Future readers reference both together when reasoning about the release flow's shape. ## The decision in one sentence v0.4.0 uses `push:main` + `workflow_dispatch` fast-path + `.release-toolkit-manifest.json` committed state + shell `git log <last_sha>..HEAD` walk via `release-decide.sh` — with **zero** `${{ github.event.* }}` / `${{ forgejo.event.* }}` accesses on the load-bearing path — and operator's merge of the rolling release PR replaces Gate 1's `workflow_dispatch` click. ## Trade-offs surfaced The ADR enumerates four: 1. Every push triggers a workflow run (mitigated by noop-fast-exit) 2. Manifest desync = new load-bearing silent-failure surface ([#56](https://git.frankenbit.de/frankenbit/release-toolkit/issues/56) tracks the guard) 3. Gate 1 mechanism is opinionated about merge-flow style (matches ADR-0003 audience intent) 4. v0.3.x → v0.4.0 migration must be all-or-nothing per consumer (coexistence period for legacy refs) ## What this PR does NOT do - Not implementation. No `_release.yml`, no `release-decide.sh`, no manifest schema enforcement. - Not consumer-side adoption. The toolkit's own consumer-side workflows still consume v0.3.x refs. - Not a supersession of ADR-0003. ADR-0003 stays in force; this ADR narrows Gate 1's mechanism only. ## Implementation follows Slice 1 (`_release.yml` + `release-decide.sh` + manifest schema enforcement + bats unit tests for the decision tree) opens as its own PR after this ADR merges. Surveyor d296 has two substrate-checks queued for slice 1: manifest-desync guard ([#56](https://git.frankenbit.de/frankenbit/release-toolkit/issues/56)) traced through the manifest-read path + the semver `-rc.1` parse-verify (substrate-confirmed in the design sketch §Bootstrap mechanic; contract holds, no `semver.sh` changes needed). ## Refs - Predecessor: [ADR-0003 multi-stage operator-gate](https://git.frankenbit.de/frankenbit/release-toolkit/src/branch/main/docs/adr/0003-multi-stage-operator-gate.md) — **narrows, does NOT supersede** - Design sketch: [`docs/design/v0.4.0-release-please-shape.md`](https://git.frankenbit.de/frankenbit/release-toolkit/src/branch/main/docs/design/v0.4.0-release-please-shape.md) (PR #55, merged 01946c7) - Tracking arc: [#52](https://git.frankenbit.de/frankenbit/release-toolkit/issues/52) - Substrate evidence: [#41](https://git.frankenbit.de/frankenbit/release-toolkit/issues/41) + [#47](https://git.frankenbit.de/frankenbit/release-toolkit/issues/47) - Desync guard follow-up: [#56](https://git.frankenbit.de/frankenbit/release-toolkit/issues/56)
docs(adr): 0004 push-trigger + manifest-state + rolling-PR release flow
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
7643fd01e9
Records the v0.4.0 release-flow decision. Narrows ADR-0003 (gate-1
mechanism shift from explicit dispatch to rolling-PR merge) while
preserving the four-gate ceremony verbatim — does NOT supersede.

Decision: push:main + workflow_dispatch fast-path + manifest JSON
state + shell git-log walk via release-decide.sh. Zero github.event.*
or forgejo.event.* expression-engine accesses on the load-bearing
path. Gate 1 = operator merges the rolling PR; Gates 2/3/4 unchanged.

Reasoning sections cover:
- Narrows-not-supersedes — ADR-0003 reasoning survives verbatim;
  this ADR changes mechanism, not gate substance
- Why substrate-honest escape required mechanism-level change (3
  escape paths considered, only (3) eliminates dependency on the
  buggy expression-engine surface)
- Why rolling-PR matches operator intent (Surveyor d296 textual
  read + operator confirmation 2026-06-25)
- Why push:main is right (reliable + idempotent + composable)
- Why manifest is machine-managed JSON (not extending YAML config)

Trade-offs accepted:
- Every push triggers (noop-fast-exit mitigates)
- Manifest desync = new load-bearing silent-failure surface (#56 guards)
- Gate 1 opinionated about merge-flow style
- v0.3.x → v0.4.0 migration must be all-or-nothing per consumer

Refs #52, design sketch #55 (merged 01946c7), #56 desync guard tracker,
#41 + #47 substrate evidence.
surveyor approved these changes 2026-06-25 02:14:59 +02:00
Dismissed
surveyor left a comment

ADR-0004 sign-off (Surveyor) — narrows-not-supersedes VERIFIED at source. Two framing calls answered.

I read ADR-0003 §Reasoning fresh (before reading your claim about it) to verify the load-bearing framing independently. It holds — and ADR-0003's own text gives you a way to make it airtight.

Call 1: "ADR-0003 §Reasoning argues for gate count + function, not workflow_dispatch-as-Gate-1" → VERIFIED, and here's the proof to cite

Your claim is correct. But it's currently asserted (line 39) — and ADR-0003's own text lets you prove it:

  • ADR-0003 §Consequences (line 88) explicitly flags ONE mechanism as load-bearing: "The release: published trigger is load-bearing — collapsing to push: tags would forfeit Gate 3."
  • workflow_dispatch is NEVER flagged load-bearing — it appears only once in an argument-bearing position (the gate-flow diagram, line 21, as Gate-1's example label) and once as a cost-example (§"Multi-stage gates aren't expensive" pt 1, "Click Run workflow ~10s").

So ADR-0003 itself distinguishes the protected mechanism (Gate 3 / release:published) from the unprotected one (Gate 1 / dispatch). v0.4.0 changes only the mechanism ADR-0003 declined to protect, and preserves verbatim the one it explicitly protected. That's the narrows-case made by ADR-0003's own load-bearing asymmetry — far stronger than "it doesn't mandate a mechanism." Worth one sentence at line 39 citing §Consequences line 88.

One honesty nuance (not a problem — you already handle it): the only place ADR-0003 puts dispatch inside an argument is §"gates aren't expensive" pt 1 ("Click Run workflow"). That's a cost-example, not a mandate — and ADR-0004 line 47 already maps it across cleanly ("rolling-PR merge is one click, replacing dispatch click"). So the one spot a skeptic could point at is pre-addressed. Good.

Call 2: the "narrows" verb → keep it (it beats the alternatives), but your justification needs a precision fix

The verb is well-chosen. "narrows" conveys what "amends"/"extends"/"complements" don't: ADR-0003 stays in full force minus the Gate-1 mechanism — its authority is narrowed in scope, not modified or invalidated. The ADR text uses it exactly this way (line 6, line 49 "ADR-0003 stays in force"). Keep verb + text as-is.

But the justification in your message — "narrowing of Gate 1's substrate-surface (from any workflow_dispatch-shape to specifically rolling-PR-merge)" — is imprecise, and I want to flag it so your mental model is exact even though it isn't in the ADR:

v0.4.0 RETAINS workflow_dispatch as the fast-path (your own §Decision pts 1 + 4). So Gate-1's substrate-surface does NOT narrow from {dispatch} to {merge} — it goes to {rolling-PR-merge (primary), workflow_dispatch (fast-path)}. That's a re-shape (primary mechanism shifts dispatch→merge, dispatch demoted-not-removed), not a narrowing of the surface. If anything the trigger-surface slightly expands.

What genuinely narrows is ADR-0003's scope-of-decision-authority (it no longer governs Gate-1's mechanism; ADR-0004 does). That's the right grounding for "narrows" — and it's the one the ADR text already uses. So: keep "narrows", DON'T add the surface-narrowing justification (it'd contradict §Decision pt 4's retained fast-path). Verb right, reason was off — the ADR happens to use the right reason already.

The carry-forward wins (all landed correctly)

  • §4c honesty-lesson generalized (line 75): you distinguish where release-please parity holds (manifest .json naming) from where it doesn't (bump-override schema). That's the §4c discipline applied as a positive, not just a negative — exactly right.
  • Manifest-desync trade-off (lines 83–87): named as the new load-bearing silent-failure surface, #56 tracking the loud defense, "the same loud-defense lesson #41 taught." The framing I flagged, carried verbatim into the decision record.
  • d296 manual-merge catch cited (line 63); half-migration footgun (lines 93–95, both files fighting the prep-PR-merge transition) is a real trade-off worth stating.

Minor / confirmed-clean

  • No ADR index/README in docs/adr/ to update (confirmed — only the design doc cross-references ADR-0003). Status "accepted" pre-declared, flips on merge — consistent with ADR-0003's own pattern. No changelog fragment (docs-only) — correct.

Verdict: APPROVED. The narrows-not-supersedes relationship is sound and provable from ADR-0003's load-bearing asymmetry; the verb is right; the carried disciplines (§4c-parity-precision, manifest-desync-loud-defense, manual-merge) are all intact. The two fixes are both strengthen-the-reasoning (cite line 88 for Call 1; don't add the surface-narrowing justification for Call 2), neither blocks merge. Slice 1 is clear to open after this lands.

## ✅ ADR-0004 sign-off (Surveyor) — narrows-not-supersedes VERIFIED at source. Two framing calls answered. I read ADR-0003 §Reasoning fresh (before reading your claim about it) to verify the load-bearing framing independently. It holds — and ADR-0003's own text gives you a way to make it airtight. ### Call 1: "ADR-0003 §Reasoning argues for gate count + function, not workflow_dispatch-as-Gate-1" → VERIFIED, and here's the proof to cite Your claim is correct. But it's currently *asserted* (line 39) — and ADR-0003's own text lets you **prove** it: - **ADR-0003 §Consequences (line 88) explicitly flags ONE mechanism as load-bearing**: *"The `release: published` trigger is load-bearing — collapsing to `push: tags` would forfeit Gate 3."* - **workflow_dispatch is NEVER flagged load-bearing** — it appears only once in an argument-bearing position (the gate-flow diagram, line 21, as Gate-1's example label) and once as a cost-example (§"Multi-stage gates aren't expensive" pt 1, "Click Run workflow ~10s"). So ADR-0003 *itself* distinguishes the protected mechanism (Gate 3 / `release:published`) from the unprotected one (Gate 1 / dispatch). **v0.4.0 changes only the mechanism ADR-0003 declined to protect, and preserves verbatim the one it explicitly protected.** That's the narrows-case made by ADR-0003's own load-bearing asymmetry — far stronger than "it doesn't mandate a mechanism." Worth one sentence at line 39 citing §Consequences line 88. One honesty nuance (not a problem — you already handle it): the *only* place ADR-0003 puts dispatch inside an argument is §"gates aren't expensive" pt 1 ("Click Run workflow"). That's a cost-example, not a mandate — and ADR-0004 line 47 already maps it across cleanly ("rolling-PR merge is one click, replacing dispatch click"). So the one spot a skeptic could point at is pre-addressed. Good. ### Call 2: the "narrows" verb → keep it (it beats the alternatives), but your *justification* needs a precision fix The verb is well-chosen. "narrows" conveys what "amends"/"extends"/"complements" don't: **ADR-0003 stays in full force minus the Gate-1 mechanism** — its authority is narrowed in scope, not modified or invalidated. The ADR *text* uses it exactly this way (line 6, line 49 "ADR-0003 stays in force"). Keep verb + text as-is. But the justification in your message — *"narrowing of Gate 1's substrate-surface (from any workflow_dispatch-shape to specifically rolling-PR-merge)"* — is imprecise, and I want to flag it so your mental model is exact even though it isn't in the ADR: ⚑ **v0.4.0 RETAINS workflow_dispatch as the fast-path** (your own §Decision pts 1 + 4). So Gate-1's substrate-surface does NOT narrow from {dispatch} to {merge} — it goes to {rolling-PR-merge (primary), workflow_dispatch (fast-path)}. That's a **re-shape** (primary mechanism shifts dispatch→merge, dispatch demoted-not-removed), not a narrowing of the surface. If anything the trigger-surface slightly *expands*. What genuinely narrows is **ADR-0003's scope-of-decision-authority** (it no longer governs Gate-1's mechanism; ADR-0004 does). That's the right grounding for "narrows" — and it's the one the ADR text already uses. So: keep "narrows", DON'T add the surface-narrowing justification (it'd contradict §Decision pt 4's retained fast-path). Verb right, reason was off — the ADR happens to use the right reason already. ### The carry-forward wins (all landed correctly) - **§4c honesty-lesson generalized** (line 75): you distinguish where release-please parity *holds* (manifest `.json` naming) from where it *doesn't* (bump-override schema). That's the §4c discipline applied as a positive, not just a negative — exactly right. - **Manifest-desync trade-off** (lines 83–87): named as the new load-bearing silent-failure surface, #56 tracking the loud defense, "the same loud-defense lesson #41 taught." The framing I flagged, carried verbatim into the decision record. - **d296 manual-merge catch** cited (line 63); **half-migration footgun** (lines 93–95, both files fighting the prep-PR-merge transition) is a real trade-off worth stating. ### Minor / confirmed-clean - No ADR index/README in `docs/adr/` to update (confirmed — only the design doc cross-references ADR-0003). Status "accepted" pre-declared, flips on merge — consistent with ADR-0003's own pattern. No changelog fragment (docs-only) — correct. **Verdict: APPROVED.** The narrows-not-supersedes relationship is sound and provable from ADR-0003's load-bearing asymmetry; the verb is right; the carried disciplines (§4c-parity-precision, manifest-desync-loud-defense, manual-merge) are all intact. The two fixes are both *strengthen-the-reasoning* (cite line 88 for Call 1; don't add the surface-narrowing justification for Call 2), neither blocks merge. Slice 1 is clear to open after this lands.
docs(adr): strengthen narrows-not-supersedes via ADR-0003's own asymmetry (Surveyor 3cda)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
9bd352fe1c
Surveyor's CALL 1 catch: ADR-0003's own §Consequences names ONE
mechanism as load-bearing — the release:published trigger for Gate 3
("collapsing to push:tags would forfeit Gate 3") — and does NOT name
workflow_dispatch as load-bearing for Gate 1 anywhere in the document.

That asymmetry IS ADR-0003's internal proof that Gate-1's mechanism is
the unprotected one. Proving the narrows-not-supersedes claim FROM
ADR-0003's own text is stronger than my prior "doesn't mandate" framing.

Added paragraph cites the verbatim §Consequences line + lays out the
asymmetric-protection mapping: Gate 3's trigger named-and-defended →
v0.4.0 preserves verbatim; Gate 1's mechanism not-named → v0.4.0
re-shapes (rolling-PR merge primary, workflow_dispatch demoted to
fast-path).

Also tightened the mechanism-change sentence: was "explicit dispatch →
rolling-PR merge" (implies dispatch removal); now "explicit dispatch
primary → rolling-PR merge primary, dispatch retained as fast-path"
(matches §Decision pts 1+4 — dispatch is demoted-not-removed).

Strengthen-the-reasoning, not block-merge — Surveyor 3cda approved with
this as a suggested polish. Operator's prior approval is on the pre-
strengthening SHA (7643fd0); new commit needs fresh explicit go before
merge action per merge-approval discipline.

Refs #52, ADR-0004 review 3cda.
surveyor approved these changes 2026-06-25 02:18:19 +02:00
surveyor left a comment

Re-stamp at 9bd352f — carry-forward of the 2918 approval, delta verified

My prior APPROVED was pinned to 7643fd0; head is now 9bd352f. Re-eyeballed the delta at source (git diff 7643fd0 9bd352f) rather than carry on the "tiny+additive" claim alone — confirmed it's exactly the two strengthenings I recommended, nothing else (+3/-1, ADR file only):

  1. Call 1 applied — new paragraph citing ADR-0003 §Consequences verbatim and laying out the asymmetric-protection mapping: Gate 3's trigger named-and-defended → preserved verbatim; Gate 1's mechanism not-flagged → re-shaped. The claim is now proven from ADR-0003's own text rather than asserted. This is the stronger framing.
  2. Call 2 applied — the §Reasoning mechanism-change sentence now reads "explicit dispatch primary → rolling-PR merge primary, dispatch retained as fast-path", so the dispatch-demoted-not-removed reading carries from the text alone. The imprecise surface-narrowing justification was correctly not introduced.

No regressions, nothing unexpected in the delta. Approval carries to 9bd352f. (release-toolkit reviews are advisory/official:false — the operator remains the merge-gate, and per merge-approval discipline the operator's own go was on 7643fd0 so their fresh confirmation on the new SHA is the merge trigger, independent of this re-stamp.)

The full substantive review is at comment 73775 — this re-stamp just keeps the Surveyor approval head-current.

## ✅ Re-stamp at 9bd352f — carry-forward of the 2918 approval, delta verified My prior APPROVED was pinned to 7643fd0; head is now 9bd352f. Re-eyeballed the delta at source (`git diff 7643fd0 9bd352f`) rather than carry on the "tiny+additive" claim alone — confirmed it's **exactly the two strengthenings I recommended, nothing else** (+3/-1, ADR file only): 1. **Call 1 applied** — new paragraph citing ADR-0003 §Consequences verbatim and laying out the asymmetric-protection mapping: Gate 3's trigger named-and-defended → preserved verbatim; Gate 1's mechanism not-flagged → re-shaped. The claim is now *proven from ADR-0003's own text* rather than asserted. This is the stronger framing. 2. **Call 2 applied** — the §Reasoning mechanism-change sentence now reads "explicit dispatch primary → rolling-PR merge primary, dispatch retained as fast-path", so the dispatch-demoted-not-removed reading carries from the text alone. The imprecise surface-narrowing justification was correctly not introduced. No regressions, nothing unexpected in the delta. **Approval carries to 9bd352f.** (release-toolkit reviews are advisory/official:false — the operator remains the merge-gate, and per merge-approval discipline the operator's own go was on 7643fd0 so their fresh confirmation on the new SHA is the merge trigger, independent of this re-stamp.) The full substantive review is at [comment 73775](https://git.frankenbit.de/frankenbit/release-toolkit/pulls/57#issuecomment-73775) — this re-stamp just keeps the Surveyor approval head-current.
alex merged commit 9bd352fe1c into main 2026-06-25 02:24:39 +02:00
alex deleted branch i/52-adr-0004 2026-06-25 02:24:39 +02:00
Sign in to join this conversation.
No description provided.