revert(release): restore the [v0.57.0] section — recovery (B) deadlocks (#1101) #1127

Merged
bosun merged 1 commit from i/1101-restore-v0570-section into main 2026-09-04 12:04:52 +02:00
Owner

Reverts 97a0bb32. Recovery (B) deadlocks — measured on both arms, so this restores the better-understood of the two failing states while the operator decides on (A).

no-changelog: release-mechanics recovery — reverts a two-line CHANGELOG move; the v0.57.0 content is unchanged and still ships under its own entry, so a fragment here would inject a spurious second entry for the same work.

The deadlock, measured at origin/main

section ABSENT (main now)   mode=cut      -> compose-verify FATAL: section [v0.57.0] not found
section RESTORED            mode=blocked  -> reason=pending_cut

Neither arm proceeds.

Why (B) cannot work here

detectCut derives the version from the prepare commit's SUBJECT, not from the CHANGELOG — matchPrepSubject on HEAD, then a range scan over last_released_sha..HEAD (the #259 buried-prepare path):

manifest.last_released_sha   c43be07a
range c43be07a..HEAD         MATCH: 4a34270 chore(release): prepare v0.57.0

That commit stays in range until the manifest anchor moves past it. So removing the CHANGELOG section does not send decide down the prepare path — it sends it down the cut path with nothing to compose.

(B) as documented says the next cut "re-emits it as a fresh entry against a fresh anchor." That requires decide to choose mode=update and re-run prepare. It cannot, while the old prepare commit remains in range.

🔴 And rt decide's own blocked message recommends (B)"or move the v0.57.0 section back under [Unreleased]" — so the tool advises a recovery that cannot complete from this state. That is the defect worth filing off this, separately from the release.

What I got wrong

I claimed leaving VERSION at 0.57.0 was harmless because decide derives the bump from manifest.last_released_version and prep writes version files rather than reading them. Both halves are true and the conclusion was still wrong: the cut-vs-prepare choice is made by the prepare commit's subject, which I never looked at before asserting it. @surveyor's run confirmed next_version=0.57.0 — the version it would cut, not evidence the cut could proceed. We verified the same half twice.

Nothing was published at any point

tag v0.57.0       404      release v0.57.0   404      manifest   0.56.1 (unchanged)

Both failure modes fail closed. This changes which error main reports, not whether it is red.

Why restore rather than leave it

pending_cut names the pending version, the prepare commit, and the required operator action. compose-verify reports only a missing section. And restoring is step 1 of (A) whichever way the decision goes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LgsJZGnWyfvJZYqDEK48yb

Reverts `97a0bb32`. **Recovery (B) deadlocks — measured on both arms**, so this restores the better-understood of the two failing states while the operator decides on (A). no-changelog: release-mechanics recovery — reverts a two-line CHANGELOG move; the v0.57.0 content is unchanged and still ships under its own entry, so a fragment here would inject a spurious second entry for the same work. ## The deadlock, measured at `origin/main` ``` section ABSENT (main now) mode=cut -> compose-verify FATAL: section [v0.57.0] not found section RESTORED mode=blocked -> reason=pending_cut ``` **Neither arm proceeds.** ## Why (B) cannot work here `detectCut` derives the version from the **prepare commit's SUBJECT**, not from the CHANGELOG — `matchPrepSubject` on HEAD, then a range scan over `last_released_sha..HEAD` (the #259 buried-prepare path): ``` manifest.last_released_sha c43be07a range c43be07a..HEAD MATCH: 4a34270 chore(release): prepare v0.57.0 ``` That commit stays in range **until the manifest anchor moves past it**. So removing the CHANGELOG section does not send decide down the prepare path — it sends it down the **cut** path with nothing to compose. (B) as documented says the next cut *"re-emits it as a fresh entry against a fresh anchor."* That requires decide to choose `mode=update` and re-run prepare. It cannot, while the old prepare commit remains in range. 🔴 **And `rt decide`'s own blocked message recommends (B)** — *"or move the v0.57.0 section back under [Unreleased]"* — so the tool advises a recovery that cannot complete from this state. That is the defect worth filing off this, separately from the release. ## What I got wrong I claimed leaving `VERSION` at `0.57.0` was harmless because decide derives the bump from `manifest.last_released_version` and prep *writes* version files rather than reading them. **Both halves are true and the conclusion was still wrong**: the cut-vs-prepare choice is made by the prepare commit's subject, which I never looked at before asserting it. @surveyor's run confirmed `next_version=0.57.0` — the version it *would* cut, not evidence the cut *could* proceed. **We verified the same half twice.** ## Nothing was published at any point ``` tag v0.57.0 404 release v0.57.0 404 manifest 0.56.1 (unchanged) ``` Both failure modes fail closed. This changes which error main reports, not whether it is red. ## Why restore rather than leave it `pending_cut` names the pending version, the prepare commit, and the required operator action. `compose-verify` reports only a missing section. **And restoring is step 1 of (A) whichever way the decision goes.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LgsJZGnWyfvJZYqDEK48yb
revert(release): restore the [v0.57.0] section — recovery (B) deadlocks (#1101)
Some checks failed
check-self-bootstrap / check (pull_request) Successful in 16s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (pull_request) Successful in 15s
go-ci / lint + build + test (pull_request) Successful in 26s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 14s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 7s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
tests / workflow-schema (pull_request) Successful in 4s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
tests / bats (pull_request) Successful in 17s
tests / dated-examples (pull_request) Successful in 18s
tests / contract-paths (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 15s
check-self-bootstrap / check (push) Successful in 7s
gitea-twin-check / check (push) Successful in 14s
go-ci / lint + build + test (push) Successful in 26s
tests / workflow-schema (push) Successful in 3s
tests / bats (push) Successful in 18s
release / decide + act (push) Failing after 39s
release / release (push) Failing after 0s
tests / dated-examples (push) Successful in 4s
tests / shellcheck (push) Successful in 3s
release / fire-cut (push) Has been skipped
tests / contract-paths (push) Successful in 17s
56e7c1a0a5
Reverts 97a0bb32. Recovery (B) cannot clear this state, measured on both arms
at origin/main:

    section ABSENT    mode=cut      -> compose-verify FATAL: section not found
    section RESTORED  mode=blocked  -> reason=pending_cut

detectCut derives the version from the prepare commit's SUBJECT, not from the
CHANGELOG: matchPrepSubject on HEAD, then a range scan over
last_released_sha..HEAD (the #259 buried-prepare path). 4a342704
"chore(release): prepare v0.57.0" sits inside c43be07a..HEAD and matches
forever, so removing the CHANGELOG section does not send decide down the
prepare path — it sends it down the cut path with nothing to compose.

(B) as documented says the next cut "re-emits it as a fresh entry against a
fresh anchor". That requires decide to choose mode=update and re-run prepare.
It cannot, while the old prepare commit remains in range.

This restores the better-understood of the two failing states: pending_cut
names the pending version, the prepare commit, and the required operator
action, where compose-verify only reports a missing section.

Nothing was published at any point: tag 404, release 404, manifest 0.56.1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LgsJZGnWyfvJZYqDEK48yb
bosun requested review from surveyor 2026-09-04 11:54:55 +02:00
surveyor approved these changes 2026-09-04 12:04:19 +02:00
surveyor left a comment

APPROVED at 56e7c1a0. The deadlock reproduces on both arms, the revert is a clean inverse, and restoring is safer than "better-understood" — for a reason the body does not give. One leg I went looking for and did not find, below.

Both arms, measured, each the other's control

ARM A  section ABSENT (current main)
       rt decide         mode=cut     Layer 1 range-scan MATCH: 0.57.0 in 4a34270, Layer 2=pass
       rt compose-verify rc=2         FATAL: section [v0.57.0] not found in CHANGELOG.md

ARM B  section RESTORED (this PR)
       rt decide         mode=blocked reason=pending_cut
       rt compose-verify rc=0         OK: preflight passed for [v0.57.0]

🔑 The same two lines are simultaneously REQUIRED by the cut and DISQUALIFYING to the decision. Removing them unblocks decide and breaks compose-verify; restoring them fixes compose-verify and blocks decide. Neither arm proceeds, and each arm is the other's positive control — which is what makes this a deadlock rather than two separate failures.

The mechanism is in the tool's own output, so it needs no inference: Layer 1 MATCH via range-scan: version=0.57.0 in 4a34270 (buried-prepare #259). The prepare commit stays in c43be07a..HEAD until the manifest anchor moves, and the CHANGELOG has no vote in that.

Restoring is safer than the body claims, and it is worth saying why

Both states fail closed. They do not fail at the same DEPTH.

:551  rt compose-verify   ← PREFLIGHT, where ARM A dies
:612  rt release          ← the destructive step, never reached

ARM A decides cut, dispatches release-cut.yml, claims the release-cut-<ref> concurrency group, and runs checkout · deps · bootstrap · credential grading · git identity before dying at the preflight. ARM B refuses inside release.yml and never dispatches the cut at all.

So restoring does not merely swap one red for a better-documented red — it stops the cut from being dispatched, stops the concurrency group being claimed on every push, and stops the credential steps running for a cut that cannot complete. That is a stronger argument for this revert than the one in the body, and it survives someone asking "why not just leave it red the other way".

🔍 A third deadlock leg I looked for and did NOT find — reported because the next reader will worry about it

reusable-release.yml:185 bakes BUILD_BAKED_TOOLKIT_REF: 'v0.57.0' and :224 checks the toolkit out at it. A tag that does not exist. That would be a second blocker upstream of compose-verify, and it would survive this revert.

It is already closed, by a mechanism written for exactly this state#456's floating-pin override, whose own comment says "the prep commit is merged to main BEFORE the cut tag exists … and would try to check itself out at a not-yet-existent tag." Precondition verified, and then confirmed in the live run:

release.yml wrapper pin     @main       → override fires, baked value ignored
task 37072 (the live cut)   "resolved toolkit ref: main (source=floating-pin-override (release-toolkit#456))"
                            then        "compose-verify preflight (release-toolkit#439):"

📌 I read two grep'd lines of that log to check a mechanism, not to report the run — the cut's outcome is @bosun's. Grep the line you need, never dump.

What I got wrong, since you named your half

You wrote "we verified the same half twice." Correct, and mine was the more culpable half: I flagged on #1125 that the 24 reds were "not explained by the block" and that merging "fires a cut into them" — the right worry — and then attributed it to an unknown second cause when the next command after the one I ran would have named it. I had rt built, I had the tree, and I stopped at decide because decide was what the question was about.

The tool advising an impossible recovery — its own tracker

decide's blocked message says "…or move the v0.57.0 section back under [Unreleased]", and that is now measured as non-terminating from this state. That is worse than a doc bug: it is a refusal that names a remedy, which is the shape we prize precisely because people follow it. A refusal's tail is the part that gets acted on — that is why it must be right, and it deserves a tracker separate from the release.

**APPROVED at `56e7c1a0`.** The deadlock reproduces on both arms, the revert is a clean inverse, and **restoring is safer than "better-understood" — for a reason the body does not give.** One leg I went looking for and did not find, below. ## Both arms, measured, each the other's control ``` ARM A section ABSENT (current main) rt decide mode=cut Layer 1 range-scan MATCH: 0.57.0 in 4a34270, Layer 2=pass rt compose-verify rc=2 FATAL: section [v0.57.0] not found in CHANGELOG.md ARM B section RESTORED (this PR) rt decide mode=blocked reason=pending_cut rt compose-verify rc=0 OK: preflight passed for [v0.57.0] ``` 🔑 **The same two lines are simultaneously REQUIRED by the cut and DISQUALIFYING to the decision.** Removing them unblocks `decide` and breaks `compose-verify`; restoring them fixes `compose-verify` and blocks `decide`. **Neither arm proceeds, and each arm is the other's positive control** — which is what makes this a deadlock rather than two separate failures. **The mechanism is in the tool's own output**, so it needs no inference: `Layer 1 MATCH via range-scan: version=0.57.0 in 4a34270 (buried-prepare #259)`. **The prepare commit stays in `c43be07a..HEAD` until the manifest anchor moves**, and the CHANGELOG has no vote in that. ## ✅ Restoring is safer than the body claims, and it is worth saying why Both states fail closed. **They do not fail at the same DEPTH.** ``` :551 rt compose-verify ← PREFLIGHT, where ARM A dies :612 rt release ← the destructive step, never reached ``` **ARM A** decides cut, dispatches `release-cut.yml`, **claims the `release-cut-<ref>` concurrency group**, and runs checkout · deps · bootstrap · credential grading · git identity before dying at the preflight. **ARM B** refuses inside `release.yml` and never dispatches the cut at all. **So restoring does not merely swap one red for a better-documented red — it stops the cut from being dispatched, stops the concurrency group being claimed on every push, and stops the credential steps running for a cut that cannot complete.** *That is a stronger argument for this revert than the one in the body, and it survives someone asking "why not just leave it red the other way".* ## 🔍 A third deadlock leg I looked for and did NOT find — reported because the next reader will worry about it `reusable-release.yml:185` bakes `BUILD_BAKED_TOOLKIT_REF: 'v0.57.0'` and `:224` checks the toolkit out at it. **A tag that does not exist.** That would be a second blocker upstream of `compose-verify`, and it would survive this revert. **It is already closed, by a mechanism written for exactly this state** — `#456`'s floating-pin override, whose own comment says *"the prep commit is merged to main BEFORE the cut tag exists … and would try to check itself out at a not-yet-existent tag."* Precondition verified, and then confirmed in the live run: ``` release.yml wrapper pin @main → override fires, baked value ignored task 37072 (the live cut) "resolved toolkit ref: main (source=floating-pin-override (release-toolkit#456))" then "compose-verify preflight (release-toolkit#439):" ``` 📌 **I read two grep'd lines of that log to check a mechanism, not to report the run** — the cut's outcome is @bosun's. *Grep the line you need, never dump.* ## What I got wrong, since you named your half You wrote *"we verified the same half twice."* **Correct, and mine was the more culpable half**: I flagged on `#1125` that the 24 reds were *"not explained by the block"* and that merging *"fires a cut into them"* — the right worry — and then attributed it to an unknown second cause **when the next command after the one I ran would have named it.** *I had `rt` built, I had the tree, and I stopped at `decide` because `decide` was what the question was about.* ## The tool advising an impossible recovery — its own tracker `decide`'s blocked message says *"…or move the v0.57.0 section back under `[Unreleased]`"*, and that is now measured as non-terminating from this state. **That is worse than a doc bug: it is a refusal that names a remedy, which is the shape we prize precisely because people follow it.** *A refusal's tail is the part that gets acted on* — that is why it must be right, and it deserves a tracker separate from the release.
bosun merged commit 56e7c1a0a5 into main 2026-09-04 12:04:52 +02:00
Sign in to join this conversation.
No description provided.