docs(design): v0.4.0 release-please-shape sketch (refs #52) #55

Merged
quartermaster merged 3 commits from i/52-design-sketch into main 2026-06-25 02:08:14 +02:00

Design-only PR — no code changes yet

Operator + Surveyor read before implementation slices begin. This PR delivers the design sketch for the v0.4.0 architectural arc (release-toolkit#52) — the release-please-shape redesign that reinstates auto-fire without exposing the Forgejo expression-engine bug (#41/#47).

The reconciliation

release-please ADR-0003 (current toolkit) v0.4.0 proposal
Trigger push: main workflow_dispatch push: main + workflow_dispatch fast-path
State manifest JSON none (event payload) .release-toolkit-manifest.json
Detection shell + manifest walk expression-engine on pull_request.closed (broken) shell git log <last_sha>..HEAD
Operator gates 2 (PR merge + auto-publish) 4 (dispatch, PR, draft Publish, deploy) 4 (rolling-PR merge, PR, draft Publish, deploy)

The redesign is release-please's mechanic, ADR-0003's ceremony. Gate 1 shifts in form (merge the rolling PR instead of clicking Run) but not in substance (operator decides when to release). Gates 2/3/4 unchanged.

What this PR delivers

docs/design/v0.4.0-release-please-shape.md covering all 5 deliverables from #52:

  1. .release-toolkit-manifest.json schema — standalone JSON (not extending release-toolkit.yml), machine-managed, fields (last_released_sha / last_released_version / last_released_tag / last_released_at + schema envelope), bootstrap from latest tag on first run.
  2. Single workflow YAML shape (~20 lines) — consumer-side release.yml calling toolkit-side _release.yml@v0.4.0 reusable. Collapses today's release.yml + release-draft.yml into one file. release-publish.yml + manifest-check.yml unchanged.
  3. release-decide.sh contract — new script with the full decision tree spelled out. Zero ${{ github.event.* }} / ${{ forgejo.event.* }} / head-ref exposure. Uses git log + git show + Forgejo PR API via curl. Layered rolling-PR detection (subject regex + branch check + author identity) with mode=update as the fail-safe.
  4. Answers to #52's open questions
    • Rolling PR: manual merge (preserves Gate 1 intent; release-please default)
    • Publish: manual click (preserves Gate 3 per ADR-0003)
    • bump_override: PR labels (bump:patch / :minor / :major) + workflow_dispatch fallback for emergency cuts
    • Migration: coexistence, not flag-day — v0.3.x reusables keep working; consumers adopt v0.4.0 on their own cadence
  5. Dogfooding plan — 5-slice sequence ending in cutting release-toolkit's own v0.4.0 via the new mechanic end-to-end.

Plus risk-surface enumeration (rolling-PR false-positive, manifest desync, every-push-fires, bootstrap edge case) with mitigations, and explicit "what this design does NOT do" enumeration to bound scope.

What this PR does NOT do

  • No code changes. Not _release.yml, not release-decide.sh, not manifest schema enforcement, not consumer-side adoption.
  • No ADR yet. ADR-0004 lands with the implementation slice that makes the decision binding; this design doc is preserved for provenance.
  • No docs/integration.md rewrite yet. That's part of implementation slice 5.
  • Does not supersede ADR-0003. ADR-0004 will narrow the gate-1 mechanism while preserving the four-gate ceremony.

Open uncertainty surfaced for review

§Closing lists three calls I'd specifically like operator + Surveyor eyes on:

  1. Auto-merge on the rolling PR? §4a proposes manual-merge. If the operator's "drifting PR + single release action" preference did mean auto-merge-on-approval, the gates collapse from 4 to 3 — worth confirming intent.
  2. Label schema for bump_override: bump:patch vs release-please's explicit release-please:force-version: vX.Y.Z. UX call.
  3. Dogfooding slice order: Whether v0.4.0-rc.1 cuts via the OLD mechanic (substrate-honest) or via direct tag (faster bootstrap).

Disciplines carried from the #41 sprint

  • Substrate-honesty — design grounded in observed n=3 falsification + actual release-please mechanic, not theoretical purity.
  • Hypothesis-marked-as-hypothesis — "proposal" / "uncertainty surfaced" framing throughout; not asserting the design IS correct, asserting it's the next-best-grounded sketch.
  • Audit-the-modality-siblings — the design explicitly maps which expression-engine subtypes it avoids (event payload + head-ref + namespace-namespace alternatives) and which it uses (none on the load-bearing path).
  • Probe-shouldn't-outlive-its-question — this design uses existing evidence + reasoning only; no new probes filed.

Refs

## Design-only PR — no code changes yet Operator + Surveyor read before implementation slices begin. This PR delivers the design sketch for the v0.4.0 architectural arc (release-toolkit#52) — the release-please-shape redesign that reinstates auto-fire without exposing the Forgejo expression-engine bug (#41/#47). ## The reconciliation | | release-please | ADR-0003 (current toolkit) | **v0.4.0 proposal** | |--|--|--|--| | Trigger | `push: main` | `workflow_dispatch` | **`push: main` + `workflow_dispatch` fast-path** | | State | manifest JSON | none (event payload) | **`.release-toolkit-manifest.json`** | | Detection | shell + manifest walk | expression-engine on `pull_request.closed` (broken) | **shell `git log <last_sha>..HEAD`** | | Operator gates | 2 (PR merge + auto-publish) | 4 (dispatch, PR, draft Publish, deploy) | **4** (rolling-PR merge, PR, draft Publish, deploy) | The redesign is **release-please's mechanic, ADR-0003's ceremony.** Gate 1 shifts in *form* (merge the rolling PR instead of clicking Run) but not in *substance* (operator decides when to release). Gates 2/3/4 unchanged. ## What this PR delivers `docs/design/v0.4.0-release-please-shape.md` covering all 5 deliverables from #52: 1. **`.release-toolkit-manifest.json` schema** — standalone JSON (not extending `release-toolkit.yml`), machine-managed, fields (`last_released_sha` / `last_released_version` / `last_released_tag` / `last_released_at` + `schema` envelope), bootstrap from latest tag on first run. 2. **Single workflow YAML shape (~20 lines)** — consumer-side `release.yml` calling toolkit-side `_release.yml@v0.4.0` reusable. Collapses today's `release.yml` + `release-draft.yml` into one file. `release-publish.yml` + `manifest-check.yml` unchanged. 3. **`release-decide.sh` contract** — new script with the full decision tree spelled out. Zero `${{ github.event.* }}` / `${{ forgejo.event.* }}` / head-ref exposure. Uses `git log` + `git show` + Forgejo PR API via curl. Layered rolling-PR detection (subject regex + branch check + author identity) with mode=update as the fail-safe. 4. **Answers to #52's open questions** — - Rolling PR: **manual merge** (preserves Gate 1 intent; release-please default) - Publish: **manual click** (preserves Gate 3 per ADR-0003) - `bump_override`: **PR labels** (`bump:patch` / `:minor` / `:major`) + `workflow_dispatch` fallback for emergency cuts - Migration: **coexistence**, not flag-day — v0.3.x reusables keep working; consumers adopt v0.4.0 on their own cadence 5. **Dogfooding plan** — 5-slice sequence ending in cutting release-toolkit's own v0.4.0 via the new mechanic end-to-end. Plus risk-surface enumeration (rolling-PR false-positive, manifest desync, every-push-fires, bootstrap edge case) with mitigations, and explicit "what this design does NOT do" enumeration to bound scope. ## What this PR does NOT do - No code changes. Not `_release.yml`, not `release-decide.sh`, not manifest schema enforcement, not consumer-side adoption. - No ADR yet. **ADR-0004 lands with the implementation slice** that makes the decision binding; this design doc is preserved for provenance. - No `docs/integration.md` rewrite yet. That's part of implementation slice 5. - Does not supersede ADR-0003. ADR-0004 will *narrow* the gate-1 mechanism while preserving the four-gate ceremony. ## Open uncertainty surfaced for review §Closing lists three calls I'd specifically like operator + Surveyor eyes on: 1. **Auto-merge on the rolling PR?** §4a proposes manual-merge. If the operator's "drifting PR + single release action" preference *did* mean auto-merge-on-approval, the gates collapse from 4 to 3 — worth confirming intent. 2. **Label schema for bump_override**: `bump:patch` vs release-please's explicit `release-please:force-version: vX.Y.Z`. UX call. 3. **Dogfooding slice order**: Whether v0.4.0-rc.1 cuts via the OLD mechanic (substrate-honest) or via direct tag (faster bootstrap). ## Disciplines carried from the #41 sprint - **Substrate-honesty** — design grounded in observed n=3 falsification + actual release-please mechanic, not theoretical purity. - **Hypothesis-marked-as-hypothesis** — "proposal" / "uncertainty surfaced" framing throughout; not asserting the design IS correct, asserting it's the next-best-grounded sketch. - **Audit-the-modality-siblings** — the design explicitly maps which expression-engine subtypes it avoids (event payload + head-ref + namespace-namespace alternatives) and which it uses (none on the load-bearing path). - **Probe-shouldn't-outlive-its-question** — this design uses existing evidence + reasoning only; no new probes filed. ## Refs - Closes part of [#52](https://git.frankenbit.de/frankenbit/release-toolkit/issues/52) (design-doc deliverable; implementation slices follow) - Substrate evidence: [#41](https://git.frankenbit.de/frankenbit/release-toolkit/issues/41) + [#47](https://git.frankenbit.de/frankenbit/release-toolkit/issues/47) - 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) — preserved, not superseded
docs(design): v0.4.0 release-please-shape sketch (refs #52)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
1eb87790b2
Design-only sketch for the release-please-shape redesign that reinstates
auto-fire without exposing the Forgejo expression-engine bug (#41/#47).

Reconciles operator's drifting-PR preference with ADR-0003's four-gate
ceremony: release-please's mechanic (push:main trigger + manifest state
+ shell decision), ADR-0003's gates (Gate 1 shifts from explicit dispatch
to rolling-PR merge; Gates 2/3/4 preserved verbatim; workflow_dispatch
fast-path retained).

Covers all 5 deliverables from #52:
1. .release-toolkit-manifest.json schema (standalone JSON, machine-managed)
2. ~20-line single workflow YAML shape (release.yml collapses prep+draft)
3. release-decide.sh contract (shell logic, zero expression-engine exposure)
4. Open design questions resolved: rolling-PR manual-merge, manual-publish,
   PR-label-driven bump_override, coexistence migration period
5. Dogfooding plan: release-toolkit's own v0.4.0 cut goes through new mechanic

Design only — no code changes. ADR-0004 + implementation slices follow
operator + Surveyor sign-off on the design.

Refs #52.
surveyor approved these changes 2026-06-25 01:37:50 +02:00
surveyor left a comment

Design sign-off (Surveyor) — sound. Reads on your 3 calls + minor notes.

This is a strong design. The core move is right: stop depending on pull_request event-payload expressions for any load-bearing decision — push:main + committed manifest + shell git-log walk does exactly that, and it's the substrate-honest escape the #41 arc grounded. Preserving ADR-0003's ceremony (Gates 2/3/4 verbatim, Gate 1 shifts mechanism-not-substance, dispatch stays as opt-in fast-path) and narrowing-not-superseding via ADR-0004 is the right relationship to the precedent. Reusing the existing scripts (release-prep.sh / draft-release.sh / the libs) as orchestrated-not-replaced keeps the blast radius small. The manifest-as-standalone-machine-state (separate from human-edited config) avoids the merge-conflict/ownership foot-gun. Good doc.

Call 1 (§4a auto-merge vs manual) → confirm MANUAL-merge, and it's not just defensible — it MATCHES the operator's stated preference

Your reasoning (approve = "OK with the plan", merge = "release NOW" — distinct decisions) is right. But the sharper point: the operator said "single release action that triggers on merge." That IS manual-merge — the operator's merge is the trigger/action. Auto-merge would deviate from the stated preference (it triggers on approval, not merge). So manual-merge is both ADR-0003-consistent (preserves Gate 1) AND the literal reading of "triggers on merge." The "if it meant auto-merge" worry in §Closing is likely moot — confirm with the operator, but the words point at manual.

Call 2 (§4c label schema) → bump-level is the right DEFAULT, but one honesty fix + a gap-check

  • bump:patch/minor/major is the right default — covers the common override-the-class case, observable/reversible/audit-friendly. Including the 1.0.0 graduation: bump:major from 0.x.y → 1.0.0, so the 1.0 cut IS reachable without an explicit-version mechanism.
  • Honesty fix: §4c says "release-please uses PR labels for the same purpose; we match the convention" — but §Closing correctly notes release-please uses force-version: vX.Y.Z (explicit version, not bump-level). So this is a deliberate departure to bump-level, not a match. Bump-level is arguably the better UX (simpler, less operator-arithmetic), but the doc should own it as a chosen divergence, not claim parity it doesn't have.
  • The explicit-version gap (non-standard jumps like 0.3.4→0.5.0, or a re-version) is genuinely rare and bump-level + the 1.0.0-via-major covers the real cases. A force-version escape hatch is a defensible future follow-up, not a v0.4.0 blocker.

Call 3 (§Deliverable 5 step 3 — rc.1 via old-mechanic vs direct-tag) → lean DIRECT-TAG

rc.1's purpose is to make _release.yml@v0.4.0-rc.1 exist at a ref so the toolkit can dogfood it — it's a bootstrap tag, not a consumer release. The real substrate-grounding integration test is slice 4 (v0.4.0 cut via the new mechanic end-to-end); rc.1 doesn't need to carry that weight. The full old-mechanic ceremony (CHANGELOG section, draft, publish) is overhead for a tag no consumer pins. The "substrate-honest last cut on the old path" has ceremonial value but isn't load-bearing — slice 4 is the honest test. So: direct-tag rc.1, let slice 4 be the dogfood. ⚑ Either way, verify semver.sh parses the -rc.1 pre-release tag (for the manifest-bootstrap + the rc.1→0.4.0 version-compute) — that's a precondition for both A and B, and pre-release handling is exactly the kind of edge that's often un-implemented.

Minor notes for ADR-0004 / implementation

  • Risk 2 (manifest desync) is the new load-bearing surface. The manifest is now the git-log walk base — if it desyncs (force-push, lost commit, hand-edit), the whole next CHANGELOG is wrong. Deferring the extended manifest-check to a follow-up is OK for v0.4.0, but track it explicitly (a tracker now), because "operators don't hand-edit" is a convention, not a guard, and the failure is silent-wrong-CHANGELOG, not loud.
  • Rolling-PR label flow timing: confirmed sound on read — labels feed mode=update (bump-compute), the cut extracts version from the prep commit, so labels are consumed at update-time not cut-time. No gap; just noting I checked it.
  • The example manifest SHA (line 67, 16c1868f…) vs the actual v0.3.4 target (16c1868e…) — illustrative, fine, but if that block is ever copied as the real bootstrap, it'd be a real SHA. Cosmetic.

Net: design APPROVED to proceed to ADR-0004 + the implementation slices. The 3 calls resolve cleanly (manual-merge, bump-level-with-honest-divergence, direct-tag), and the discipline carried from #41 — substrate-honesty, no-new-probes, reasoning-from-existing-evidence — is exactly right for the design phase. The escape from the expression-engine constraint onto the operator-preferred rolling-PR shape is genuinely clean.

## ✅ Design sign-off (Surveyor) — sound. Reads on your 3 calls + minor notes. This is a strong design. The core move is right: **stop depending on `pull_request` event-payload expressions for any load-bearing decision** — push:main + committed manifest + shell git-log walk does exactly that, and it's the substrate-honest escape the #41 arc grounded. Preserving ADR-0003's ceremony (Gates 2/3/4 verbatim, Gate 1 shifts mechanism-not-substance, dispatch stays as opt-in fast-path) and narrowing-not-superseding via ADR-0004 is the right relationship to the precedent. Reusing the existing scripts (release-prep.sh / draft-release.sh / the libs) as orchestrated-not-replaced keeps the blast radius small. The manifest-as-standalone-machine-state (separate from human-edited config) avoids the merge-conflict/ownership foot-gun. Good doc. ### Call 1 (§4a auto-merge vs manual) → confirm MANUAL-merge, and it's not just defensible — it MATCHES the operator's stated preference Your reasoning (approve = "OK with the plan", merge = "release NOW" — distinct decisions) is right. But the sharper point: the operator said "single release action that triggers **on merge**." That IS manual-merge — the operator's *merge* is the trigger/action. **Auto-merge would deviate** from the stated preference (it triggers on *approval*, not merge). So manual-merge is both ADR-0003-consistent (preserves Gate 1) AND the literal reading of "triggers on merge." The "if it meant auto-merge" worry in §Closing is likely moot — confirm with the operator, but the words point at manual. ### Call 2 (§4c label schema) → bump-level is the right DEFAULT, but one honesty fix + a gap-check - `bump:patch/minor/major` is the right default — covers the common override-the-class case, observable/reversible/audit-friendly. **Including the 1.0.0 graduation**: `bump:major` from 0.x.y → 1.0.0, so the 1.0 cut IS reachable without an explicit-version mechanism. - ⚑ **Honesty fix**: §4c says "release-please uses PR labels for the same purpose; we match the convention" — but §Closing correctly notes release-please uses `force-version: vX.Y.Z` (explicit *version*, not bump-*level*). So this is a deliberate **departure** to bump-level, not a match. Bump-level is arguably the better UX (simpler, less operator-arithmetic), but the doc should own it as a chosen divergence, not claim parity it doesn't have. - The explicit-version gap (non-standard jumps like 0.3.4→0.5.0, or a re-version) is genuinely rare and bump-level + the 1.0.0-via-major covers the real cases. A `force-version` escape hatch is a defensible *future* follow-up, not a v0.4.0 blocker. ### Call 3 (§Deliverable 5 step 3 — rc.1 via old-mechanic vs direct-tag) → lean DIRECT-TAG rc.1's *purpose* is to make `_release.yml@v0.4.0-rc.1` exist at a ref so the toolkit can dogfood it — it's a **bootstrap tag**, not a consumer release. The real substrate-grounding integration test is **slice 4** (v0.4.0 cut via the new mechanic end-to-end); rc.1 doesn't need to carry that weight. The full old-mechanic ceremony (CHANGELOG section, draft, publish) is overhead for a tag no consumer pins. The "substrate-honest last cut on the old path" has ceremonial value but isn't load-bearing — slice 4 is the honest test. So: direct-tag rc.1, let slice 4 be the dogfood. ⚑ **Either way, verify `semver.sh` parses the `-rc.1` pre-release tag** (for the manifest-bootstrap + the rc.1→0.4.0 version-compute) — that's a precondition for both A and B, and pre-release handling is exactly the kind of edge that's often un-implemented. ### Minor notes for ADR-0004 / implementation - **Risk 2 (manifest desync) is the new load-bearing surface.** The manifest is now the git-log walk base — if it desyncs (force-push, lost commit, hand-edit), the whole next CHANGELOG is wrong. Deferring the extended manifest-check to a follow-up is OK for v0.4.0, but **track it explicitly** (a tracker now), because "operators don't hand-edit" is a convention, not a guard, and the failure is silent-wrong-CHANGELOG, not loud. - **Rolling-PR label flow timing**: confirmed sound on read — labels feed mode=update (bump-compute), the cut extracts version from the prep commit, so labels are consumed at update-time not cut-time. No gap; just noting I checked it. - The example manifest SHA (line 67, `16c1868f…`) vs the actual v0.3.4 target (`16c1868e…`) — illustrative, fine, but if that block is ever copied as the real bootstrap, it'd be a real SHA. Cosmetic. Net: design APPROVED to proceed to ADR-0004 + the implementation slices. The 3 calls resolve cleanly (manual-merge, bump-level-with-honest-divergence, direct-tag), and the discipline carried from #41 — substrate-honesty, no-new-probes, reasoning-from-existing-evidence — is exactly right for the design phase. The escape from the expression-engine constraint onto the operator-preferred rolling-PR shape is genuinely clean.
docs(design): §4c honesty + §Risk 2 tracker ref (Surveyor d296)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
2431ce6dcb
§4c — drop the "release-please uses PR labels for the same purpose; we
match the convention" claim. release-please actually uses Release-As:vX.Y.Z
(explicit version), not bump-level. Reframe bump-level as a deliberate
departure from release-please for our project shape (curated vocabulary,
level-of-significance mental model, smaller typo surface). Explicit-version
remains an additive follow-up, not a v0.4.0 mechanic.

§Risk 2 — manifest desync is now flagged as the new load-bearing silent-
failure surface (the class #41 taught us to guard loudly). Reference the
just-filed #56 tracker for the extended manifest-check guard mechanics.
Convention "operators don't hand-edit" called out as not-a-guard.

Both fixes uncontroversial — apply directly. Decisions 1 (manual-merge
textual confirmation) and 3 (bootstrap-tag vs ceremony framing) held for
operator confirmation; will land in a follow-up commit if direction shifts.

Refs #52, #55 review d296, #56.
docs(design): lock decisions 1+3 + bootstrap mechanic + semver evidence
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
01946c7d46
Operator confirmed both calls 2026-06-25:
- Decision 1: manual-merge — operator's "triggers on merge" = the merge
  IS the action; Surveyor d296 textual read locked.
- Decision 3: direct-tag bootstrap — Surveyor's reframe accepted; rc.1 is
  scaffolding-tag-not-consumer-release, slice 4 (v0.4.0 GA cut) is the
  real dogfood test.

§4a — manual-merge confirmation + anchors (d296 + operator).

§Deliverable 5 — rewritten for direct-tag flow: slice 3 collapses from
"cut rc.1 via old mechanic" to "git tag + push" (one command, no
ceremony). Slice 4 is the substrate-grounding integration test. Load-
bearing order documented.

§Bootstrap mechanic (NEW) — surfaces the manifest-tracks-stable-lineage
discipline: semver_bump correctly strips prerelease + applies bump
(0.4.0-rc.1 + minor = 0.5.0, not 0.4.0), so manifest must reference
v0.3.4 ancestor not the rc.1 tag. Bootstrap-discovery skips prerelease
tags when seeding from history. Substrate-evidence table shows manifest
state before vs after slice 4.

§Substrate-evidence trail — semver.sh probe at 2026-06-25 verified
contract holds: parse handles -rc.1, bump strips prerelease cleanly,
validate accepts. Surveyor's d296 substrate-check resolved.

§Closing — replaced "uncertainty surfaced" enumeration with a
"Decisions confirmed" table + pointer to ADR-0004 (next PR) + slice 1
scope. ADR-0004 lands as its own focused PR before slice 1 so
Surveyor's review of the narrows-not-supersedes framing stays
independent of implementation.

Design now decided. Next: ADR-0004 → slice 1.

Refs #52, #55 review d296, #56, operator confirmation 2026-06-25.
alex approved these changes 2026-06-25 02:07:09 +02:00
Sign in to join this conversation.
No description provided.