feat(recovery): a verb for the (B) cut-recovery, so the one manual step is not a hand-edit #1130

Closed
opened 2026-09-04 15:27:51 +02:00 by bosun · 1 comment
Owner

Recovering from an interrupted cut still needs a hand-edit of the CHANGELOG. The one manual step sits in the middle of an otherwise mechanical path, and it is the step where it goes wrong.

What is missing

docs/integration.md § Cut-cancellation recovery (#417) says: "move the orphaned ## [vX.Y.Z] content back under ## [Unreleased] + commit". That is prose, executed by hand.

With #1128 landed, everything after that step is automatic — decide routes to update, prep re-composes, the cut fires. Only the first step is manual.

Why a verb

The visible change is deleting two lines, but which two decides whether the result composes. A sed one line off produces a plausible CHANGELOG that behaves differently, and the failure surfaces later as a cut that dies at compose-verify.

The parsing is already solved and already in the codebase — changelog.NewParser().SectionContent and prep's mergeUnreleased(unreleasedProse, combined). It is not exposed to the operator.

Precedent: close-stale-rolling-pr is already a recovery verb for a stuck release state.

Scope

Recovery (B) only. (A) publishes a release and involves judgement about which tree the version should name — that stays human.

AC

  • a verb moves the pending version's section content back under [Unreleased], preserving it byte-for-byte
  • it refuses when the state is not the one it repairs — no pending section, or a tag/release already exists for that version
  • --dry-run prints the diff without writing
  • after running it, rt decide returns mode=update on the same tree
  • arms for: the live state, a clean repo, a version already tagged, and an absent CHANGELOG

Test area

v0.57.0 is currently in exactly this state on main and the cut is being held for it. Run the verb against it and the recovery is the acceptance test, as it was for #1128.

Recovering from an interrupted cut still needs a hand-edit of the CHANGELOG. The one manual step sits in the middle of an otherwise mechanical path, and it is the step where it goes wrong. ## What is missing `docs/integration.md § Cut-cancellation recovery (#417)` says: "move the orphaned `## [vX.Y.Z]` content back under `## [Unreleased]` + commit". That is prose, executed by hand. With #1128 landed, everything after that step is automatic — decide routes to `update`, prep re-composes, the cut fires. Only the first step is manual. ## Why a verb The visible change is deleting two lines, but which two decides whether the result composes. A `sed` one line off produces a plausible CHANGELOG that behaves differently, and the failure surfaces later as a cut that dies at compose-verify. The parsing is already solved and already in the codebase — `changelog.NewParser().SectionContent` and prep's `mergeUnreleased(unreleasedProse, combined)`. It is not exposed to the operator. Precedent: `close-stale-rolling-pr` is already a recovery verb for a stuck release state. ## Scope Recovery **(B)** only. (A) publishes a release and involves judgement about which tree the version should name — that stays human. ## AC - [x] a verb moves the pending version's section content back under `[Unreleased]`, preserving it byte-for-byte - [x] it refuses when the state is not the one it repairs — no pending section, or a tag/release already exists for that version - [x] `--dry-run` prints the diff without writing - [x] after running it, `rt decide` returns `mode=update` on the same tree - [x] arms for: the live state, a clean repo, a version already tagged, and an absent CHANGELOG ## Test area `v0.57.0` is currently in exactly this state on main and the cut is being held for it. Run the verb against it and the recovery is the acceptance test, as it was for #1128.
Owner

ACs ticked — all five verified on main's live v0.57.0 state, twice: by me while building #1131, and independently by @bosun before merge.

byte-for-byte     apply output IDENTICAL to the hand recovery at 97a0bb32 (cmp -s)
refuses           second run rc=1 "not the state recovery (B) repairs";
                  tag/release-exists, ungraded-evidence and control arms in cmd/rt
--dry-run         rc=0, dirty=0 after — asserted on the FILE, not the message
decide after      mode=update next_version=0.57.0
arms              live · dry-run · refusals · absent CHANGELOG · already-recovered

Two bounds the tick does not carry, both stated in #1131:

  • byte-for-byte is the section BODY; the blank lines immediately around it are structure and normalise to one, and a non-empty [Unreleased] folds through MergeSections rather than concatenating.
  • the AC's "a version already tagged" arm is unit-driven from the #885 evidence fields, not from a live tagged repo.
ACs ticked — all five verified on main's live `v0.57.0` state, twice: by me while building #1131, and independently by @bosun before merge. ``` byte-for-byte apply output IDENTICAL to the hand recovery at 97a0bb32 (cmp -s) refuses second run rc=1 "not the state recovery (B) repairs"; tag/release-exists, ungraded-evidence and control arms in cmd/rt --dry-run rc=0, dirty=0 after — asserted on the FILE, not the message decide after mode=update next_version=0.57.0 arms live · dry-run · refusals · absent CHANGELOG · already-recovered ``` Two bounds the tick does not carry, both stated in #1131: - byte-for-byte is the section BODY; the blank lines immediately around it are structure and normalise to one, and a non-empty `[Unreleased]` folds through `MergeSections` rather than concatenating. - the AC's "a version already tagged" arm is unit-driven from the `#885` evidence fields, not from a live tagged repo.
bosun closed this issue 2026-09-04 15:49:25 +02:00
Sign in to join this conversation.
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#1130
No description provided.