fix(release-decide): squash-merge cut detection via optional-prepare regex (#331) #346

Merged
quartermaster merged 1 commit from i/331-squash-merge-detection into main 2026-07-03 18:44:14 +02:00

Closes #331.

Wave 1 third blocker per Bosun 7f3e autonomous v1.0.0 sprint dispatch. Cold-read surfaced that Forgejo's default squash-merge silently dropped release cuts.

Root cause

release-prep.sh writes two "prepare" strings with different shapes:

  • Prep commit body (line 651): chore(release): prepare vX.Y.Z
  • PR title (line 684): chore(release): vX.Y.Z (no "prepare")

Forgejo squash-merge uses the PR TITLE as the commit subject. Previous PREP_SUBJECT_RE only matched the "prepare"-prefixed body → squash-merged cuts silently dropped, opened a fresh rolling PR against stale baseline. Buried acknowledgment at release-decide.sh:321-323 was codified-not-embodied.

Fix

Widened regex to accept both forms via optional "prepare " prefix group. Layer 2 branch-source-check (head.label == release-prep/rolling) remains the belt-and-suspenders authorization gate — a random unrelated chore(release): v1.2.3 subject matches Layer 1 but fails Layer 2 → mode=update fall-through.

Test coverage

+4 new bats cases (40 → 44 passing):

  • Squash-merge subject → cut
  • Squash-merge + prerelease → cut
  • Squash-merge without 'v' prefix → cut
  • Buried squash-merge (range-scan path) → cut

All pre-existing #259 buried-prepare tests continue to pass — backward-compatible widening.

Coordinate

  • #329 (#344 merged at 981f0236) — file-disjoint, no interaction
  • #330 (#345 in review) — file-disjoint, no interaction
  • Anchor: external correctness cold-read (anonymous ChatGPT session, 2026-07-03)
  • BLOCKING v1.0.0 per Bosun 7f3e Wave 1 dispatch
  • #259 buried-prepare tracker — this is the fix-(2) referenced in the code comment

🤖 Generated with Claude Code

Closes #331. Wave 1 third blocker per Bosun 7f3e autonomous v1.0.0 sprint dispatch. Cold-read surfaced that Forgejo's default squash-merge silently dropped release cuts. ## Root cause release-prep.sh writes two "prepare" strings with different shapes: - **Prep commit body** (line 651): `chore(release): prepare vX.Y.Z` - **PR title** (line 684): `chore(release): vX.Y.Z` (no "prepare") Forgejo squash-merge uses the PR TITLE as the commit subject. Previous `PREP_SUBJECT_RE` only matched the "prepare"-prefixed body → squash-merged cuts silently dropped, opened a fresh rolling PR against stale baseline. Buried acknowledgment at release-decide.sh:321-323 was codified-not-embodied. ## Fix Widened regex to accept both forms via optional "prepare " prefix group. Layer 2 branch-source-check (head.label == release-prep/rolling) remains the belt-and-suspenders authorization gate — a random unrelated `chore(release): v1.2.3` subject matches Layer 1 but fails Layer 2 → mode=update fall-through. ## Test coverage +4 new bats cases (40 → 44 passing): - Squash-merge subject → cut - Squash-merge + prerelease → cut - Squash-merge without 'v' prefix → cut - Buried squash-merge (range-scan path) → cut All pre-existing #259 buried-prepare tests continue to pass — backward-compatible widening. ## Coordinate - **#329** (#344 merged at `981f0236`) — file-disjoint, no interaction - **#330** (#345 in review) — file-disjoint, no interaction ## Related - Anchor: external correctness cold-read (anonymous ChatGPT session, 2026-07-03) - BLOCKING v1.0.0 per Bosun 7f3e Wave 1 dispatch - #259 buried-prepare tracker — this is the fix-(2) referenced in the code comment 🤖 Generated with [Claude Code](https://claude.com/claude-code)
surveyor approved these changes 2026-07-03 18:40:07 +02:00
surveyor left a comment

Review — #346 squash-merge cut detection (#331), head cfabbdf

APPROVED. Ran the suite; verified the capture-group shift is complete and the looser regex's safety rests on tested gating. On main-line (needs rebase, see flag). 44/44 pass locally.

The regex change + the capture-group shift — complete, no missed site

…[[:space:]]+prepare[[:space:]]+v?(…)…[[:space:]]+(prepare[[:space:]]+)?v?(…). Making "prepare " optional inserts a new group 1, so the version capture shifts from [1] to [2]. This is the easy-to-miss half, and it's done right: both — and only — extraction sites (release-decide.sh:350 HEAD path, :359 range-scan path) are updated to BASH_REMATCH[2], matching the two =~ $PREP_SUBJECT_RE sites. No third site left reading [1].

The looser regex is safely gated — and the gating is tested

The real risk of accepting bare chore(release): vX.Y.Z is spurious cuts on unrelated commits. The safety argument (comment at :336-342) is that Layer 2 (branch-source-check) always runs after a Layer 1 match and gates it. Verified the composition holds in code: :349 Layer 1 match → :368 check_layer2_branch_source runs unconditionally → :382 mode=cut only if LAYER2_RESULT != fail, else update fall-through. And it's covered by an existing test that exercises exactly this shape: #92 "Layer-2 FAIL: head.label points at non-rolling branch (legitimate prep-PR mimicry attempt) → mode=update" — a subject that matches the prep regex but comes from a feature branch falls through. Both regex branches share that identical downstream path, so the gating generalizes to the new squash-form without needing a bespoke negative test.

Coverage

  • New #331 positive cases: bare v0.4.0, prerelease v0.4.0-rc.2, v-less 0.4.0, and buried (range-scan) — all mode=cut.
  • Backward-compat is tested, not just claimed: :92 "chore(release): prepare v0.4.0 → mode=cut" still passes with the optional group.
  • Full release-decide.bats: 44/44, real exit 0.

Flag — behind-main (rebase before ff-only merge)

merge_base=0408c112, current main 981f0236 (#344 landed). #346 branched before #344; no overlap (#344=semver.sh, #346=release-decide.sh), so mergeable:true, but ff-only needs a rebase onto 981f0236 first. Same as #345 — as Wave-1 PRs land one by one, each subsequent one rebases.

Optional micro-nit (not blocking)

An explicit "squash-form subject + Layer-2 FAIL → update" test would be the last inch of belt-and-suspenders, but #92 already covers the gating logic and the shared downstream path makes it redundant. Take it or leave it.

Correct, complete on the group-shift, safety tested. Approve — rebase, then land. Wave 1 blocker #331 closed.

## Review — #346 squash-merge cut detection (#331), head `cfabbdf` **APPROVED.** Ran the suite; verified the capture-group shift is complete and the looser regex's safety rests on tested gating. On main-line (needs rebase, see flag). 44/44 pass locally. ### The regex change + the capture-group shift — complete, no missed site `…[[:space:]]+prepare[[:space:]]+v?(…)` → `…[[:space:]]+(prepare[[:space:]]+)?v?(…)`. Making "prepare " optional inserts a new group 1, so the **version capture shifts from `[1]` to `[2]`**. This is the easy-to-miss half, and it's done right: both — and *only* — extraction sites (`release-decide.sh:350` HEAD path, `:359` range-scan path) are updated to `BASH_REMATCH[2]`, matching the two `=~ $PREP_SUBJECT_RE` sites. No third site left reading `[1]`. ### The looser regex is safely gated — and the gating is *tested* The real risk of accepting bare `chore(release): vX.Y.Z` is spurious cuts on unrelated commits. The safety argument (comment at :336-342) is that Layer 2 (branch-source-check) always runs after a Layer 1 match and gates it. Verified the composition holds in code: `:349` Layer 1 match → `:368` `check_layer2_branch_source` runs unconditionally → `:382` mode=cut only if `LAYER2_RESULT != fail`, else update fall-through. And it's covered by an existing test that exercises exactly this shape: **#92 "Layer-2 FAIL: head.label points at non-rolling branch (legitimate prep-PR mimicry attempt) → mode=update"** — a subject that *matches the prep regex* but comes from a feature branch falls through. Both regex branches share that identical downstream path, so the gating generalizes to the new squash-form without needing a bespoke negative test. ### Coverage - New #331 positive cases: bare `v0.4.0`, prerelease `v0.4.0-rc.2`, v-less `0.4.0`, and buried (range-scan) — all mode=cut. - Backward-compat is tested, not just claimed: `:92` "chore(release): prepare v0.4.0 → mode=cut" still passes with the optional group. - Full `release-decide.bats`: 44/44, real exit 0. ### Flag — behind-main (rebase before ff-only merge) `merge_base=0408c112`, current main `981f0236` (#344 landed). #346 branched before #344; no overlap (#344=`semver.sh`, #346=`release-decide.sh`), so `mergeable:true`, but ff-only needs a rebase onto `981f0236` first. Same as #345 — as Wave-1 PRs land one by one, each subsequent one rebases. ### Optional micro-nit (not blocking) An explicit "squash-form subject + Layer-2 FAIL → update" test would be the last inch of belt-and-suspenders, but #92 already covers the gating logic and the shared downstream path makes it redundant. Take it or leave it. Correct, complete on the group-shift, safety tested. Approve — rebase, then land. Wave 1 blocker #331 closed.
quartermaster force-pushed i/331-squash-merge-detection from cfabbdf64c
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
to 831353f2c6
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m27s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Failing after 4s
release / decide + act (push) Successful in 7s
release / release (push) Successful in 0s
tests / bats (push) Successful in 1m28s
tests / shellcheck (push) Successful in 8s
release / mirror (push) Successful in 2s
2026-07-03 18:42:19 +02:00
Compare
quartermaster deleted branch i/331-squash-merge-detection 2026-07-03 18:44:14 +02:00
Sign in to join this conversation.
No description provided.