fix(cut): push-during-cut cancellation recovery — concurrency + orphan-CHANGELOG fail-loud (#417) #418
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!418
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/417-cut-cancellation-recovery"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
v1.0.0 must-fix per operator directive 2026-07-05. Two-part fix for the push-during-cut cancellation class:
concurrency:block in the consumer wiring template (prevention) + orphan-CHANGELOG fail-loud inrelease-decide.sh(belt-and-suspenders detection). Closes #417.Empirical anchor
#382 tic-tac-toe demo repo v0.1.1 → v0.2.0 cycle 2026-07-05 evening. PR#3 (rolling for v0.1.1) merged → cut workflow queued → adopter pushed a feat commit before the runner picked up → Codeberg CI cancelled the cut → next workflow walked commits from the stale manifest anchor → v0.2.0 rolling PR's CHANGELOG duplicated the fix that was already in the never-tagged v0.1.1 section. Bosun manually recovered v0.1.1 tag+release post-hoc.
Two-part fix
Part 1 — Prevention (adopter-side, required for v1.0.0+)
docs/integration.md's consumer wiring template gains:Standard CI concurrency pattern — subsequent pushes queue behind the in-flight cut. Documented with rationale, empirical anchor, and why it's required (not optional). New "Cut-cancellation recovery (#417)" section explains both recovery paths.
Part 2 — Detection (toolkit-side, belt-and-suspenders)
scripts/release-decide.shgains an orphan-CHANGELOG check after manifest load, before the commit walk. IfCHANGELOG.md's top-most released section is AHEAD ofmanifest.last_released_version, that's the orphan signature. Fail-loud with actionable recovery:git tag vX.Y.Z <commit>+ push + create Forgejo release + edit manifest. Preserves the CHANGELOG entry as-is.[Unreleased]+ commit. Next cut re-emits it fresh.The message also names the
concurrency:block as prevention with a link to the docs section.Why fail-loud, not auto-heal
Auto-heal is out of scope for v1.0.0. Either recovery path is a scope decision the adopter has to make (was the version intended? does the whole cycle need to reset?), and silent-heal in the toolkit would hide the underlying setup gap. Same shape as
#56manifest-desync fail-loud +#380manifest-vs-tag divergence fail-loud.Test coverage
tests/orphan-changelog.bats— 5 tests:concurrency:blockFull sweep: 660/660 EXIT=0 (was 655 pre-#417 + 5 new tests, grep-c pattern per
feedback_bats_sweep_count_verification). Local register-check clean at HEAD perfeedback_verify_after_mutation.Verification AC (from tracker)
concurrency:block on consumer's release.yml (documented as required)Files
scripts/release-decide.sh(orphan check + changelog.sh source),docs/integration.md(consumer wiring template + Cut-cancellation recovery section)tests/orphan-changelog.bats(5 regression tests),changelog.d/417.fixed.mdOption B deferral
Option B (cut-branch fork) is architectural rework — deferred to v1.1.0+ per Bosun's dispatch. Options A + C ship v1.0.0-quality reliability without the ephemeral-branch complexity.
Refs #417 (main), #382 (empirical), #56 (sibling manifest-desync class), #128 (sibling partial-completion), #380 (sibling manifest-vs-tag precedence).
APPROVED — #418 (#417 push-during-cut cancellation recovery)
Reviewed at head
23e7c25, 0 behind main. All three depth-review checks verified at source on live state; the mid-review fixup (23e7c25, "scope orphan-skip to VIRTUAL bootstrap only") resolves the one gap I found.(A) semver vs string compare — CORRECT.
cmp=$(semver_compare "$CHANGELOG_TOP_VERSION" "$LAST_VERSION")with[[ "$cmp" == "1" ]](release-decide.sh:274). Proper numeric ordering — the classic0.10 < 0.9string bug is avoided.(B) bootstrap-skip masking a genuine orphan — WAS REAL, now closed. The first draft's
[[ -z "$BOOTSTRAPPED" ]]skipped both bootstrap paths. That's too broad: on the stable-tag bootstrap path (manifest absent BUT a stable tag exists →LAST_VERSIONderived from the tag, release-decide.sh:243) there is a meaningful anchor, and an orphan can hide there (adopter with av0.4.0tag, mid-cut cancellation onv0.5.0, fresh toolkit adoption with no manifest yet). The fixup re-keys the skip to[[ -z "$BOOTSTRAP_VIRTUAL" ]], so only the virtual-0.0.0-anchor path (no manifest AND no stable tag, #368) skips — where fail-loud would otherwise false-positive on any hand-authored history and break the fresh-repo "Just Works" promise. Verified sound underset -euo pipefail:BOOTSTRAP_VIRTUALis initialized to""(:125) and only set to1on the virtual-anchor branch (:238), so the bare reference is always bound. New test#417 orphan detected: STABLE-tag bootstrap with mid-cut cancellationis mutation-meaningful — it constructs the exactv0.4.0-tag + CHANGELOG-v0.5.0+ no-manifest state and assertsstatus != 0plus both versions in the output; the renamed virtual-anchor test asserts the skip viavirtual-anchor bootstrapin output.(C) concurrency scope
github.ref— CORRECT. One refinement to the stated premise: the consumer template triggers onpush: branches:[main]andworkflow_dispatch(integration.md:312-323), not push alone. That doesn't weaken the choice — it strengthens it: aworkflow_dispatchonmainresolves to the samerefs/heads/maingroup, so a manual cut correctly serializes with an auto cut; a dispatch on another branch gets its own group and runs independently.github.repositorywould over-serialize (one repo-wide group for all refs).cancel-in-progress: falsequeues rather than cancels — the whole point. Complementary to the existing#139 paths-ignorebelt (skips the manifest commit so it doesn't fire a self-cancelling run) — the two are belt-and-suspenders, no conflict.Verification performed:
grep -c '^ok '/'^not ok ', not tail).register-check.shgreen at head (0 drift hits) — confirmed the pre-commit catch on the first-draft attribution was fully scrubbed from both release-decide.sh and the test title.[Unreleased]); changelog fragment + integration.md doc both accurate, name the #382 empirical anchor, and correctly scope auto-heal out of v1.0.0.Design call to fail-loud-not-auto-heal is right: either recovery is a scope decision only the adopter can make. Ship it.