docs(release): an interrupted cut has no documented recovery — the recipe lives in two closed trackers #1452

Closed
opened 2026-09-07 21:19:37 +02:00 by bosun · 1 comment
Owner

When a cut publishes its release and then fails before the manifest write, the only recovery is a hand-written commit, and the recipe exists only in two closed trackers.

Why it still matters after #1447

#1447 landed a bounded fetch-rebase-retry, so the concurrent-merge cause is fixed. It does not cover the other ways a cut can stop after rt release and before the bookkeeping — a runner dying, a credential expiring, a timeout. The residue is identical whichever cause produced it:

tag + release + assets   present
manifest                 stale
rt decide                mode=blocked reason=pending_cut on every push to main

And rt recover-pending-cut is the wrong tool by construction — it folds a RELEASED section back under [Unreleased], and prepared-uncut-check refuses to recommend it for exactly that reason.

What exists today

Two hand fixes, both in closed trackers: #1356 (v0.61.1) and #1444 (v0.62.4). The second records the field semantics a future fixer needs and would otherwise get wrong:

last_released_sha   the TRIGGERING commit, not the tag's target — v0.62.3's tag points
                    at an off-main `pin action image [skip ci]` commit while its manifest
                    records the prepare commit
last_released_at    when `rt post-cut` RAN — time.Now().UTC() at post_cut.go:582, NOT the
                    release object's published_at

AC

  • The recovery is written where someone facing a blocked rt decide will meet it -- prepared-uncut-check's own refusal text is the surface that reader is already looking at
  • The two field semantics above are stated, since both were got wrong once and one of them was got wrong IN a commit message that asserted the opposite
  • A decision is recorded either way: a verb, or the hand fix as the supported path -- not left implicit

Anchor

#1447 AC3, deferred here rather than ticked. @bosun performed both hand fixes; @carpenter wrote the retry that removes the common cause.

When a cut publishes its release and then fails before the manifest write, the only recovery is a hand-written commit, and the recipe exists only in two closed trackers. ## Why it still matters after `#1447` `#1447` landed a bounded fetch-rebase-retry, so the concurrent-merge cause is fixed. **It does not cover the other ways a cut can stop after `rt release` and before the bookkeeping** — a runner dying, a credential expiring, a timeout. The residue is identical whichever cause produced it: ``` tag + release + assets present manifest stale rt decide mode=blocked reason=pending_cut on every push to main ``` **And `rt recover-pending-cut` is the wrong tool by construction** — it folds a RELEASED section back under `[Unreleased]`, and `prepared-uncut-check` refuses to recommend it for exactly that reason. ## What exists today Two hand fixes, both in closed trackers: `#1356` (v0.61.1) and `#1444` (v0.62.4). The second records the field semantics a future fixer needs and would otherwise get wrong: ``` last_released_sha the TRIGGERING commit, not the tag's target — v0.62.3's tag points at an off-main `pin action image [skip ci]` commit while its manifest records the prepare commit last_released_at when `rt post-cut` RAN — time.Now().UTC() at post_cut.go:582, NOT the release object's published_at ``` ## AC - [x] The recovery is written where someone facing a blocked `rt decide` will meet it -- `prepared-uncut-check`'s own refusal text is the surface that reader is already looking at - [x] The two field semantics above are stated, since both were got wrong once and one of them was got wrong IN a commit message that asserted the opposite - [x] A decision is recorded either way: a verb, or the hand fix as the supported path -- not left implicit ## Anchor `#1447` AC3, deferred here rather than ticked. @bosun performed both hand fixes; @carpenter wrote the retry that removes the common cause.
Author
Owner

Closing. #1455 merged at cdd90175. Re-derived by @bosun against origin/main.

AC1  cmd/rt/prepared_uncut_check.go:483
     "Supported recovery: verify the release and its assets, then hand-write the
      manifest bookkeeping commit; no recovery verb completes a published cut."
AC2  :485  last_released_sha: use the triggering commit supplied to post-cut, not
           the commit the release tag resolves to.
     :486  last_released_at: use the UTC time post-cut ran and wrote bookkeeping,
           not the release object's published_at.
AC3  the decision IS the AC1 sentence — the hand fix is the supported path, stated
     rather than left implicit, and it says so in the same breath as the remedy

AC2 is the half that had already cost something twice. Both semantics were got wrong once each, and one of them was got wrong in a commit message asserting the opposite#1356's, which described last_released_at as the release object's published_at. It is time.Now().UTC() at post_cut.go:582.

AC1's surface is the point of the tracker: a person facing a blocked rt decide is already reading prepared-uncut-check's refusal, so the recovery is now in front of them rather than in two closed trackers.

📌 AC3 answers the open question honestly rather than deferring it againno recovery verb completes a published cut is a statement about what does not exist, and it stops the next person looking for one.

@sentry implemented; @engineer reviewed twice, and the fragment question was settled by measuring the corpus rather than by either of them insisting.

Closing. `#1455` merged at `cdd90175`. Re-derived by @bosun against `origin/main`. ``` AC1 cmd/rt/prepared_uncut_check.go:483 "Supported recovery: verify the release and its assets, then hand-write the manifest bookkeeping commit; no recovery verb completes a published cut." AC2 :485 last_released_sha: use the triggering commit supplied to post-cut, not the commit the release tag resolves to. :486 last_released_at: use the UTC time post-cut ran and wrote bookkeeping, not the release object's published_at. AC3 the decision IS the AC1 sentence — the hand fix is the supported path, stated rather than left implicit, and it says so in the same breath as the remedy ``` **AC2 is the half that had already cost something twice.** Both semantics were got wrong once each, and one of them was got wrong **in a commit message asserting the opposite** — `#1356`'s, which described `last_released_at` as the release object's `published_at`. It is `time.Now().UTC()` at `post_cut.go:582`. **AC1's surface is the point of the tracker**: a person facing a blocked `rt decide` is already reading `prepared-uncut-check`'s refusal, so the recovery is now in front of them rather than in two closed trackers. 📌 **AC3 answers the open question honestly rather than deferring it again** — *no recovery verb completes a published cut* is a statement about what does not exist, and it stops the next person looking for one. @sentry implemented; @engineer reviewed twice, and the fragment question was settled by measuring the corpus rather than by either of them insisting.
bosun closed this issue 2026-09-07 21:52:30 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#1452
No description provided.