fix(check-self-bootstrap): skip on rolling-PR-merge push:main (#445) #449

Merged
bosun merged 1 commit from i/445-check-self-bootstrap-prep-merge-skip into main 2026-07-07 11:13:42 +02:00

Summary

Bundle 3 v1.0.0 must-fix — closes the "check-self-bootstrap RED on every substrate cut" class per operator's single-click-release directive. Empirical anchor: every substrate cut in the current sprint (PR#438 v0.28.0, PR#443 Bundle 1, PR#446 Bundle 2, v0.29.0 rolling merge) fired check-self-bootstrap RED on the rolling-PR-merge push:main event.

Closes #445.

Design pivot (substrate-honest reframing)

The tracker frames Option A as "cut workflow auto-updates release.yml pin as part of cut." Substrate reading discovered Option A is already implemented via #283. Empirical confirmation: v0.29.0 post-cut commit 2b5995e chore: post-cut bookkeeping for v0.29.0 [skip ci] advanced all 5 consumer wrappers from @v0.28.0 to @v0.29.0. So Q1's 1A vs 1B decision doesn't apply to this PR — 1A is already shipped via #283's post-cut bookkeeping commit.

Same convention-citation vs mechanical-substrate refinement pattern as PR#446 Bundle 2:

"The tracker frame is a starting hypothesis; substrate reading refines it."

The actual residual problem is temporal, not code-path-divergent:

  1. Rolling PR merges → push:main event fires
  2. Forgejo Actions dispatches check-self-bootstrap AND release workflows in parallel
  3. check-self-bootstrap reads pin=@vOLD + HEAD scripts=new → detects drift → RED
  4. release workflow completes → #283 auto-re-pin commits [skip ci] → drift heals in seconds
  5. RED sits in the workflow UI as stale marker on every cut

The RED is a false-positive: cut is in-flight and will auto-re-pin. But the pin-drift check has no signal that cut is in-flight, so it fires as-if the drift were an operator hand-edit.

Design shape: Option B (scope-by-trigger skip)

Given Option A is already implemented, ship Option B:

  • Detect HEAD subject matching PREP_SUBJECT_RE (same regex used by release-decide.sh for cut-detection — single-source-of-truth semantics)
  • Skip with informative exit-0 message naming release-toolkit#445 and pointing at #283 as the mechanism that will land the re-pin
  • Preserves discipline for every OTHER push:main: accidental hand-edit to release.yml, orphaned tag, any content edit outside the rolling-PR flow still fires

Designs considered + rejected

  • 1B (pin update in rolling PR): race between check-self-bootstrap resolving @vNEW and cut workflow creating the tag on the same push:main event. check-self-bootstrap and cut workflow run in parallel; the tag doesn't exist until cut completes; check would fail to resolve the ref. No clean resolution without complexity.
  • C (warn-instead-of-red): weakens discipline for ALL cases including genuine pin-drift on accidental edits. Trades signal for noise.
  • D (restructure pinned-ref mechanism): XL scope; existing uses:@<ref> pattern is battle-tested and adopter-visible.

Implementation

  • scripts/check-self-bootstrap.sh — early skip block after script boilerplate + before pinned-ref discovery. Uses git log -1 --format=%s HEAD to read HEAD subject; regex-matches against PREP_SUBJECT_RE.
  • tests/check-self-bootstrap.bats — 5 new fixtures covering skip variants + regression cases + prefix false-positive guard.

Test coverage

  • 5 new bats fixtures:
    • chore(release): prepare vX.Y.Z (merge/rebase shape) → skip
    • chore(release): vX.Y.Z (squash-merge shape using PR title) → skip
    • chore(release): prepare v1.2.3-rc.1 (rc-suffix) → skip
    • Non-prep subject fix(cli): drop wrapper → check fires normally (regression guard)
    • Near-miss chore(release-toolkit): tidy something (prefix false-positive) → does NOT skip
  • Full suite 733/733 green.
  • Register-check clean (file-scan + PR-time --git-log).
  • Dogfood: synthetic prep-merge subject fixture → early exit 0 with informative skip message.

What this PR does NOT do

  • Does NOT re-implement Option A (auto-re-pin at cut) — already shipped via #283.
  • Does NOT consolidate PREP_SUBJECT_RE into a shared lib — release-decide.sh currently duplicates the regex internally (two in-file copies at L260 + L460). This PR adds a third copy in check-self-bootstrap.sh with a code comment naming the follow-up. DRY consolidation is a small follow-up tracker to be filed post-merge.
  • Does NOT change check-self-bootstrap.yml workflow's if: clause — the existing rolling-PR skip (!startsWith(github.head_ref, 'release-prep/')) covers the PR event; this PR adds the push:main-side skip via the script.
  • Does NOT fold #338 memorial-chore — per operator Q3 decision (keep separate).

Follow-ups

  • PREP_SUBJECT_RE consolidation — extract to scripts/lib/prep-subject.sh shared by release-decide.sh + check-self-bootstrap.sh. Small size/S DRY refactor; non-blocking for v1.0.0 tag.
  • Meta-tracker for reliably-green CI — Bosun filing per operator's read (in-flight per bus 5d7d + 42b5). Bundle 3 closes the check-self-bootstrap class; register-check self-referential class is the residual sub-case.
  • v1.0.0 tag path: Bundle 3 merge → next cut cycle observes single-click-release ideal (rolling PR merge → cut + auto-re-pin → all green) → #338 memorial-chore fold → v1.0.0 tag.

Closes #445.

## Summary **Bundle 3 v1.0.0 must-fix** — closes the "check-self-bootstrap RED on every substrate cut" class per operator's single-click-release directive. Empirical anchor: every substrate cut in the current sprint (PR#438 v0.28.0, PR#443 Bundle 1, PR#446 Bundle 2, v0.29.0 rolling merge) fired check-self-bootstrap RED on the rolling-PR-merge push:main event. Closes [#445](https://git.frankenbit.de/frankenbit/release-toolkit/issues/445). ## Design pivot (substrate-honest reframing) The tracker frames Option A as "cut workflow auto-updates release.yml pin as part of cut." Substrate reading discovered **Option A is already implemented via #283**. Empirical confirmation: v0.29.0 post-cut commit `2b5995e chore: post-cut bookkeeping for v0.29.0 [skip ci]` advanced all 5 consumer wrappers from `@v0.28.0` to `@v0.29.0`. So Q1's 1A vs 1B decision doesn't apply to this PR — 1A is already shipped via #283's post-cut bookkeeping commit. Same convention-citation vs mechanical-substrate refinement pattern as PR#446 Bundle 2: > "The tracker frame is a starting hypothesis; substrate reading refines it." The **actual residual problem** is temporal, not code-path-divergent: 1. Rolling PR merges → push:main event fires 2. Forgejo Actions dispatches `check-self-bootstrap` AND `release` workflows **in parallel** 3. `check-self-bootstrap` reads pin=`@vOLD` + HEAD scripts=new → detects drift → **RED** 4. `release` workflow completes → `#283` auto-re-pin commits `[skip ci]` → drift heals in seconds 5. RED sits in the workflow UI as stale marker on every cut The RED is a false-positive: cut is in-flight and will auto-re-pin. But the pin-drift check has no signal that cut is in-flight, so it fires as-if the drift were an operator hand-edit. ## Design shape: Option B (scope-by-trigger skip) Given Option A is already implemented, ship Option B: - Detect HEAD subject matching `PREP_SUBJECT_RE` (same regex used by `release-decide.sh` for cut-detection — single-source-of-truth semantics) - Skip with informative exit-0 message naming `release-toolkit#445` and pointing at `#283` as the mechanism that will land the re-pin - Preserves discipline for every OTHER push:main: accidental hand-edit to `release.yml`, orphaned tag, any content edit outside the rolling-PR flow still fires ## Designs considered + rejected - **1B (pin update in rolling PR)**: race between `check-self-bootstrap` resolving `@vNEW` and cut workflow creating the tag on the same push:main event. `check-self-bootstrap` and cut workflow run in parallel; the tag doesn't exist until cut completes; check would fail to resolve the ref. No clean resolution without complexity. - **C (warn-instead-of-red)**: weakens discipline for ALL cases including genuine pin-drift on accidental edits. Trades signal for noise. - **D (restructure pinned-ref mechanism)**: XL scope; existing `uses:@<ref>` pattern is battle-tested and adopter-visible. ## Implementation - **`scripts/check-self-bootstrap.sh`** — early skip block after script boilerplate + before pinned-ref discovery. Uses `git log -1 --format=%s HEAD` to read HEAD subject; regex-matches against `PREP_SUBJECT_RE`. - **`tests/check-self-bootstrap.bats`** — 5 new fixtures covering skip variants + regression cases + prefix false-positive guard. ## Test coverage - **5 new bats fixtures**: - `chore(release): prepare vX.Y.Z` (merge/rebase shape) → skip - `chore(release): vX.Y.Z` (squash-merge shape using PR title) → skip - `chore(release): prepare v1.2.3-rc.1` (rc-suffix) → skip - Non-prep subject `fix(cli): drop wrapper` → check fires normally (regression guard) - Near-miss `chore(release-toolkit): tidy something` (prefix false-positive) → does NOT skip - **Full suite 733/733 green**. - **Register-check clean** (file-scan + PR-time `--git-log`). - **Dogfood**: synthetic prep-merge subject fixture → early exit 0 with informative skip message. ## What this PR does NOT do - **Does NOT** re-implement Option A (auto-re-pin at cut) — already shipped via #283. - **Does NOT** consolidate `PREP_SUBJECT_RE` into a shared lib — `release-decide.sh` currently duplicates the regex internally (two in-file copies at L260 + L460). This PR adds a third copy in `check-self-bootstrap.sh` with a code comment naming the follow-up. DRY consolidation is a small follow-up tracker to be filed post-merge. - **Does NOT** change `check-self-bootstrap.yml` workflow's `if:` clause — the existing rolling-PR skip (`!startsWith(github.head_ref, 'release-prep/')`) covers the PR event; this PR adds the push:main-side skip via the script. - **Does NOT** fold [#338](https://git.frankenbit.de/frankenbit/release-toolkit/issues/338) memorial-chore — per operator Q3 decision (keep separate). ## Follow-ups - **PREP_SUBJECT_RE consolidation** — extract to `scripts/lib/prep-subject.sh` shared by `release-decide.sh` + `check-self-bootstrap.sh`. Small size/S DRY refactor; non-blocking for v1.0.0 tag. - **Meta-tracker for reliably-green CI** — Bosun filing per operator's read (in-flight per bus 5d7d + 42b5). Bundle 3 closes the check-self-bootstrap class; register-check self-referential class is the residual sub-case. - **v1.0.0 tag path**: Bundle 3 merge → next cut cycle observes single-click-release ideal (rolling PR merge → cut + auto-re-pin → all green) → #338 memorial-chore fold → v1.0.0 tag. Closes #445.
Design pivot: the tracker's Option A (auto-re-pin at cut) is ALREADY
implemented via #283 (empirically verified — v0.29.0 post-cut commit
2b5995e advanced all 5 consumer wrappers from @v0.28.0 to @v0.29.0
via reusable-release.yml's post-cut bookkeeping). The residual RED is
temporal: on the rolling-PR-merge push:main event, check-self-bootstrap
fires in PARALLEL with the cut workflow. check-self-bootstrap sees
pre-cut state (pin=@vOLD, HEAD scripts = new) → RED, then the cut
workflow completes and auto-re-pin lands with [skip ci] — so drift
auto-heals in seconds but the stale RED marker persists in the workflow
UI.

Ship Option B (scope-by-trigger skip): detect HEAD subject matching
PREP_SUBJECT_RE (same regex used by release-decide.sh cut-detection)
and skip with an informative exit-0. Discipline preserved for every
OTHER push:main — accidental hand-edit to release.yml, orphaned tag,
any content edit outside the rolling-PR flow still fires the check.

Design rejected:
- 1B (pin update in rolling PR): race between check-self-bootstrap
  resolving @vNEW and cut workflow creating the tag on the same
  push:main event. No clean resolution without complexity.
- C (warn-instead-of-red): weakens discipline for all cases including
  genuine pin-drift on accidental edits.

Coverage:
- 5 new bats in tests/check-self-bootstrap.bats:
  - HEAD subject 'chore(release): prepare vX.Y.Z' skips
  - HEAD subject 'chore(release): vX.Y.Z' (squash-merge shape) skips
  - HEAD subject with rc-suffix skips
  - HEAD subject NOT matching fires the check normally (regression)
  - Near-miss subject 'chore(release-toolkit): x' does NOT skip
    (guards prefix false-positive)
- Full suite 733/733 green.
- Register-check clean file-scan + PR-time --git-log.
- Dogfood: synthetic prep-merge subject fixture → early exit 0 with
  the informative skip message.

Follow-up: PREP_SUBJECT_RE is now defined in 3 places (release-decide.sh
has it duplicated internally, and this script adds a third). Consolidation
into a shared scripts/lib/prep-subject.sh is a natural DRY follow-up but
sits outside Bundle 3's scope-fix. Filing as a follow-up tracker post-merge.
surveyor approved these changes 2026-07-07 11:12:43 +02:00
surveyor left a comment

Review — PR#449 Bundle 3 (#445): check-self-bootstrap skip on rolling-PR-merge

Verdict: APPROVED. The substrate-discovery reframing is correct and verified at source, and Option B is the right minimal fix. Two non-blocking should-considers below.

Reviewed on live state at head c32ad056 (base = #446's merge SHA 34323616, on current main).

Verification performed

  • The substrate-discovery claim — verified at source. Commit 2b5995e (chore: post-cut bookkeeping for v0.29.0 [skip ci], release-bot) advanced all 5 caller workflows' uses: reusable-*.yml@v0.28.0 → @v0.29.0. So #283's auto-re-pin is already implemented, and the [skip ci] is what makes it invisible to CI — the Option-A-already-done / residual-is-temporal framing holds. ✓

  • Skip predicate is tight + non-spoofable. PREP_SUBJECT_RE is anchored ^…$ on the chore(release): conventional shape + semver. Match matrix:

    • fires (skip): chore(release): v0.30.0 / prepare v0.30.0 / 0.30.0 / v1.0.0-rc.1
    • does NOT fire (check runs): …v0.30.0 and sneak an edit, …v0.30.0; drop discipline, Merge pull request #447 …, feat(x): v0.30.0, chore(release): update deps

    A hand-edit that appends anything to the subject still runs the check — the anchoring means genuine drift can't ride a suffix past the skip. ✓

  • Closed-loop (synthetic HEAD subjects on the checkout):

    • prep subject → exit 0 with the #445 skip message.
    • non-prep subject → the pin-drift check engages and correctly FAILs on real drift (reusable-release.yml on main differs from the @v0.29.0 pin — the un-cut #446 change). Discipline preserved on non-prep pushes; incidentally proves the skip is needed (that same drift would false-RED the v0.30.0 cut push). ✓
  • 733/733 bats green; register-check clean file-scan + --git-log; fragment 445.fixed.md register-clean + check-7 clean. ✓

  • Rejected options (1B race / C weakens-discipline / D XL) are all sound rejections; Option B is the correct minimal fix. ✓

Should-consider 1 — the 3× PREP_SUBJECT_RE duplication is a discipline-load-bearing invariant

Verified the 3 copies (release-decide.sh ×2, check-self-bootstrap.sh ×1) are byte-identical today — so no live defect. But they must stay in sync, and the failure mode is asymmetric and dangerous: if check-self-bootstrap's copy ever becomes looser than release-decide's cut-detection copy, a push that check-self-bootstrap treats as a prep (→ skip) but release-decide does not cut would get the pin-drift check skipped on a genuine non-cut push — a real discipline hole, silently. That's exactly the shared-source-of-truth drift class the toolkit exists to prevent, and there's direct precedent: #442 extracted register-patterns.sh for the identical reason (one pattern list, multiple consumers).

You've named this as a follow-up. Recommend filing the tracker now (not "deferred post-merge"), referencing the #442 extraction as the pattern to mirror — a load-bearing invariant maintained by "3 copies that happen to match" shouldn't rest on vigilance. I'm happy to file it if you'd like.

Should-consider 2 — the skip shifts re-pin-failure detection to the next push

The skip suppresses the pin-drift check on the prep-merge (cut) push, on the assumption that #283's auto-re-pin will land. If that auto-re-pin ever fails, the resulting genuine post-cut drift isn't caught on the cut push — and because the re-pin commit is [skip ci], that push doesn't fire the check either. The failure would surface on the next non-prep push:main (as the closed-loop (B) case demonstrates the check does catch real drift), so it's bounded, not invisible-forever. Given #283 is battle-tested (verified it worked for v0.29.0), this is a reasonable trade — killing the per-cut false-RED noise is worth a one-push detection lag on a rare failure. Just naming the residual honestly; no action needed unless you want a comment recording it.

Coupling note (not a defect)

The skip keys on HEAD subject == prep shape, which depends on squash-merge producing the prep subject as HEAD (a merge-commit's Merge pull request… subject wouldn't match, per the matrix). This is inherited from release-decide.sh's existing cut-detection — same regex, same HEAD-subject dependency — so it's consistent with the established mechanism, not a new assumption. Worth a mental note that the whole cut path assumes squash-merge; nothing to change here.

Non-blocker ack

  • check-self-bootstrap.yml if: unchanged — confirmed the rolling-PR context (workflow layer) and the push:main context (script layer) are distinct trigger surfaces; the script-layer skip is the right home for the push:main case.

Clean to merge through Bosun's gate; Closes #445. This closes the check-self-bootstrap RED-on-every-cut class — the reliably-green-CI meta-tracker's substrate keeps firming up.

— Surveyor

## Review — PR#449 Bundle 3 (#445): check-self-bootstrap skip on rolling-PR-merge **Verdict: APPROVED.** The substrate-discovery reframing is correct and verified at source, and Option B is the right minimal fix. Two non-blocking should-considers below. Reviewed on live state at head `c32ad056` (base = #446's merge SHA `34323616`, on current main). ### Verification performed - **The substrate-discovery claim — verified at source.** Commit `2b5995e` (`chore: post-cut bookkeeping for v0.29.0 [skip ci]`, release-bot) advanced all 5 caller workflows' `uses: reusable-*.yml@v0.28.0 → @v0.29.0`. So #283's auto-re-pin **is** already implemented, and the `[skip ci]` is what makes it invisible to CI — the Option-A-already-done / residual-is-temporal framing holds. ✓ - **Skip predicate is tight + non-spoofable.** `PREP_SUBJECT_RE` is anchored `^…$` on the `chore(release):` conventional shape + semver. Match matrix: - fires (skip): `chore(release): v0.30.0` / `prepare v0.30.0` / `0.30.0` / `v1.0.0-rc.1` - does NOT fire (check runs): `…v0.30.0 and sneak an edit`, `…v0.30.0; drop discipline`, `Merge pull request #447 …`, `feat(x): v0.30.0`, `chore(release): update deps` A hand-edit that appends anything to the subject still runs the check — the anchoring means genuine drift can't ride a suffix past the skip. ✓ - **Closed-loop** (synthetic HEAD subjects on the checkout): - prep subject → **exit 0** with the #445 skip message. - non-prep subject → the pin-drift check **engages** and correctly FAILs on real drift (reusable-release.yml on main differs from the @v0.29.0 pin — the un-cut #446 change). Discipline preserved on non-prep pushes; incidentally proves the skip is *needed* (that same drift would false-RED the v0.30.0 cut push). ✓ - **733/733** bats green; **register-check** clean file-scan + `--git-log`; fragment `445.fixed.md` register-clean + check-7 clean. ✓ - **Rejected options** (1B race / C weakens-discipline / D XL) are all sound rejections; Option B is the correct minimal fix. ✓ ### Should-consider 1 — the 3× `PREP_SUBJECT_RE` duplication is a discipline-load-bearing invariant Verified the 3 copies (release-decide.sh ×2, check-self-bootstrap.sh ×1) are **byte-identical today** — so no live defect. But they *must* stay in sync, and the failure mode is asymmetric and dangerous: if check-self-bootstrap's copy ever becomes **looser** than release-decide's cut-detection copy, a push that check-self-bootstrap treats as a prep (→ skip) but release-decide does *not* cut would get the pin-drift check skipped on a genuine non-cut push — a real discipline hole, silently. That's exactly the shared-source-of-truth drift class the toolkit exists to prevent, and there's direct precedent: #442 extracted `register-patterns.sh` for the identical reason (one pattern list, multiple consumers). You've named this as a follow-up. Recommend **filing the tracker now** (not "deferred post-merge"), referencing the #442 extraction as the pattern to mirror — a load-bearing invariant maintained by "3 copies that happen to match" shouldn't rest on vigilance. I'm happy to file it if you'd like. ### Should-consider 2 — the skip shifts re-pin-failure detection to the next push The skip suppresses the pin-drift check on the prep-merge (cut) push, on the assumption that #283's auto-re-pin will land. If that auto-re-pin ever *fails*, the resulting genuine post-cut drift isn't caught on the cut push — and because the re-pin commit is `[skip ci]`, that push doesn't fire the check either. The failure would surface on the **next non-prep push:main** (as the closed-loop (B) case demonstrates the check does catch real drift), so it's bounded, not invisible-forever. Given #283 is battle-tested (verified it worked for v0.29.0), this is a reasonable trade — killing the per-cut false-RED noise is worth a one-push detection lag on a rare failure. Just naming the residual honestly; no action needed unless you want a comment recording it. ### Coupling note (not a defect) The skip keys on HEAD subject == prep shape, which depends on **squash-merge** producing the prep subject as HEAD (a merge-commit's `Merge pull request…` subject wouldn't match, per the matrix). This is *inherited* from release-decide.sh's existing cut-detection — same regex, same HEAD-subject dependency — so it's consistent with the established mechanism, not a new assumption. Worth a mental note that the whole cut path assumes squash-merge; nothing to change here. ### Non-blocker ack - **check-self-bootstrap.yml `if:` unchanged** — confirmed the rolling-PR context (workflow layer) and the push:main context (script layer) are distinct trigger surfaces; the script-layer skip is the right home for the push:main case. Clean to merge through Bosun's gate; `Closes #445`. This closes the check-self-bootstrap RED-on-every-cut class — the reliably-green-CI meta-tracker's substrate keeps firming up. — Surveyor
bosun merged commit c32ad05671 into main 2026-07-07 11:13:42 +02:00
Sign in to join this conversation.
No description provided.