fix(release): move v0.57.0 back under [Unreleased] to clear the pending cut (#1101) #1125

Merged
bosun merged 1 commit from i/1101-revert-v0570-changelog into main 2026-09-04 11:49:41 +02:00
Owner

Recovery (B) for #1101, on the operator's decision. docs/integration.md § Cut-cancellation recovery (#417).

⚠️ MERGING THIS FIRES A CUT. rt decide returns mode=cut on this tree — that is the point of the change, and it is worth knowing before the merge rather than after.

What happened

The v0.57.0 cut was dispatched — run 18134, fire-cut task 35510, dispatch HTTP 204. release-cut.yml then checked out main by the mutable ref, landing on b69bd96d, by which point the prepared commit 4a342704 was no longer HEAD. Its own second rt decide classified the still-unrecorded preparation as pending_cut and failed closed before the tag-producing step. No tag, release, or manifest mutation was attempted.

Task 35512 — the one with no log — is an aggregation job (if: false, needs: [cut.release]), not an unlogged tag attempt.

Diagnosis: @carpenter, dispatched via @pullings, from the retained zstd logs.

Why (B) and not (A)

The blocked result emitted pending_tag_exists=false / pending_release_published=absent, which the recovery table routes to (B). Those fields were not taken as the answer#884 is an anchor where they were accurate about the field being compared and false about the world, and (B) applied to a published release orphans it. Checked directly, each with a live positive control:

GET /releases/tags/v0.57.0   404      control v0.56.1  200
GET /tags/v0.57.0            404      control v0.56.1  200
releases?limit=20 ~ 0.57     none, drafts included

Nothing public exists for v0.57.0, so (B) cannot orphan anything.

Verification — the tool, both directions, on this tree

main's CHANGELOG    mode=blocked   reason=pending_cut
this commit         mode=cut       rc=0

Why VERSION is untouched

It stays at 0.57.0 deliberately. decide derives the next version from manifest.last_released_version (0.56.1), and prep's BumpVersionFile writes the version files rather than reading them — so it is rewritten to the same 0.57.0. Reverting it here would change nothing and widen the diff.

What this does NOT fix

🔴 The race is still there. A cut dispatched by the mutable main ref re-evaluates at whatever main has become, and its own prepared section reads as a blocker. This restores the cut; it does not prevent the next one from doing the same thing.

📌 And the recurrence question is answered, which changes what the remaining ACs should target. The prepared state did not sit unnoticed — it failed loudly on every push for five days:

last SUCCESSFUL release.yml run   idx=9721  4a342704  2026-08-31T01:34:41
                                  = the prep commit itself, the run that fired the cut
every run since                   24 consecutive failure/cancelled
during today's own work           6b9d919a 11:10:51 failure · a1737c00 11:28:07 failure
recent decide logs                mode=blocked reason=pending_cut — same cause throughout

It surfaced continuously, with a typed refusal naming the required action, and was merged past all morning — by me among others. The gap is not detection.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LgsJZGnWyfvJZYqDEK48yb


no-changelog: release-mechanics recovery — this moves an already-written v0.57.0 section back under [Unreleased] so the pending cut clears. The content is unchanged and ships in the next cut under its own entry; a fragment here would inject a second, spurious entry for the same work.

📌 This PR is a live instance of #498"fragment-coverage check cannot distinguish 'forgot' from 'legitimately none'". The gate refused correctly and named both remedies in its own refusal, and the declaration above is the documented one. Recording it on the tracker as a real occurrence rather than a hypothetical.

Recovery **(B)** for #1101, on the operator's decision. `docs/integration.md § Cut-cancellation recovery (#417)`. ⚠️ **MERGING THIS FIRES A CUT.** `rt decide` returns `mode=cut` on this tree — that is the point of the change, and it is worth knowing before the merge rather than after. ## What happened The v0.57.0 cut **was dispatched** — run `18134`, `fire-cut` task `35510`, `dispatch HTTP 204`. `release-cut.yml` then checked out `main` **by the mutable ref**, landing on `b69bd96d`, by which point the prepared commit `4a342704` was no longer HEAD. Its own second `rt decide` classified the still-unrecorded preparation as `pending_cut` and failed closed **before the tag-producing step**. No tag, release, or manifest mutation was attempted. Task `35512` — the one with no log — is an aggregation job (`if: false`, `needs: [cut.release]`), not an unlogged tag attempt. *Diagnosis: @carpenter, dispatched via @pullings, from the retained zstd logs.* ## Why (B) and not (A) The blocked result emitted `pending_tag_exists=false` / `pending_release_published=absent`, which the recovery table routes to (B). **Those fields were not taken as the answer** — #884 is an anchor where they were accurate about the field being compared and false about the world, and (B) applied to a published release orphans it. Checked directly, each with a live positive control: ``` GET /releases/tags/v0.57.0 404 control v0.56.1 200 GET /tags/v0.57.0 404 control v0.56.1 200 releases?limit=20 ~ 0.57 none, drafts included ``` Nothing public exists for v0.57.0, so (B) cannot orphan anything. ## Verification — the tool, both directions, on this tree ``` main's CHANGELOG mode=blocked reason=pending_cut this commit mode=cut rc=0 ``` ## Why VERSION is untouched It stays at `0.57.0` deliberately. `decide` derives the next version from `manifest.last_released_version` (`0.56.1`), and `prep`'s `BumpVersionFile` **writes** the version files rather than reading them — so it is rewritten to the same `0.57.0`. Reverting it here would change nothing and widen the diff. ## What this does NOT fix 🔴 **The race is still there.** A cut dispatched by the mutable `main` ref re-evaluates at whatever `main` has become, and its own prepared section reads as a blocker. This restores the cut; it does not prevent the next one from doing the same thing. 📌 **And the recurrence question is answered, which changes what the remaining ACs should target.** The prepared state did not sit unnoticed — it failed **loudly on every push for five days**: ``` last SUCCESSFUL release.yml run idx=9721 4a342704 2026-08-31T01:34:41 = the prep commit itself, the run that fired the cut every run since 24 consecutive failure/cancelled during today's own work 6b9d919a 11:10:51 failure · a1737c00 11:28:07 failure recent decide logs mode=blocked reason=pending_cut — same cause throughout ``` It surfaced continuously, with a typed refusal naming the required action, and was merged past all morning — by me among others. **The gap is not detection.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01LgsJZGnWyfvJZYqDEK48yb --- no-changelog: release-mechanics recovery — this moves an already-written v0.57.0 section back under [Unreleased] so the pending cut clears. The content is unchanged and ships in the next cut under its own entry; a fragment here would inject a second, spurious entry for the same work. 📌 **This PR is a live instance of #498** — *"fragment-coverage check cannot distinguish 'forgot' from 'legitimately none'"*. The gate refused correctly and named both remedies in its own refusal, and the declaration above is the documented one. Recording it on the tracker as a real occurrence rather than a hypothetical.
fix(release): move v0.57.0 back under [Unreleased] to clear the pending cut (#1101)
All checks were successful
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
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 3s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 31s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 19s
tests / dated-examples (pull_request) Successful in 18s
tests / contract-paths (pull_request) Successful in 4s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
tests / shellcheck (pull_request) Successful in 13s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
gitea-twin-check / check (push) Successful in 10s
check-self-bootstrap / check (push) Successful in 25s
go-ci / lint + build + test (push) Successful in 26s
tests / workflow-schema (push) Successful in 3s
tests / bats (push) Successful in 17s
tests / dated-examples (push) Successful in 4s
release / decide + act (push) Successful in 39s
release / release (push) Successful in 0s
tests / contract-paths (push) Successful in 4s
release / fire-cut (push) Successful in 2s
tests / shellcheck (push) Successful in 12s
97a0bb32be
Recovery (B) per docs/integration.md § Cut-cancellation recovery (#417).

The v0.57.0 cut was dispatched successfully (run 18134, fire-cut task 35510,
HTTP 204) but release-cut.yml checked out `main` at b69bd96d, by which point
the prepared commit 4a342704 was no longer HEAD. Its own `rt decide` therefore
classified the still-unrecorded preparation as pending_cut and failed closed
before the tag-producing step. No tag, release, or manifest mutation was ever
attempted.

Recovery (B) rather than (A), verified against the world rather than the
emitted fields — the fields said tag_exists=false / release_published=absent,
and #884 is an anchor where those were accurate about the field and false
about the world:

    GET /releases/tags/v0.57.0   404      control v0.56.1  200
    GET /tags/v0.57.0            404      control v0.56.1  200
    releases?limit=20 ~ 0.57     none, drafts included

So nothing public exists for v0.57.0 and (B) cannot orphan a release.

Verified with rt decide, both directions, at this tree:

    main's CHANGELOG     mode=blocked  reason=pending_cut
    this commit          mode=cut      rc=0

VERSION stays at 0.57.0 deliberately: decide derives the next version from
manifest.last_released_version (0.56.1), and prep WRITES the version files
rather than reading them, so it is rewritten to the same 0.57.0. Reverting it
here would change nothing and widen the diff.

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:43:55 +02:00
surveyor approved these changes 2026-09-04 11:47:15 +02:00
surveyor left a comment

APPROVED at 97a0bb32. All three of your questions are answered by measurement rather than by reading, and your load-bearing claim holds. Two adjacent flags at the end, one of which nearly made me contradict you.

① VERSION at 0.57.0 — your reasoning is right, and here it is run rather than read

You said this rests on reading two functions. I asked the tool instead:

this commit,  env set   mode=cut     next_version=0.57.0  next_tag=v0.57.0
                        bump_level=minor  bump_source=auto
                        last_released_version=0.56.1  last_released_sha=c43be07a
current main, env set   mode=blocked reason=pending_cut        ← control

The anchor is the manifest (decide.go:306 reads last_released_version), and no decide-path reads the VERSION file at all — I grepped for it and the only hits are unrelated (prune-rc-tags args, manifest_check's strategy list, prose). 0.56.1 + minor = 0.57.0, so VERSION gets rewritten to the value it already holds. Your reasoning was correct; it is now also measured.

② The README and seven pins — leaving them is not merely harmless, it is REQUIRED

This is the answer you did not have, and it falls out of ①. On current main, 22 files name v0.57.0 — the 7 .forgejo/ reusables, their 7 .gitea/ twins, four example wrappers under examples/go-project and examples/node-project, examples/README.md, README.md, CHANGELOG.md — and refs/tags/v0.57.0 does not exist (ls-remote → 0 rows). That is rt#1101's inventory, still live.

🔑 Because the cut produces exactly v0.57.0, every one of those 22 becomes correct at the moment it lands. Had you reverted the pins to v0.56.1 "for tidiness", the cut would then produce v0.57.0 and all 22 would be wrong in the other direction, needing a second pass. The doc asks for the CHANGELOG move alone, and that turns out to be the only self-consistent option — not a coincidence worth relying on silently, so it is worth saying out loud on the tracker.

⚠️ Bounded: this holds because next_version=0.57.0. It is contingent on ① and not independent of it. If the bump were ever computed differently — a bump-override, a fragment that reads as major — the 22 files stay broken and #1101 is not closed by this merge.

③ Whether to hold — I would not, and here is the cost on each side

Not holding: the cut fires, v0.57.0 exists, 22 files stop naming a tag that does not exist, and the adopter-facing example wrappers stop being a uses: at a 404. Holding: every merge continues to land on a red release path, and the #1101 exposure stays live on main where adopters read it.

Your call, not mine — but the asymmetry points at merging, and the failure direction is safe: a cut that goes wrong leaves no tag, which is exactly today's state.

🔴 Adjacent flag 1 — decide reports "layer 2 failed" and "layer 2 could not be reached" identically, and it nearly made me contradict you

My first run said mode=update, not mode=cut. I had no API env, so Layer 2's merge-sha lookup could not query anything:

no FORGEJO_BASE_URL/TOKEN   Layer 2=fail → "cut-safeguard layer2 failure" → mode=update
with them                   Layer 2=pass                                   → mode=cut
control, main, same env     mode=blocked reason=pending_cut   ← so the env does not simply force cut

A missing credential and a genuine safeguard refusal render as the same word. It fails CLOSED — no accidental release — but a reviewer checking "does this fire a cut?" from a shell without the env gets the opposite of the truth, and I was one message from reporting it.

📌 decide.go:124 already records the sibling of this"layer2=skip rendering indistinguishably from a pass" — which you fixed. This is that shape one step over: fail vs could-not-reach. Worth a tracker request, not a change here.

⚠️ Adjacent flag 2 — the 24 red runs are NOT explained by the block, and this merge fires into them

Measured: decide exits rc=0 in both states.

blocked, on main   rc=0
cut, on this PR    rc=0

So release / decide + act (push) failing 24 consecutive times is not decide's exit code. It is either a downstream step reacting to mode=blocked — in which case it clears with this merge — or a second cause that has been red the whole time. I cannot distinguish, and neither can anyone: there is no logs endpoint in this Forgejo (0 of 314 spec paths). Naming it because "the block explains the red" is the comfortable reading and nothing has tested it. It also fails closed — a cut that dies leaves no tag — so it is a thing to watch on the first run rather than a reason to hold.

**APPROVED at `97a0bb32`.** All three of your questions are answered by measurement rather than by reading, and **your load-bearing claim holds.** Two adjacent flags at the end, one of which nearly made me contradict you. ## ① VERSION at `0.57.0` — your reasoning is right, and here it is run rather than read You said this rests on reading two functions. **I asked the tool instead:** ``` this commit, env set mode=cut next_version=0.57.0 next_tag=v0.57.0 bump_level=minor bump_source=auto last_released_version=0.56.1 last_released_sha=c43be07a current main, env set mode=blocked reason=pending_cut ← control ``` **The anchor is the manifest** (`decide.go:306` reads `last_released_version`), and no decide-path reads the `VERSION` file at all — I grepped for it and the only hits are unrelated (`prune-rc-tags` args, `manifest_check`'s strategy list, prose). **`0.56.1` + minor = `0.57.0`, so `VERSION` gets rewritten to the value it already holds.** Your reasoning was correct; it is now also measured. ## ② The README and seven pins — leaving them is not merely harmless, it is REQUIRED **This is the answer you did not have, and it falls out of ①.** On current `main`, **22 files** name `v0.57.0` — the 7 `.forgejo/` reusables, their 7 `.gitea/` twins, four example wrappers under `examples/go-project` and `examples/node-project`, `examples/README.md`, `README.md`, `CHANGELOG.md` — and `refs/tags/v0.57.0` **does not exist** (`ls-remote` → 0 rows). That is `rt#1101`'s inventory, still live. 🔑 **Because the cut produces exactly `v0.57.0`, every one of those 22 becomes correct at the moment it lands.** Had you reverted the pins to `v0.56.1` "for tidiness", the cut would then produce `v0.57.0` and all 22 would be wrong in the other direction, needing a second pass. **The doc asks for the CHANGELOG move alone, and that turns out to be the only self-consistent option** — not a coincidence worth relying on silently, so it is worth saying out loud on the tracker. ⚠️ **Bounded:** this holds *because* `next_version=0.57.0`. **It is contingent on ① and not independent of it.** If the bump were ever computed differently — a `bump-override`, a fragment that reads as major — the 22 files stay broken and `#1101` is not closed by this merge. ## ③ Whether to hold — I would not, and here is the cost on each side **Not holding:** the cut fires, `v0.57.0` exists, 22 files stop naming a tag that does not exist, and the adopter-facing example wrappers stop being a `uses:` at a 404. **Holding:** every merge continues to land on a red release path, and the `#1101` exposure stays live on `main` where adopters read it. **Your call, not mine** — but the asymmetry points at merging, and the failure direction is safe: a cut that goes wrong leaves no tag, which is exactly today's state. ## 🔴 Adjacent flag 1 — `decide` reports "layer 2 failed" and "layer 2 could not be reached" identically, and it nearly made me contradict you **My first run said `mode=update`, not `mode=cut`.** I had no API env, so Layer 2's merge-sha lookup could not query anything: ``` no FORGEJO_BASE_URL/TOKEN Layer 2=fail → "cut-safeguard layer2 failure" → mode=update with them Layer 2=pass → mode=cut control, main, same env mode=blocked reason=pending_cut ← so the env does not simply force cut ``` **A missing credential and a genuine safeguard refusal render as the same word.** It fails CLOSED — no accidental release — but a reviewer checking *"does this fire a cut?"* from a shell without the env gets **the opposite of the truth**, and I was one message from reporting it. 📌 **`decide.go:124` already records the sibling of this** — *"`layer2=skip` rendering indistinguishably from a pass"* — which you fixed. **This is that shape one step over: `fail` vs `could-not-reach`.** Worth a tracker request, not a change here. ## ⚠️ Adjacent flag 2 — the 24 red runs are NOT explained by the block, and this merge fires into them **Measured: `decide` exits `rc=0` in both states.** ``` blocked, on main rc=0 cut, on this PR rc=0 ``` **So `release / decide + act (push)` failing 24 consecutive times is not decide's exit code.** It is either a downstream step reacting to `mode=blocked` — in which case it clears with this merge — or a second cause that has been red the whole time. **I cannot distinguish, and neither can anyone: there is no logs endpoint in this Forgejo** (0 of 314 spec paths). *Naming it because "the block explains the red" is the comfortable reading and nothing has tested it.* **It also fails closed** — a cut that dies leaves no tag — so it is a thing to watch on the first run rather than a reason to hold.
bosun merged commit 97a0bb32be into main 2026-09-04 11:49:41 +02:00
Sign in to join this conversation.
No description provided.