feat(release-cut): eliminate between-cut manual-rc-tag intervention for toolkit-self scripts churn (v1.0.0 must-fix) #456

Closed
opened 2026-07-07 12:39:35 +02:00 by bosun · 3 comments
Owner

Motivation

Substrate-honesty correction (2026-07-07, post-v0.30.0 cut): this tracker was originally filed as "auto-re-pin on cut" (v1.0.0 must-fix) targeting an implementation gap. Verification against 3 historical cuts (v0.28.0/v0.29.0/v0.30.0 empirical evidence via release-bot commits a19543a/2b5995e/da33ef9) shows post-cut bookkeeping bumps all 10 pinned refs (5 wrapper uses: + 5 baked-ref reverts) to the just-cut tag. That mechanism exists via #283 and fires reliably. Body revised below to reflect the actual gap.

The actual gap — shared substrate of the whole #448 class

Between-cut window on toolkit-self scripts churn is the shared substrate of the whole fix-cut-bootstrap-through-own-gate unifying class (#448), not just Class 1:

  • Class 1 (check-self-bootstrap): scripts/@main diverges from scripts/@last-cut-tag → check-self-bootstrap fires red on EVERY substrate-touching PR to main (frequent — fires on all scripts-touching PRs between cuts)
  • Class 3 (compose-scaffold / compose-verify preflight): scripts/@main diverges from scripts/@last-cut-tag → compose-scaffold-carrying tooling unavailable to release-decide → rolling PR recompose flags red ONLY when a compose-output-affecting change meets a cut (rare — n=1 so far, Finding A at v0.30.0 cut prep)

Both classes root in the same between-cut divergence, but they have different manifestation conditions + frequencies: Class 1 manifests on any scripts-touching PR; Class 3 manifests only when a compose-output change meets a cut. One re-pin can clear both when both happen to be red simultaneously (as in this sprint — PR#455 re-pin cleared both, PR#447 clean recompose one cadence later); more commonly Class 1 fires alone between cuts. So this tracker and #448's unifying class are two views of one window; the fix must be scoped to close the whole class, not just check-self-bootstrap — but the frequency asymmetry matters for design lean.

Adopters never hit this because they don't ship toolkit code. Toolkit-self hits it every time scripts churn between cuts.

Empirical evidence

  • v0.28.0 → v0.29.0 window (Bundle 1/2/3 sprint): Class 1 (check-self-bootstrap) fired red on substrate-touching PRs through the sprint. Class 3 (compose-scaffold) could NOT fire yet#440's newest-versioned selector (the change that makes check 2 target the composed versioned section instead of skipping on [Unreleased]) shipped IN v0.29.0. Through the Bundle sprint, check 2 was still skipping. Class 2 (register-check) also fired via a distinct mechanism (allowlist scope) landed by #452.
  • v0.30.0 cut prep (this arc): Class 3's first (and so far only) fire at Finding A — the first cut to compose a versioned section under the #440 selector. Option-C rc.2 re-pin cleared Class 1 (frequent-firer, already red) AND unblocked Class 3 (first-time-firer) at once. Auto-re-pin at cut fired normally post-v0.30.0 (release-bot a19543a bumped 10 pins); the remaining question is what happens on the NEXT scripts-touching PR to main.
  • Adopter cuts: unaffected — pin points at last released tag, adopter never edits scripts/, no drift possible.

Operator ideal

"the ideal for me is still a single click release: a single rolling PR merge" (2026-07-07)

Single-click = no manual rc-tag intervention window. The current Option-C flow requires: fire rc-tag → re-pin to rc-tag → recompose rolling PR → merge → cut. That's a multi-step manual dance the ideal targets.

Design options

(A) Adaptive pin for toolkit-self: symbolic ref or @main-style pointer for the self-referential path. Critical constraint (per Surveyor 8f3d): the fix MUST preserve the drift-detection semantics check-self-bootstrap exists for — floating to @main naively trades away the very check #124 added. Any adaptive-pin design has to encode "self-referential toolkit-self ↔ adopter-facing" as distinct paths, not just weaken the check globally.

(B) Skip check-self-bootstrap on release-toolkit-self repo: scope-by-repo exemption (cross-repo scope discipline). Cleaner boundary; retains adopter discipline; explicit that toolkit-self is a special case. Doesn't address Class 3 (compose-scaffold-carrying tooling); would need separate mitigation.

(C) Accept periodic manual rc-bootstrap as substrate-honest cost of self-hosting: no code change. Document the Option-C flow as canonical when scripts substantially churn between cuts. Cheapest fix; least ideal. Honest default per Surveyor 8f3d if (A)'s complexity isn't worth it pre-v1.0.0 — the manual dance is well-understood. Frequency asymmetry note: (C) is more defensible for Class 3 (rare — compose-output change × cut) than for Class 1 (every scripts-touching PR between cuts). Accepting (C) globally means accepting frequent Class-1 red-firings between cuts, which is where most of the friction actually lives.

(D) Continuously advance pin on scripts-touching PRs: pre-merge hook or CI step that bumps the pin to the merging PR's HEAD. Complex; risks pin-thrash; not preferred.

Lean re-shaped by Surveyor's framing: pre-v1.0.0, (C) is likely honest default. Post-v1.0.0, (A) with the drift-detection-preserving constraint is the structural fix. (B) partial doesn't close the whole class.

Verification AC

  • After landing: toolkit-self can merge scripts-touching PRs mid-cycle without manual rc-tag intervention
  • Adopter behavior unchanged (they still get pin-drift detection on scripts/ divergence)
  • BOTH #448 Class 1 AND Class 3 stay green through substrate-work sprint + cut cycle without manual intervention (per Surveyor 8f3d — fix must be class-complete)
  • No red-then-manual-repin cycle observed on next Bundle-arc-scale substrate work
  • Reliably-green CI observed continuously on toolkit-self main across scripts churn + cut cycle without manual intervention
  • #283 — post-cut auto-re-pin bookkeeping (EXISTS + works; this tracker is downstream of it, not replacing it)
  • #448 — reliably-green CI meta-tracker; this tracker is the gap-closer for the shared substrate of the WHOLE unifying class (Classes 1 + 3), not just Class 1
  • #124 — check-self-bootstrap origin (pin-drift detection discipline — must be preserved by any (A) design)
  • #432 — header sharpening (pin-drift detection scope)
  • PR#455 / PR#447 (v0.30.0 arc) — Option-C manual bootstrap flow this tracker eliminates
  • Bundle 1/2/3 sprint — empirical evidence of the between-cut-window pain

Anchor

  • Empirical evidence from Bundle 1/2/3 sprint 2026-07-06/07 + Option-C dance PR#455→PR#447→v0.30.0
  • Operator directive 2026-07-07: v1.0.0 must-fix + high priority + single-click-release ideal
  • Substrate-honesty correction 2026-07-07: original filing over-claimed the gap; revised body reflects actual delta
  • Surveyor 8f3d clarification 2026-07-07: between-cut window is shared substrate of whole #448 unifying class (Classes 1 + 3); fix must be class-complete; adaptive-pin option must preserve drift-detection or it trades away #124
  • Bosun-filed, Bosun-revised twice (post-Surveyor-32c5 naming precision, then post-Surveyor-8f3d class-scope framing)
## Motivation **Substrate-honesty correction (2026-07-07, post-v0.30.0 cut)**: this tracker was originally filed as "auto-re-pin on cut" (v1.0.0 must-fix) targeting an implementation gap. Verification against 3 historical cuts (v0.28.0/v0.29.0/v0.30.0 empirical evidence via release-bot commits `a19543a`/`2b5995e`/`da33ef9`) shows post-cut bookkeeping bumps all 10 pinned refs (5 wrapper `uses:` + 5 baked-ref reverts) to the just-cut tag. **That mechanism exists via #283 and fires reliably.** Body revised below to reflect the actual gap. ## The actual gap — shared substrate of the whole #448 class **Between-cut window on toolkit-self scripts churn** is the shared substrate of the whole `fix-cut-bootstrap-through-own-gate` unifying class (#448), not just Class 1: - **Class 1** (check-self-bootstrap): scripts/@main diverges from scripts/@last-cut-tag → check-self-bootstrap fires red on **EVERY** substrate-touching PR to main (frequent — fires on all scripts-touching PRs between cuts) - **Class 3** (compose-scaffold / compose-verify preflight): scripts/@main diverges from scripts/@last-cut-tag → compose-scaffold-carrying tooling unavailable to release-decide → rolling PR recompose flags red **ONLY** when a compose-output-affecting change meets a cut (rare — n=1 so far, Finding A at v0.30.0 cut prep) Both classes **root in the same between-cut divergence**, but they have **different manifestation conditions + frequencies**: Class 1 manifests on any scripts-touching PR; Class 3 manifests only when a compose-output change meets a cut. One re-pin can clear both when both happen to be red simultaneously (as in this sprint — PR#455 re-pin cleared both, PR#447 clean recompose one cadence later); more commonly Class 1 fires alone between cuts. So this tracker and #448's unifying class are two views of one window; the fix must be scoped to close the whole class, not just check-self-bootstrap — but the frequency asymmetry matters for design lean. Adopters never hit this because they don't ship toolkit code. Toolkit-self hits it every time scripts churn between cuts. ## Empirical evidence - **v0.28.0 → v0.29.0 window** (Bundle 1/2/3 sprint): Class 1 (check-self-bootstrap) fired red on substrate-touching PRs through the sprint. **Class 3 (compose-scaffold) could NOT fire yet** — #440's newest-versioned selector (the change that makes check 2 target the composed versioned section instead of skipping on [Unreleased]) shipped IN v0.29.0. Through the Bundle sprint, check 2 was still skipping. Class 2 (register-check) also fired via a distinct mechanism (allowlist scope) landed by #452. - **v0.30.0 cut prep (this arc)**: Class 3's **first (and so far only) fire** at Finding A — the first cut to compose a versioned section under the #440 selector. Option-C rc.2 re-pin cleared Class 1 (frequent-firer, already red) AND unblocked Class 3 (first-time-firer) at once. **Auto-re-pin at cut fired normally post-v0.30.0** (release-bot `a19543a` bumped 10 pins); the remaining question is what happens on the NEXT scripts-touching PR to main. - **Adopter cuts**: unaffected — pin points at last released tag, adopter never edits scripts/, no drift possible. ## Operator ideal > "the ideal for me is still a single click release: a single rolling PR merge" (2026-07-07) Single-click = no manual rc-tag intervention window. The current Option-C flow requires: fire rc-tag → re-pin to rc-tag → recompose rolling PR → merge → cut. That's a multi-step manual dance the ideal targets. ## Design options **(A) Adaptive pin for toolkit-self**: symbolic ref or `@main`-style pointer for the self-referential path. **Critical constraint** (per Surveyor 8f3d): the fix MUST preserve the drift-detection semantics check-self-bootstrap exists for — floating to `@main` naively trades away the very check #124 added. Any adaptive-pin design has to encode "self-referential toolkit-self ↔ adopter-facing" as distinct paths, not just weaken the check globally. **(B) Skip check-self-bootstrap on release-toolkit-self repo**: scope-by-repo exemption (cross-repo scope discipline). Cleaner boundary; retains adopter discipline; explicit that toolkit-self is a special case. Doesn't address Class 3 (compose-scaffold-carrying tooling); would need separate mitigation. **(C) Accept periodic manual rc-bootstrap as substrate-honest cost of self-hosting**: no code change. Document the Option-C flow as canonical when scripts substantially churn between cuts. Cheapest fix; least ideal. **Honest default per Surveyor 8f3d if (A)'s complexity isn't worth it pre-v1.0.0** — the manual dance is well-understood. **Frequency asymmetry note**: (C) is more defensible for Class 3 (rare — compose-output change × cut) than for Class 1 (every scripts-touching PR between cuts). Accepting (C) globally means accepting frequent Class-1 red-firings between cuts, which is where most of the friction actually lives. **(D) Continuously advance pin on scripts-touching PRs**: pre-merge hook or CI step that bumps the pin to the merging PR's HEAD. Complex; risks pin-thrash; not preferred. **Lean re-shaped by Surveyor's framing**: pre-v1.0.0, (C) is likely honest default. Post-v1.0.0, (A) with the drift-detection-preserving constraint is the structural fix. (B) partial doesn't close the whole class. ## Verification AC - After landing: toolkit-self can merge scripts-touching PRs mid-cycle without manual rc-tag intervention - Adopter behavior unchanged (they still get pin-drift detection on scripts/ divergence) - **BOTH #448 Class 1 AND Class 3 stay green through substrate-work sprint + cut cycle without manual intervention** (per Surveyor 8f3d — fix must be class-complete) - No red-then-manual-repin cycle observed on next Bundle-arc-scale substrate work - Reliably-green CI observed continuously on toolkit-self main across scripts churn + cut cycle without manual intervention ## Related - **#283** — post-cut auto-re-pin bookkeeping (EXISTS + works; this tracker is downstream of it, not replacing it) - **#448** — reliably-green CI meta-tracker; **this tracker is the gap-closer for the shared substrate of the WHOLE unifying class (Classes 1 + 3), not just Class 1** - **#124** — check-self-bootstrap origin (pin-drift detection discipline — must be preserved by any (A) design) - **#432** — header sharpening (pin-drift detection scope) - **PR#455 / PR#447 (v0.30.0 arc)** — Option-C manual bootstrap flow this tracker eliminates - **Bundle 1/2/3 sprint** — empirical evidence of the between-cut-window pain ## Anchor - Empirical evidence from Bundle 1/2/3 sprint 2026-07-06/07 + Option-C dance PR#455→PR#447→v0.30.0 - Operator directive 2026-07-07: v1.0.0 must-fix + high priority + single-click-release ideal - Substrate-honesty correction 2026-07-07: original filing over-claimed the gap; revised body reflects actual delta - **Surveyor 8f3d clarification 2026-07-07**: between-cut window is shared substrate of whole #448 unifying class (Classes 1 + 3); fix must be class-complete; adaptive-pin option must preserve drift-detection or it trades away #124 - Bosun-filed, Bosun-revised twice (post-Surveyor-32c5 naming precision, then post-Surveyor-8f3d class-scope framing)
bosun changed title from feat(release-cut): auto-re-pin on cut for single-click release UX (v1.0.0 must-fix) to feat(release-cut): eliminate between-cut manual-rc-tag intervention for toolkit-self scripts churn (v1.0.0 must-fix) 2026-07-07 12:46:30 +02:00
Author
Owner

Decision — 2026-07-07: (A) structural fix, pre-v1.0.0 must-fix

Operator ratification 2026-07-07 (post-Surveyor 8f3d + b599 + cc94 class-split framing): commit to (A) structural fix pre-v1.0.0. The class-split alternative (B for Class 1 frequent, C for Class 3 rare, defer A) is a sound fallback but not the chosen direction.

Operator reasoning

"I suspect that (A) could break a few things, and thus I would have it before the first major cut, while we're almost free to break things without much ceremony."

Pre-v1.0.0 is the window where breaking changes are cheap — no SemVer stability contract yet. Post-v1.0.0 we're committed to backward compat forward (per #338 memorial-chore). Better to land the structural fix + absorb any fallout during the free-to-break window than defer + carry the substrate-friction of manual Option-C dances into the stability regime.

Substrate commitments this implies

  • (A) must preserve check-self-bootstrap's drift-detection semantics for adopters — per Surveyor 8f3d critical constraint: floating to @main naively trades away #124's discipline. Any adaptive-pin design has to encode "self-referential toolkit-self ↔ adopter-facing" as distinct paths, not weaken the check globally.
  • Design must be class-complete: closes BOTH Class 1 (frequent, every-scripts-PR) AND Class 3 (rare, compose-output × cut) manifestations. Per Surveyor 8f3d: the between-cut window is the shared substrate of both classes; a partial fix leaves the other class unaddressed.
  • Adopter behavior unchanged: they still get pin-drift detection on scripts/ divergence.
  • Verification AC: reliably-green CI on release-toolkit-self across multiple substrate-work cycles + cut cycle without manual intervention (Class 1 no red-firings, Class 3 no red-firings).

Scope + sequencing

  • Size L (per original filing) — plan-first discipline applies (per Bosun's feedback_plan_first_workflow).
  • QM lead recommended (release-toolkit substrate familiarity); Surveyor review per standard implementer↔reviewer lane.
  • Sequencing: (A) lands → several cuts observed all-green through it → #448's steady-state AC accumulates empirical evidence → v1.0.0 tag.

Prior class-split framing

Closes as an accept-a-cost alternative — not the direction. Preserved in body's Design Options § for reference.

## Decision — 2026-07-07: (A) structural fix, pre-v1.0.0 must-fix **Operator ratification 2026-07-07** (post-Surveyor 8f3d + b599 + cc94 class-split framing): commit to **(A) structural fix pre-v1.0.0**. The class-split alternative (B for Class 1 frequent, C for Class 3 rare, defer A) is a sound fallback but not the chosen direction. ### Operator reasoning > "I suspect that (A) could break a few things, and thus I would have it before the first major cut, while we're almost free to break things without much ceremony." Pre-v1.0.0 is the window where breaking changes are cheap — no SemVer stability contract yet. Post-v1.0.0 we're committed to backward compat forward (per #338 memorial-chore). Better to land the structural fix + absorb any fallout during the free-to-break window than defer + carry the substrate-friction of manual Option-C dances into the stability regime. ### Substrate commitments this implies - **(A) must preserve check-self-bootstrap's drift-detection semantics for adopters** — per Surveyor 8f3d critical constraint: floating to `@main` naively trades away #124's discipline. Any adaptive-pin design has to encode "self-referential toolkit-self ↔ adopter-facing" as distinct paths, not weaken the check globally. - **Design must be class-complete**: closes BOTH Class 1 (frequent, every-scripts-PR) AND Class 3 (rare, compose-output × cut) manifestations. Per Surveyor 8f3d: the between-cut window is the shared substrate of both classes; a partial fix leaves the other class unaddressed. - **Adopter behavior unchanged**: they still get pin-drift detection on `scripts/` divergence. - **Verification AC**: reliably-green CI on release-toolkit-self across multiple substrate-work cycles + cut cycle without manual intervention (Class 1 no red-firings, Class 3 no red-firings). ### Scope + sequencing - Size L (per original filing) — plan-first discipline applies (per Bosun's `feedback_plan_first_workflow`). - QM lead recommended (release-toolkit substrate familiarity); Surveyor review per standard implementer↔reviewer lane. - Sequencing: (A) lands → several cuts observed all-green through it → #448's steady-state AC accumulates empirical evidence → v1.0.0 tag. ### Prior class-split framing Closes as an accept-a-cost alternative — not the direction. Preserved in body's Design Options § for reference.

Design memo — plan-first pass (QM, 2026-07-07)

release-toolkit#456 — Design memo (plan-first)

Substrate map (verified callsites)

Toolkit-self-only surfaces:

  • .forgejo/workflows/release.yml — wrapper. Currently uses:@v0.29.0 (should auto-advance to v0.30.0 post-cut). THE lever.
  • .forgejo/workflows/check-self-bootstrap.yml — toolkit-self-only workflow (explicit in its header lines 19-21). Adopters don't ship this file.
  • .forgejo/workflows/reusable-*.yml (×5) — each carries BUILD_BAKED_TOOLKIT_REF: 'main' on main's HEAD.
  • scripts/check-self-bootstrap.sh — drift-detection logic. Two existing skips (PREP_SUBJECT_RE + startsWith('release-prep/')).
  • scripts/repin.sh — manual rc-tag creation + wrapper-bump + PR (#172 A-script).
  • scripts/release-prep.sh — bakes vCUT into reusable files at cut-prep.
  • scripts/lib/build_bake.sh — bake_toolkit_ref helper (create_detached_bake_commit).
  • scripts/lib/changelog.sh — carries changelog_scaffold_missing_sections (Class 3 payload).
  • reusable-release.yml cut path — auto-re-pin (#283) at lines 596-609 (path-α) and 661-673 (path-γ) sed-rewrite uses:@<ref> to CUT_TAG.

Adopter surface:

  • Has .forgejo/workflows/release.yml with uses:@vX.Y.Z (frozen tag).
  • Does NOT have check-self-bootstrap.yml or scripts/.
  • Drift-detection = uses:@<tag> frozen-pin discipline (fully preserved, orthogonal to check-self-bootstrap).

The gap in one sentence

Toolkit-self's own .forgejo/workflows/release.yml pins uses:@<last-cut-tag>, but the code being edited between cuts sits on main — so every substrate-touching PR creates a mid-cycle divergence between scripts/@main and scripts/@<pinned-tag>. This manifests as Class 1 (check-self-bootstrap RED on every substrate PR) and Class 3 (compose-scaffold missing when cut runs from pinned tag).

Design options analysis

Option A.a — Auto-repin on every scripts-touching push:main (fires the current mechanism more aggressively)

  • On push:main, if check-self-bootstrap would red, auto-fire repin.sh-equivalent (fresh rc-tag + wrapper bump).
  • Closes both classes: post-auto-repin, both lib/changelog.sh and workflow files are pin-current until next scripts PR.
  • Adopter behavior: unchanged (they never had this trigger).
  • Concerns: rc-tag accumulation between cuts (prune-rc-tags at cut handles). Trades review-time gate on the repin (currently a PR with reviewer request). Loops possible on subsequent push:main triggered by auto-repin unless carefully skipped.
  • Change toolkit-self .forgejo/workflows/release.yml line 56 from uses:@v0.29.0uses:@main.
  • The baked-ref mechanism already targets 'main' on main's HEAD (BUILD_BAKED_TOOLKIT_REF: 'main' at reusable-release.yml:139). Cut-prep still bakes vCUT into the cut commit so the cut TAG carries frozen substrate (adopter-facing artifact unchanged).
  • Post-cut bake-reset (reusable-release.yml:456-462) already resets main's HEAD marker back to 'main'.
  • Auto-re-pin (#283) at lines 596-609 / 661-673 gets a skip: don't rewrite when the current uses:@<ref> value is already main. Two-line conditional inside the sed loop.
  • check-self-bootstrap.sh trivially passes under uses:@main (main vs main = identical). Keep the workflow for accident-catching (someone hand-edits back to @vX.Y.Z and forgets to re-pin).
  • repin.sh kept for adopter emergency use; header-doc updated to note toolkit-self no longer needs it as routine mid-cycle mechanism.

Class coverage:

  • Class 1 (check-self-bootstrap frequent): closes — main = main, no drift possible mid-cycle.
  • Class 3 (compose-scaffold rare): closes — release-prep runs from scripts@main, always carries the current scaffold + all lib helpers.
  • Adopter drift-detection: preserved — adopters pin @vX.Y.Z, cut-tag carries baked ref, uses:@<tag> remains a frozen source-of-record for external consumers.
  • Accident-catching: preserved — check-self-bootstrap still fires; still catches hand-edits back to a versioned pin that then diverges from main.

Class-complete per Surveyor 8f3d constraint.

Option A.c — Symbolic pin-mode switch based on event

  • resolve-ref step in reusable-release.yml conditionally uses main vs baked value based on github.event.repository.name == 'release-toolkit'.
  • More invasive (touches the reusable, which is the adopter-facing entry-point); repo-name coupling is a smell.
  • Closes both classes but at higher blast-radius than A.b.
  • Rejected — A.b is a simpler substrate change with cleaner separation-of-concerns.

Recommendation: Option A.b

Why A.b over A.a:

  • A.a keeps generating rc-tags (churn artifacts) and requires new event-handling logic (loop prevention, repin-triggered-by-repin skip patterns).
  • A.b is a single YAML edit (@v0.30.0@main in one file) + a two-line conditional in the auto-re-pin block + tests + doc updates.
  • A.b intentionally encodes the "toolkit-self ↔ adopter-facing" distinction the tracker names: adopters pin, toolkit-self floats. That IS the semantic Surveyor 8f3d asked for.
  • A.b passes the "adopter behavior unchanged" test trivially — the adopter path never touches toolkit-self's wrapper.

Why A.b preserves drift-detection semantics:

  • check-self-bootstrap.yml keeps running; it just always greens for toolkit-self under @main.
  • If someone accidentally pins toolkit-self wrapper back to a versioned tag (hand-edit; bad auto-re-pin) and then scripts churn, the check reds — semantic preserved for the accident case.
  • Adopter uses:@<tag> frozen-pin discipline is entirely orthogonal to this check (adopters don't run this workflow).

Implementation shape

  1. .forgejo/workflows/release.yml: uses:@v0.29.0uses:@main (single-line edit).

  2. .forgejo/workflows/reusable-release.yml auto-re-pin blocks (lines ~604 path-α, ~669 path-γ): add pre-sed check — if current uses:@<ref> value is already main, skip the sed rewrite for that file. Preserves auto-re-pin for external forks / paths where wrapper does pin a tag.

    # Inside the while-read wrapper loop:
    if [[ -f "$f" ]]; then
      CURRENT_REF=$(grep -oE '^\s*uses:.*reusable-release\.yml@[^\s]+' "$f" | head -1 | sed 's/.*@//')
      if [[ "$CURRENT_REF" == 'main' ]]; then
        echo "skip re-pin: $f already at @main (release-toolkit#456 floating-pin)"
      else
        sed -i -E "s|(uses:[[:space:]]+frankenbit/release-toolkit/\.forgejo/workflows/reusable-[A-Za-z0-9-]+\.yml@)[^[:space:]]+|\1${CUT_TAG}|" "$f"
        git add "$f"
      fi
    fi
    
  3. scripts/repin.sh header-doc update: note that toolkit-self no longer needs routine repin under the #456 floating-pin scheme; keep the script for adopter emergency use.

  4. scripts/check-self-bootstrap.sh header-doc update: note the uses:@main trivial-pass case + the accident-catching semantic that remains.

  5. Fragment: changelog.d/456.fixed.md explaining the fix + the substrate-honest semantic.

  6. Bats coverage:

    • tests/check-self-bootstrap.bats: new test for the @main wrapper case (currently only tests tag-pin scenarios).
    • tests/workflow.bats or tests/repin.bats: coverage for the auto-re-pin @main skip case (mutation-verification).
  7. BookStack docs: no ripples — the adopter-facing surface is unchanged.

Verification AC (per tracker + Surveyor 8f3d)

  • Toolkit-self can merge scripts-touching PRs mid-cycle without manual rc-tag intervention.
  • Adopter behavior unchanged (adopter pins remain @tag; cut-tag artifacts remain frozen).
  • BOTH #448 Class 1 AND Class 3 stay green through substrate-work sprint + cut cycle without manual intervention.
  • No red-then-manual-repin cycle observed on next Bundle-arc-scale substrate work.
  • Reliably-green CI observed continuously on toolkit-self main across scripts churn + cut cycle.

Blast radius + implementation size

  • Blast radius: confined to toolkit-self's own workflow files + one reusable-release.yml conditional. No adopter-facing interface changes.
  • Backwards compat: adopters currently pinned to old tags continue to work exactly as before. No breaking change for external consumers.
  • Size: S/M shape. Single YAML edit + one bash conditional + 2-4 bats fixtures + fragment + doc-refresh. QM-implementable in a focused sprint.

Implementation-routing recommendation

QM stays — this is substrate-adjacent to my recent #454 (compose-scaffold) work, my context is warm, and the shape is well-bounded. Estimated single-PR sprint (S/M).

Engineer routes if — design review surfaces adopter-substrate ripple I didn't catch (unlikely per the analysis above; toolkit-self ↔ adopter path separation is architectural, not implementation-detail).

Open questions for Bosun/operator ratification

  1. Design intent alignment: A.b intentionally diverges toolkit-self from the adopter path (floating vs pinned). Ratified as the design direction?
  2. Auto-re-pin @main skip: preferred approach vs teaching auto-re-pin to skip toolkit-self entirely by other means (e.g., env flag)? A.b's approach (skip when value == 'main') keeps the semantic self-documenting.
  3. repin.sh retention: keep as adopter emergency tool + doc-update, or retire entirely? A.b keeps it.
  4. check-self-bootstrap.yml retention: keep for accident-catching + trivially green under @main. Or retire entirely (semantic collapses)? A.b keeps it.

Anchor

  • release-toolkit#456 body (revised 2026-07-07 post-Surveyor-8f3d class-scope framing)
  • release-toolkit#448 comment 80859 (Class 1/2/3 framing + sequencing dependency)
  • Bosun da6f fold-forward: preserve drift-detection for adopters, close BOTH classes, adopter behavior unchanged, verification-AC = reliably-green multi-cycle
## Design memo — plan-first pass (QM, 2026-07-07) # release-toolkit#456 — Design memo (plan-first) ## Substrate map (verified callsites) **Toolkit-self-only surfaces**: - `.forgejo/workflows/release.yml` — wrapper. Currently `uses:@v0.29.0` (should auto-advance to v0.30.0 post-cut). **THE lever.** - `.forgejo/workflows/check-self-bootstrap.yml` — toolkit-self-only workflow (explicit in its header lines 19-21). Adopters don't ship this file. - `.forgejo/workflows/reusable-*.yml` (×5) — each carries `BUILD_BAKED_TOOLKIT_REF: 'main'` on main's HEAD. - `scripts/check-self-bootstrap.sh` — drift-detection logic. Two existing skips (PREP_SUBJECT_RE + startsWith('release-prep/')). - `scripts/repin.sh` — manual rc-tag creation + wrapper-bump + PR (#172 A-script). - `scripts/release-prep.sh` — bakes vCUT into reusable files at cut-prep. - `scripts/lib/build_bake.sh` — bake_toolkit_ref helper (`create_detached_bake_commit`). - `scripts/lib/changelog.sh` — carries `changelog_scaffold_missing_sections` (Class 3 payload). - `reusable-release.yml` cut path — **auto-re-pin (#283)** at lines 596-609 (path-α) and 661-673 (path-γ) sed-rewrite `uses:@<ref>` to CUT_TAG. **Adopter surface**: - Has `.forgejo/workflows/release.yml` with `uses:@vX.Y.Z` (frozen tag). - Does NOT have `check-self-bootstrap.yml` or `scripts/`. - Drift-detection = `uses:@<tag>` frozen-pin discipline (fully preserved, orthogonal to check-self-bootstrap). ## The gap in one sentence Toolkit-self's own `.forgejo/workflows/release.yml` pins `uses:@<last-cut-tag>`, but the code being edited between cuts sits on `main` — so every substrate-touching PR creates a mid-cycle divergence between `scripts/@main` and `scripts/@<pinned-tag>`. This manifests as Class 1 (check-self-bootstrap RED on every substrate PR) and Class 3 (compose-scaffold missing when cut runs from pinned tag). ## Design options analysis ### Option A.a — Auto-repin on every scripts-touching push:main (fires the current mechanism more aggressively) - On push:main, if check-self-bootstrap would red, auto-fire repin.sh-equivalent (fresh rc-tag + wrapper bump). - Closes both classes: post-auto-repin, both `lib/changelog.sh` and workflow files are pin-current until next scripts PR. - Adopter behavior: unchanged (they never had this trigger). - **Concerns**: rc-tag accumulation between cuts (prune-rc-tags at cut handles). Trades review-time gate on the repin (currently a PR with reviewer request). Loops possible on subsequent push:main triggered by auto-repin unless carefully skipped. ### Option A.b — Toolkit-self floats on `@main`; adopters continue pinning tags **(recommended)** - Change toolkit-self `.forgejo/workflows/release.yml` line 56 from `uses:@v0.29.0` → `uses:@main`. - The baked-ref mechanism already targets 'main' on main's HEAD (BUILD_BAKED_TOOLKIT_REF: 'main' at reusable-release.yml:139). Cut-prep still bakes vCUT into the cut commit so the cut TAG carries frozen substrate (adopter-facing artifact unchanged). - Post-cut bake-reset (reusable-release.yml:456-462) already resets main's HEAD marker back to 'main'. - **Auto-re-pin (#283) at lines 596-609 / 661-673** gets a skip: don't rewrite when the current `uses:@<ref>` value is already `main`. Two-line conditional inside the sed loop. - **check-self-bootstrap.sh** trivially passes under `uses:@main` (main vs main = identical). Keep the workflow for accident-catching (someone hand-edits back to `@vX.Y.Z` and forgets to re-pin). - **repin.sh** kept for adopter emergency use; header-doc updated to note toolkit-self no longer needs it as routine mid-cycle mechanism. **Class coverage**: - **Class 1 (check-self-bootstrap frequent)**: closes — main = main, no drift possible mid-cycle. - **Class 3 (compose-scaffold rare)**: closes — release-prep runs from `scripts@main`, always carries the current scaffold + all lib helpers. - **Adopter drift-detection**: preserved — adopters pin `@vX.Y.Z`, cut-tag carries baked ref, `uses:@<tag>` remains a frozen source-of-record for external consumers. - **Accident-catching**: preserved — check-self-bootstrap still fires; still catches hand-edits back to a versioned pin that then diverges from main. **Class-complete** per Surveyor 8f3d constraint. ### Option A.c — Symbolic pin-mode switch based on event - resolve-ref step in reusable-release.yml conditionally uses `main` vs baked value based on `github.event.repository.name == 'release-toolkit'`. - More invasive (touches the reusable, which is the adopter-facing entry-point); repo-name coupling is a smell. - Closes both classes but at higher blast-radius than A.b. - **Rejected** — A.b is a simpler substrate change with cleaner separation-of-concerns. ## Recommendation: Option A.b **Why A.b over A.a**: - A.a keeps generating rc-tags (churn artifacts) and requires new event-handling logic (loop prevention, repin-triggered-by-repin skip patterns). - A.b is a **single YAML edit** (`@v0.30.0` → `@main` in one file) + a **two-line conditional** in the auto-re-pin block + tests + doc updates. - A.b intentionally encodes the "toolkit-self ↔ adopter-facing" distinction the tracker names: adopters pin, toolkit-self floats. That IS the semantic Surveyor 8f3d asked for. - A.b passes the "adopter behavior unchanged" test trivially — the adopter path never touches toolkit-self's wrapper. **Why A.b preserves drift-detection semantics**: - check-self-bootstrap.yml keeps running; it just always greens for toolkit-self under `@main`. - If someone accidentally pins toolkit-self wrapper back to a versioned tag (hand-edit; bad auto-re-pin) and then scripts churn, the check reds — semantic preserved for the accident case. - Adopter `uses:@<tag>` frozen-pin discipline is entirely orthogonal to this check (adopters don't run this workflow). ## Implementation shape 1. **`.forgejo/workflows/release.yml`**: `uses:@v0.29.0` → `uses:@main` (single-line edit). 2. **`.forgejo/workflows/reusable-release.yml`** auto-re-pin blocks (lines ~604 path-α, ~669 path-γ): add pre-sed check — if current `uses:@<ref>` value is already `main`, skip the sed rewrite for that file. Preserves auto-re-pin for external forks / paths where wrapper does pin a tag. ```bash # Inside the while-read wrapper loop: if [[ -f "$f" ]]; then CURRENT_REF=$(grep -oE '^\s*uses:.*reusable-release\.yml@[^\s]+' "$f" | head -1 | sed 's/.*@//') if [[ "$CURRENT_REF" == 'main' ]]; then echo "skip re-pin: $f already at @main (release-toolkit#456 floating-pin)" else sed -i -E "s|(uses:[[:space:]]+frankenbit/release-toolkit/\.forgejo/workflows/reusable-[A-Za-z0-9-]+\.yml@)[^[:space:]]+|\1${CUT_TAG}|" "$f" git add "$f" fi fi ``` 3. **`scripts/repin.sh`** header-doc update: note that toolkit-self no longer needs routine repin under the #456 floating-pin scheme; keep the script for adopter emergency use. 4. **`scripts/check-self-bootstrap.sh`** header-doc update: note the `uses:@main` trivial-pass case + the accident-catching semantic that remains. 5. **Fragment**: `changelog.d/456.fixed.md` explaining the fix + the substrate-honest semantic. 6. **Bats coverage**: - `tests/check-self-bootstrap.bats`: new test for the `@main` wrapper case (currently only tests tag-pin scenarios). - `tests/workflow.bats` or `tests/repin.bats`: coverage for the auto-re-pin `@main` skip case (mutation-verification). 7. **BookStack docs**: no ripples — the adopter-facing surface is unchanged. ## Verification AC (per tracker + Surveyor 8f3d) - ✅ Toolkit-self can merge scripts-touching PRs mid-cycle **without** manual rc-tag intervention. - ✅ Adopter behavior unchanged (adopter pins remain @tag; cut-tag artifacts remain frozen). - ✅ **BOTH #448 Class 1 AND Class 3 stay green** through substrate-work sprint + cut cycle without manual intervention. - ✅ No red-then-manual-repin cycle observed on next Bundle-arc-scale substrate work. - ✅ Reliably-green CI observed continuously on toolkit-self main across scripts churn + cut cycle. ## Blast radius + implementation size - **Blast radius**: confined to toolkit-self's own workflow files + one reusable-release.yml conditional. No adopter-facing interface changes. - **Backwards compat**: adopters currently pinned to old tags continue to work exactly as before. No breaking change for external consumers. - **Size**: S/M shape. Single YAML edit + one bash conditional + 2-4 bats fixtures + fragment + doc-refresh. QM-implementable in a focused sprint. ## Implementation-routing recommendation **QM stays** — this is substrate-adjacent to my recent #454 (compose-scaffold) work, my context is warm, and the shape is well-bounded. Estimated single-PR sprint (S/M). **Engineer routes if** — design review surfaces adopter-substrate ripple I didn't catch (unlikely per the analysis above; toolkit-self ↔ adopter path separation is architectural, not implementation-detail). ## Open questions for Bosun/operator ratification 1. **Design intent alignment**: A.b intentionally diverges toolkit-self from the adopter path (floating vs pinned). Ratified as the design direction? 2. **Auto-re-pin @main skip**: preferred approach vs teaching auto-re-pin to skip toolkit-self entirely by other means (e.g., env flag)? A.b's approach (skip when value == 'main') keeps the semantic self-documenting. 3. **repin.sh retention**: keep as adopter emergency tool + doc-update, or retire entirely? A.b keeps it. 4. **check-self-bootstrap.yml retention**: keep for accident-catching + trivially green under @main. Or retire entirely (semantic collapses)? A.b keeps it. ## Anchor - release-toolkit#456 body (revised 2026-07-07 post-Surveyor-8f3d class-scope framing) - release-toolkit#448 comment 80859 (Class 1/2/3 framing + sequencing dependency) - Bosun da6f fold-forward: preserve drift-detection for adopters, close BOTH classes, adopter behavior unchanged, verification-AC = reliably-green multi-cycle
bosun closed this issue 2026-07-07 15:07:53 +02:00

Reference-implementation moment — merged 2026-07-07 15:07:53

PR #457 landed the A.b floating-pin substrate on main @ e9eca94a. Stamping the reference-implementation property as substrate-of-record:

The fix proved itself on its own PR branch. check-self-bootstrap.yml invokes ./scripts/check-self-bootstrap.sh directly from the checkout — not through a pinned reusable — so the #456 PINNED_REF == 'main' early-exit was live IMMEDIATELY on the PR that introduced it. Reliably-green CI on the PR that introduces reliably-green CI. Zero pre-merge rc-pin dance. The fix did not sit behind the gate it removed.

Empirical evidence sequenced from here:

  • First post-merge steady-state observation (Bosun 27eb, 15:07:53): check-self-bootstrap GREEN on main @ e9eca94a under the new @main wrapper substrate. Post-(A) evidence set for #448 begins here.
  • Multi-cycle + multi-cut verification continues on #448 per operator ratification (single green cut ≠ steady-state; closure by default at v1.0.0).

Turnaround: operator ratification 12:36 → PR opened 12:59 → APPROVED 13:07 → MERGED 15:07:53.

Sequencing acknowledged: PR#457 is the class-closer, not the milestone-closer. #448 holds open as the empirical gate-record.

## Reference-implementation moment — merged 2026-07-07 15:07:53 PR [#457](https://git.frankenbit.de/frankenbit/release-toolkit/pulls/457) landed the A.b floating-pin substrate on `main` @ `e9eca94a`. Stamping the reference-implementation property as substrate-of-record: **The fix proved itself on its own PR branch.** `check-self-bootstrap.yml` invokes `./scripts/check-self-bootstrap.sh` directly from the checkout — not through a pinned reusable — so the `#456` `PINNED_REF == 'main'` early-exit was live IMMEDIATELY on the PR that introduced it. Reliably-green CI on the PR that introduces reliably-green CI. Zero pre-merge rc-pin dance. The fix did not sit behind the gate it removed. Empirical evidence sequenced from here: - **First post-merge steady-state observation** (Bosun `27eb`, 15:07:53): `check-self-bootstrap` GREEN on `main` @ `e9eca94a` under the new `@main` wrapper substrate. Post-(A) evidence set for [#448](https://git.frankenbit.de/frankenbit/release-toolkit/issues/448) begins here. - Multi-cycle + multi-cut verification continues on [#448](https://git.frankenbit.de/frankenbit/release-toolkit/issues/448) per operator ratification (single green cut ≠ steady-state; closure by default at v1.0.0). Turnaround: operator ratification 12:36 → PR opened 12:59 → APPROVED 13:07 → MERGED 15:07:53. **Sequencing acknowledged**: PR#457 is the class-closer, not the milestone-closer. [#448](https://git.frankenbit.de/frankenbit/release-toolkit/issues/448) holds open as the empirical gate-record.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#456
No description provided.