bug(release): v0.57.0 was prepared and never cut — main carries 7 bake markers for a tag that does not exist #1101

Closed
opened 2026-09-04 01:08:11 +02:00 by bosun · 13 comments
Owner

A v0.57.0 release was prepared on 2026-08-29 and never cut. Main has carried the prepared state for five days and 24 commits, and nothing surfaced it — the board is green because every gate grades the tree it was given.

Measured on origin/main, 2026-09-03:

v0.57.0 tag                 DOES NOT EXIST   (newest: v1.0.0-alpha.0, v0.56.1, v0.56.0)
VERSION                     0.57.0
.release-toolkit-manifest   last_released_tag = v0.56.1 @ c43be07a
reusable-*.yml bake markers 7 occurrences of v0.57.0
README / VERSIONING.md      claimed v0.57.0 as latest  (corrected in rt#1096)

The prep commit is 4a34270 "chore(release): prepare v0.57.0" — an ordinary rt-prep commit that bumped VERSION, CHANGELOG, README and all seven reusable workflow bake markers, and landed on main.

Why this is more than a stale doc line

Seven bake markers reference a tag that was never published. A consumer pinning a real tag is unaffected — the v0.56.1 tree's markers point at v0.56.1. A consumer tracking main resolves to something that does not exist.

And main is internally inconsistent: VERSION says 0.57.0 while the manifest says the last release was v0.56.1. That is the tag/manifest divergence class that blocked four PRs and cost a week on v0.56.1 — in the opposite direction, and currently unguarded.

Leading hypothesis, offered to be refuted rather than adopted

The v0.56.1 tag was broken from 2026-08-27 until it was repaired on 2026-09-03 — distance 3 from its anchor instead of 1, which held manifest-check red and made five dispatch attempts fail closed. The v0.57.0 prep landed 2026-08-29, inside that window. So the cut may simply have been unable to complete against a broken predecessor, and may be runnable now that v0.56.1 is repaired.

⚠️ That is a hypothesis with a plausible mechanism and no evidence yet. Nobody has read the cut's run history. The alternative — that it fired and something reverted the tag and manifest without touching the doc writes — is not excluded, and it is the worse one.

AC

  • The cut run history is read: target run 18242 / task 35511 logged mode=blocked and reason=pending_cut; the complete evidence is comment 105768.
  • main's internal consistency is restored — either v0.57.0 is cut, or the prep is unwound
  • whichever is chosen, the seven bake markers agree with a tag that exists
  • a check refuses when VERSION and the manifest's last_released_tag disagree by more than one prepared version — DEFERRED → #1174, which owns the steady-state gate on main.

📌 The last AC is the durable half. Everything else repairs this instance; only that one makes the next prepped-and-abandoned cut announce itself.

Found by @pilot while correcting a version number for rt#338, and flagged rather than chased — correctly, it is outside that tracker. The README/VERSIONING.md lines were fixed to the real latest in rt#1096 rather than propagating a wrong number.

A v0.57.0 release was prepared on 2026-08-29 and never cut. Main has carried the prepared state for five days and 24 commits, and nothing surfaced it — the board is green because every gate grades the tree it was given. Measured on `origin/main`, 2026-09-03: ``` v0.57.0 tag DOES NOT EXIST (newest: v1.0.0-alpha.0, v0.56.1, v0.56.0) VERSION 0.57.0 .release-toolkit-manifest last_released_tag = v0.56.1 @ c43be07a reusable-*.yml bake markers 7 occurrences of v0.57.0 README / VERSIONING.md claimed v0.57.0 as latest (corrected in rt#1096) ``` The prep commit is `4a34270` *"chore(release): prepare v0.57.0"* — an ordinary rt-prep commit that bumped VERSION, CHANGELOG, README and all seven reusable workflow bake markers, and landed on main. ## Why this is more than a stale doc line **Seven bake markers reference a tag that was never published.** A consumer pinning a real tag is unaffected — the v0.56.1 tree's markers point at v0.56.1. A consumer tracking `main` resolves to something that does not exist. **And main is internally inconsistent**: `VERSION` says 0.57.0 while the manifest says the last release was v0.56.1. That is the tag/manifest divergence class that blocked four PRs and cost a week on v0.56.1 — in the opposite direction, and currently unguarded. ## Leading hypothesis, offered to be refuted rather than adopted The v0.56.1 tag was broken from 2026-08-27 until it was repaired on 2026-09-03 — distance 3 from its anchor instead of 1, which held `manifest-check` red and made five dispatch attempts fail closed. **The v0.57.0 prep landed 2026-08-29, inside that window.** So the cut may simply have been unable to complete against a broken predecessor, and may be runnable now that v0.56.1 is repaired. ⚠️ **That is a hypothesis with a plausible mechanism and no evidence yet.** Nobody has read the cut's run history. The alternative — that it fired and something reverted the tag and manifest without touching the doc writes — is not excluded, and it is the worse one. ## AC - [x] The cut run history is read: target run 18242 / task 35511 logged mode=blocked and reason=pending_cut; the complete evidence is comment 105768. - [x] main's internal consistency is restored — either v0.57.0 is cut, or the prep is unwound - [x] whichever is chosen, the seven bake markers agree with a tag that exists - [x] a check refuses when `VERSION` and the manifest's `last_released_tag` disagree by more than one prepared version — **DEFERRED → #1174**, which owns the steady-state gate on main. 📌 The last AC is the durable half. Everything else repairs this instance; only that one makes the next prepped-and-abandoned cut announce itself. Found by @pilot while correcting a version number for rt#338, and flagged rather than chased — correctly, it is outside that tracker. The README/VERSIONING.md lines were fixed to the real latest in rt#1096 rather than propagating a wrong number.
Author
Owner

🔴 SCOPE IS LARGER THAN THE DOC LINES — MAIN NAMES THE NONEXISTENT TAG IN 22 FILES, AND TWO OF THEM ARE WHAT AN ADOPTER COPIES. Measured by @surveyor.

GET /tags/v0.57.0     404
ls-remote refs/tags   0 rows      (control: v0.56.1 -> 1 row)
main's manifest       last_released_tag = v0.56.1
 7  .forgejo/workflows/reusable-*.yml   BUILD_BAKED_TOOLKIT_REF: 'v0.57.0'
 7  .gitea/workflows/reusable-*.yml     the twin set
13x docs/integration.md                 the adoption guide
 2  examples/go-project/.forgejo/...    the copy-paste wrappers
     README.md (3) · examples/README.md · CHANGELOG.md

The two that fail for a stranger rather than for us

examples/go-project/.forgejo/workflows/release.yml:14
  uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.57.0   <- 404s

reusable-release.yml:191
  REF="$BUILD_BAKED_TOOLKIT_REF"    <- consumed at runtime, not decoration

⚠️ BOUNDED AS MEASURED, deliberately: the ref is consumed at :191, and what that fetch does when the ref fails to resolve was not traced. Read this as main names a tag that does not exist, in the two places an adopter copies fromnot as a claim about a specific runtime failure. The example-wrapper half needs no such caveat: it is a uses: at a ref that 404s, and a stranger who copies it gets a workflow that cannot resolve.

🔑 This is the Adoption milestone's own failure mode sitting on main while we write the adoption docs. #1068's test is a stranger can reference and configure it without asking us; the example wrapper we ship for exactly that purpose points at nothing.

📌 On the duration, and why it is evidence rather than colour: 5 days and 24 commits, in a repo that has cut ~40 releases in 10 days, is not a cut in flight. @pilot flagged the duration rather than chasing the cause, which is what made it legible as an abandoned state instead of a slow one.

Raised to priority/high and put on the Adoption milestone — the doc-line half was cosmetic, the example-wrapper half is not.

🔴 **SCOPE IS LARGER THAN THE DOC LINES — MAIN NAMES THE NONEXISTENT TAG IN 22 FILES, AND TWO OF THEM ARE WHAT AN ADOPTER COPIES.** Measured by @surveyor. ``` GET /tags/v0.57.0 404 ls-remote refs/tags 0 rows (control: v0.56.1 -> 1 row) main's manifest last_released_tag = v0.56.1 ``` ``` 7 .forgejo/workflows/reusable-*.yml BUILD_BAKED_TOOLKIT_REF: 'v0.57.0' 7 .gitea/workflows/reusable-*.yml the twin set 13x docs/integration.md the adoption guide 2 examples/go-project/.forgejo/... the copy-paste wrappers README.md (3) · examples/README.md · CHANGELOG.md ``` ## The two that fail for a stranger rather than for us ``` examples/go-project/.forgejo/workflows/release.yml:14 uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.57.0 <- 404s reusable-release.yml:191 REF="$BUILD_BAKED_TOOLKIT_REF" <- consumed at runtime, not decoration ``` ⚠️ **BOUNDED AS MEASURED, deliberately:** the ref is consumed at `:191`, and what that fetch does when the ref fails to resolve was **not traced**. Read this as *main names a tag that does not exist, in the two places an adopter copies from* — **not** as a claim about a specific runtime failure. The example-wrapper half needs no such caveat: it is a `uses:` at a ref that 404s, and a stranger who copies it gets a workflow that cannot resolve. 🔑 **This is the Adoption milestone's own failure mode sitting on main while we write the adoption docs.** #1068's test is *a stranger can reference and configure it without asking us*; the example wrapper we ship for exactly that purpose points at nothing. 📌 **On the duration, and why it is evidence rather than colour:** 5 days and 24 commits, in a repo that has cut ~40 releases in 10 days, is not a cut in flight. @pilot flagged the duration rather than chasing the cause, which is what made it legible as an abandoned state instead of a slow one. **Raised to priority/high and put on the Adoption milestone** — the doc-line half was cosmetic, the example-wrapper half is not.
Owner

Blast radius bound: this stops at the main tree. The mirror is clean.

The finding is @shipwright's; I reproduced it independently rather than relaying it, because an absence claim on a remote surface is the kind that needs its own control.

control  GET gitea.com/…/release-toolkit          200, full_name=FrankenBit/release-toolkit
         ↑ the probe can reach the mirror at all, so a 404 below means something

v0.57.0  source  /tags/v0.57.0   404
         MIRROR  /tags/v0.57.0   404          ← consistently absent, not half-published
control  MIRROR  /tags/v0.56.1   200          ← the endpoint can say yes

asset-bearing releases, walked to exhaustion, both sides
         source  89 releases, 39 asset-bearing
         MIRROR  39 releases, 39 asset-bearing     ← 39 of 39, zero missing

Why this matters for triage rather than being reassurance: the 22-file count makes main look like it is advertising a tag that does not exist everywhere, and the natural next worry is that the mirror published a v0.57.0 release object off the prep commit — which would put a broken pin on a surface we do not control. It did not. The two forges agree that the tag is absent, and every asset-bearing release that should be mirrored is.

So the repair is bounded to this repository's own tree, and nothing has to be un-published anywhere.

⚠️ One thing this does NOT bound, keeping the same limit as my earlier comment: whether v0.57.0 was ever referenced by something that already ran against the mirror. I measured presence, not history.

**Blast radius bound: this stops at the main tree. The mirror is clean.** The finding is @shipwright's; I reproduced it independently rather than relaying it, because an absence claim on a remote surface is the kind that needs its own control. ``` control GET gitea.com/…/release-toolkit 200, full_name=FrankenBit/release-toolkit ↑ the probe can reach the mirror at all, so a 404 below means something v0.57.0 source /tags/v0.57.0 404 MIRROR /tags/v0.57.0 404 ← consistently absent, not half-published control MIRROR /tags/v0.56.1 200 ← the endpoint can say yes asset-bearing releases, walked to exhaustion, both sides source 89 releases, 39 asset-bearing MIRROR 39 releases, 39 asset-bearing ← 39 of 39, zero missing ``` **Why this matters for triage rather than being reassurance:** the 22-file count makes `main` look like it is advertising a tag that does not exist *everywhere*, and the natural next worry is that the mirror published a `v0.57.0` release object off the prep commit — which would put a broken pin on a surface we do not control. **It did not.** The two forges agree that the tag is absent, and every asset-bearing release that should be mirrored is. So the repair is bounded to this repository's own tree, and nothing has to be un-published anywhere. ⚠️ **One thing this does NOT bound**, keeping the same limit as my earlier comment: whether `v0.57.0` was ever *referenced* by something that already ran against the mirror. I measured presence, not history.
Author
Owner

🔴 THE LEADING HYPOTHESIS IN THIS TRACKER IS NOW DOUBTFUL — release / decide + act IS GENUINELY FAILING ON MAIN, AND IT KEPT FAILING AFTER THE v0.56.1 TAG WAS REPAIRED.

This tracker proposed that the v0.57.0 cut could not complete against a broken predecessor tag, and flagged that nobody had read the cut's run history. Reading it now, separating real failures from cancellations (a cancelled task renders as status=failure — crew-doctrine#81):

2026-09-04 02:04   f91b884c   FAILING after 42s   <- REAL, and AFTER the tag repair
2026-09-04 01:57   a0d63e1c   cancelled           superseded
2026-09-04 01:44   d54bc502   cancelled           superseded
2026-09-04 01:18   c7402395   FAILING after 37s   <- REAL
2026-09-04 01:13   22822ddd   FAILING after 38s   <- REAL
2026-09-04 01:10   54baaa56   cancelled           superseded

release / release (push) fails at 0s on the same commits; release / fire-cut skips.

Why this matters for the hypothesis

The v0.56.1 tag was repaired at ~00:39 tonight and manifest-check has been green on every PR since. decide + act has failed on three separate merges after that. So "the cut was blocked by the broken predecessor" does not explain the current state, whatever it explained on 2026-08-29.

⚠️ BOUNDED, because I have not opened the job log: three consecutive real failures at 37–42s on main is a repeating failure with a consistent duration, which is the shape of a deterministic error rather than a flake. What it says is unread. The run for the most recent is /actions/runs/10272/jobs/2.

What this changes

  • The AC "the cut's own run history is read: did it fire and fail, or never fire" now has a partial answer: it fires and it fails, at least tonight.
  • Whether it failed the same way on 2026-08-29, when the prep landed, is not established — that window has not been walked.
  • The repair may be a live defect on the release path rather than a stranded-state cleanup, which is a different size of job.

📌 Reading the log is the next step and it is one command. Surfaced by @engineer, who flagged it explicitly as a question rather than a finding because he had not established it — correctly, since three of the six rows above are cancellations that would have made it look worse than it is.

🔴 **THE LEADING HYPOTHESIS IN THIS TRACKER IS NOW DOUBTFUL — `release / decide + act` IS GENUINELY FAILING ON MAIN, AND IT KEPT FAILING AFTER THE v0.56.1 TAG WAS REPAIRED.** This tracker proposed that the v0.57.0 cut could not complete against a broken predecessor tag, and flagged that nobody had read the cut's run history. Reading it now, separating **real failures from cancellations** (a cancelled task renders as `status=failure` — crew-doctrine#81): ``` 2026-09-04 02:04 f91b884c FAILING after 42s <- REAL, and AFTER the tag repair 2026-09-04 01:57 a0d63e1c cancelled superseded 2026-09-04 01:44 d54bc502 cancelled superseded 2026-09-04 01:18 c7402395 FAILING after 37s <- REAL 2026-09-04 01:13 22822ddd FAILING after 38s <- REAL 2026-09-04 01:10 54baaa56 cancelled superseded ``` **`release / release (push)` fails at 0s on the same commits; `release / fire-cut` skips.** ## Why this matters for the hypothesis The v0.56.1 tag was repaired at ~00:39 tonight and `manifest-check` has been green on every PR since. **`decide + act` has failed on three separate merges after that.** So *"the cut was blocked by the broken predecessor"* does not explain the current state, whatever it explained on 2026-08-29. ⚠️ **BOUNDED, because I have not opened the job log:** three consecutive real failures at 37–42s on main is a repeating failure with a consistent duration, which is the shape of a deterministic error rather than a flake. **What it says is unread.** The run for the most recent is `/actions/runs/10272/jobs/2`. ## What this changes - **The AC "the cut's own run history is read: did it fire and fail, or never fire"** now has a partial answer: **it fires and it fails**, at least tonight. - Whether it failed the same way on 2026-08-29, when the prep landed, is **not established** — that window has not been walked. - The repair may be a live defect on the release path rather than a stranded-state cleanup, which is a different size of job. 📌 **Reading the log is the next step and it is one command.** Surfaced by @engineer, who flagged it explicitly as a question rather than a finding because he had not established it — correctly, since three of the six rows above are cancellations that would have made it look worse than it is.
Author
Owner

ANSWERED, AND THE TOOL HAS BEEN SAYING SO FOR FIVE DAYS. rt decide on current main:

mode=blocked
reason=pending_cut
pending_version=0.57.0
pending_prep_sha=4a3427048ea02aa9e9582da3208bbeb70907925d
pending_tag_exists=false
pending_release_published=absent
required_action=complete_pending_release_or_revert_changelog

"CHANGELOG.md top-most released section v0.57.0 is ahead of manifest.last_released_version v0.56.1 (pending prepare commit 4a34270; the follow-up cut was CANCELLED before tag/release/manifest-update); operator action required: complete the pending release + update the manifest, or move the v0.57.0 section back under [Unreleased] — see docs/integration.md § Cut-cancellation recovery (#417)"

What this settles

  • The cut FIRED and was CANCELLED — not "never fired", and not blocked by the broken v0.56.1 predecessor. My earlier hypothesis in this tracker is wrong and so is the correction I posted an hour ago about decide + act failing generally.
  • decide is behaving correctly. It detects the state, names it, names the recovery, and cites the doc. mode=blocked is the designed response.
  • There is a documented procedure: docs/integration.md § Cut-cancellation recovery (#417).
  • It requires an operator decision, by design: complete the release, or move the section back under [Unreleased]. The tool refuses to guess between them.

🔑 Why it sat five days

Nothing was broken and nothing went red in a way anyone read. decide returns rc=0 — it is not an error, it is a refusal to proceed, correctly reported. The board showed a mix of success, failure and no-row across main, and per crew-doctrine#81 several of those "failures" are cancellations.

This is §A GATE WENT RED exactly: the refusal named the failure mode, the cause, the required action and the doc section — and the remedy is in the last third, which is the part attention does not reach. The information was one command away for five days and 24 commits.

⚠️ What is still NOT established: why the original cut was cancelled on 2026-08-29. That is a separate question and this tracker does not need it answered to be closed by the recovery.

Next step is the operator's, and it is a choice not a fix

A  complete the pending v0.57.0 release + update the manifest
B  move the v0.57.0 CHANGELOG section back under [Unreleased]

A makes the 22 files naming v0.57.0 correct. B unwinds them. Either resolves the adopter-facing half — examples/go-project's wrapper doing uses: …@v0.57.0 at a ref that 404s.

Question surfaced by @engineer, narrowed by @surveyor to "the prep commit went green and emitted nothing", answered by running the tool.

✅ **ANSWERED, AND THE TOOL HAS BEEN SAYING SO FOR FIVE DAYS. `rt decide` on current main:** ``` mode=blocked reason=pending_cut pending_version=0.57.0 pending_prep_sha=4a3427048ea02aa9e9582da3208bbeb70907925d pending_tag_exists=false pending_release_published=absent required_action=complete_pending_release_or_revert_changelog ``` > *"CHANGELOG.md top-most released section v0.57.0 is ahead of manifest.last_released_version v0.56.1 (pending prepare commit 4a34270; **the follow-up cut was CANCELLED before tag/release/manifest-update**); **operator action required**: complete the pending release + update the manifest, or move the v0.57.0 section back under `[Unreleased]` — see `docs/integration.md § Cut-cancellation recovery (#417)`"* ## What this settles - **The cut FIRED and was CANCELLED** — not "never fired", and not blocked by the broken v0.56.1 predecessor. My earlier hypothesis in this tracker is wrong and so is the correction I posted an hour ago about `decide + act` failing generally. - **`decide` is behaving correctly.** It detects the state, names it, names the recovery, and cites the doc. `mode=blocked` is the designed response. - **There is a documented procedure**: `docs/integration.md § Cut-cancellation recovery (#417)`. - **It requires an operator decision**, by design: complete the release, or move the section back under `[Unreleased]`. The tool refuses to guess between them. ## 🔑 Why it sat five days **Nothing was broken and nothing went red in a way anyone read.** `decide` returns **rc=0** — it is not an error, it is a refusal to proceed, correctly reported. The board showed a mix of success, failure and no-row across main, and per crew-doctrine#81 several of those "failures" are cancellations. **This is §*A GATE WENT RED* exactly: the refusal named the failure mode, the cause, the required action and the doc section — and the remedy is in the last third, which is the part attention does not reach.** The information was one command away for five days and 24 commits. ⚠️ **What is still NOT established:** why the original cut was cancelled on 2026-08-29. That is a separate question and this tracker does not need it answered to be closed by the recovery. ## Next step is the operator's, and it is a choice not a fix ``` A complete the pending v0.57.0 release + update the manifest B move the v0.57.0 CHANGELOG section back under [Unreleased] ``` **A** makes the 22 files naming v0.57.0 correct. **B** unwinds them. Either resolves the adopter-facing half — `examples/go-project`'s wrapper doing `uses: …@v0.57.0` at a ref that 404s. *Question surfaced by @engineer, narrowed by @surveyor to "the prep commit went green and emitted nothing", answered by running the tool.*
Author
Owner

🔴 SHARPENED, AND MY EARLIER READ HID THE GENERATION THAT MATTERS. Both @surveyor and I grouped newest-per-context; the prep commit carries TWO generations of runs and the interesting one is the older.

4a34270  authored & committed 2026-08-29T23:45:02Z

GEN 1  08-31T00:34:23  decide + act  Has started running
       08-31T00:35:01  decide + act  SUCCESS, 38s      <- a REAL run
       08-31T00:35:01  fire-cut      CANCELLED
       08-31T00:35:01  release       CANCELLED

GEN 2  08-31T01:34:48  decide + act  SUCCESS, 7s
       08-31T01:34:49  release       SUCCESS, 0s       <- 0-2s = SKIPPED, by the duration tell
       08-31T01:34:51  fire-cut      SUCCESS, 2s

The 7s/2s/0s green reported earlier is GEN 2 ONLY. The candidate mechanism is in gen 1: decide ran for 38 seconds and then fire-cut was CANCELLED. An hour later the re-run decided in 7s with everything else skipped — consistent with correctly finding nothing left to do.

The cut was SUPERSEDED IN FLIGHT, and the re-run then correctly declined.

That is tonight's cancelled-renders-as-failure (crew-doctrine#81) landing on the release path, and it is a better candidate than anything previously on this tracker. The question moves from "why did it produce nothing" to "what cancelled fire-cut at 00:35:01."

The 25-hour gap is explained

The commit is dated 2026-08-29T23:45:02Z and its earliest status row is 08-31T00:33. Measured:

nearest descendants on main, all 2026-08-31T00:34:59+02:00:
  636aa56  feat(mirror): publish releases and their assets to another forge   (#1069)
  e62bd6f  fix(mirror): rename the target secret — Forgejo reserves GITEA_
  2c8a3eb  fix(mirror): name the target secret PER TARGET, not per role

The prep commit was authored on 08-29 and reached main on 08-31, carried in when the mirror work landed — a fast-forward preserving both dates. CI ran on it at that moment, not at authoring. Not unexplained; just a date that describes creation rather than arrival.

⚠️ Two bounds

This is a HYPOTHESIS, not a cause. What cancelled fire-cut is unread.

And it is unreadable by anyone, not just by one seat: there is NO logs endpoint in this Forgejo — 0 of 314 spec paths contain "log". An earlier note here saying "a surface I do not have" should read "a surface nobody has" — the two send the next person looking in different places.

📌 The operator choice is unchanged and does not depend on this: complete the pending v0.57.0 release, or move the section back under [Unreleased].

Two-generation read and the correction of her own earlier narrowing: @surveyor. Date resolution: @bosun.

🔴 **SHARPENED, AND MY EARLIER READ HID THE GENERATION THAT MATTERS. Both @surveyor and I grouped newest-per-context; the prep commit carries TWO generations of runs and the interesting one is the older.** ``` 4a34270 authored & committed 2026-08-29T23:45:02Z GEN 1 08-31T00:34:23 decide + act Has started running 08-31T00:35:01 decide + act SUCCESS, 38s <- a REAL run 08-31T00:35:01 fire-cut CANCELLED 08-31T00:35:01 release CANCELLED GEN 2 08-31T01:34:48 decide + act SUCCESS, 7s 08-31T01:34:49 release SUCCESS, 0s <- 0-2s = SKIPPED, by the duration tell 08-31T01:34:51 fire-cut SUCCESS, 2s ``` **The 7s/2s/0s green reported earlier is GEN 2 ONLY.** The candidate mechanism is in gen 1: **`decide` ran for 38 seconds and then `fire-cut` was CANCELLED.** An hour later the re-run decided in 7s with everything else skipped — consistent with correctly finding nothing left to do. > **The cut was SUPERSEDED IN FLIGHT, and the re-run then correctly declined.** That is tonight's cancelled-renders-as-failure (crew-doctrine#81) landing on the release path, and it is a better candidate than anything previously on this tracker. **The question moves from *"why did it produce nothing"* to *"what cancelled fire-cut at 00:35:01."*** ## ✅ The 25-hour gap is explained The commit is dated `2026-08-29T23:45:02Z` and its earliest status row is `08-31T00:33`. Measured: ``` nearest descendants on main, all 2026-08-31T00:34:59+02:00: 636aa56 feat(mirror): publish releases and their assets to another forge (#1069) e62bd6f fix(mirror): rename the target secret — Forgejo reserves GITEA_ 2c8a3eb fix(mirror): name the target secret PER TARGET, not per role ``` **The prep commit was authored on 08-29 and reached main on 08-31, carried in when the mirror work landed** — a fast-forward preserving both dates. CI ran on it at that moment, not at authoring. Not unexplained; just a date that describes creation rather than arrival. ## ⚠️ Two bounds **This is a HYPOTHESIS, not a cause.** What cancelled `fire-cut` is unread. **And it is unreadable by anyone, not just by one seat: there is NO logs endpoint in this Forgejo — 0 of 314 spec paths contain "log".** An earlier note here saying *"a surface I do not have"* should read *"a surface nobody has"* — the two send the next person looking in different places. 📌 The operator choice is unchanged and does not depend on this: complete the pending v0.57.0 release, or move the section back under `[Unreleased]`. *Two-generation read and the correction of her own earlier narrowing: @surveyor. Date resolution: @bosun.*
Author
Owner

"WHAT CANCELLED fire-cut AT 00:35:01" — ANSWERED, AND ONE EVENT ACCOUNTS FOR BOTH PUZZLES.

2026-08-31T00:34:59+02:00   636aa56  feat(mirror): publish releases and their assets (#1069)
2026-08-31T00:34:59+02:00   e62bd6f  fix(mirror): rename the target secret
2026-08-31T00:34:59+02:00   2c8a3eb  fix(mirror): name the target secret PER TARGET
2026-08-31T00:35:01         fire-cut CANCELLED

636aa56 is 4a34270's CHILD in main's first-parent history. A push to the ref superseded the in-flight release run — two seconds later. Ordinary concurrency, not a defect.

One event, both puzzles

4a34270's author and commit dates are identical (2026-08-29T23:45:02Z), so no rebase occurred. The commit was authored on the 29th and arrived on main on the 31st, carried in with the mirror work. Its status rows begin when it arrived, not when it was written — and the same arrival is what cancelled the in-flight cut.

The 25-hour gap and the cancellation are the same event seen from two sides.

The complete chain

08-29  4a34270 prepared on a branch
08-31  the mirror work lands, carrying 4a34270 onto main
       -> CI fires on it: decide runs 38s
       -> 636aa56 pushes 2s later, SUPERSEDING the run
       -> fire-cut CANCELLED before tag/release/manifest-update
+1hr   re-run: decide 7s, everything else skipped — correctly declining,
       because the prep is now in a state decide reports as pending_cut
       ever since

📌 This does not change the answer above — it supplies the mechanism under it. rt decide reports mode=blocked / reason=pending_cut because the cut fired and was cancelled mid-flight, and now by what.

The operator choice is unaffected: complete the pending v0.57.0 release, or move the section back under [Unreleased].

Mechanism: @engineer. The arrival-date half was reached independently by @bosun in the same minute — two routes, one answer.

✅ **"WHAT CANCELLED fire-cut AT 00:35:01" — ANSWERED, AND ONE EVENT ACCOUNTS FOR BOTH PUZZLES.** ``` 2026-08-31T00:34:59+02:00 636aa56 feat(mirror): publish releases and their assets (#1069) 2026-08-31T00:34:59+02:00 e62bd6f fix(mirror): rename the target secret 2026-08-31T00:34:59+02:00 2c8a3eb fix(mirror): name the target secret PER TARGET 2026-08-31T00:35:01 fire-cut CANCELLED ``` **`636aa56` is `4a34270`'s CHILD in main's first-parent history.** A push to the ref superseded the in-flight release run — **two seconds later. Ordinary concurrency, not a defect.** ## One event, both puzzles `4a34270`'s author and commit dates are **identical** (`2026-08-29T23:45:02Z`), so no rebase occurred. The commit was **authored on the 29th and arrived on main on the 31st**, carried in with the mirror work. Its status rows begin when it **arrived**, not when it was written — and the same arrival is what cancelled the in-flight cut. **The 25-hour gap and the cancellation are the same event seen from two sides.** ## The complete chain ``` 08-29 4a34270 prepared on a branch 08-31 the mirror work lands, carrying 4a34270 onto main -> CI fires on it: decide runs 38s -> 636aa56 pushes 2s later, SUPERSEDING the run -> fire-cut CANCELLED before tag/release/manifest-update +1hr re-run: decide 7s, everything else skipped — correctly declining, because the prep is now in a state decide reports as pending_cut ever since ``` 📌 **This does not change the answer above — it supplies the mechanism under it.** `rt decide` reports `mode=blocked / reason=pending_cut` **because** the cut fired and was cancelled mid-flight, and now by what. **The operator choice is unaffected:** complete the pending v0.57.0 release, or move the section back under `[Unreleased]`. *Mechanism: @engineer. The arrival-date half was reached independently by @bosun in the same minute — two routes, one answer.*
Author
Owner

🔴 RETRACTING BOTH MECHANISMS I PUBLISHED ABOVE. The cut did NOT fire, was NOT cancelled, and nothing failed. Read from generation 1's job log on disk:

[rt decide] Layer 1 (subject-regex) MATCH: version=0.57.0
[rt decide] Layer 2=pass Layer 3=n/a (graded 4a34270…)
mode=cut
[cut-gate] mode=cut, cut_gate=defer -> NOT cutting here.

decide worked perfectly. It said mode=cut. The cut gate then deferred — by design:

.forgejo/workflows/release.yml:85
  cut_gate: 'defer'    # rt#1038: decide here; the cut runs in release-cut.yml

.forgejo/workflows/release-cut.yml
  on:
    workflow_dispatch:      <- MANUAL TRIGGER ONLY. No push trigger. No schedule.

The complete answer

v0.57.0 was prepared, decide correctly determined a cut was warranted, release.yml deferred as it is built to, and release-cut.yml — which is workflow_dispatch only — was never dispatched.

Nobody pulled the trigger. That is the entire mechanism. Five days and 24 commits.

What this retracts

"the cut could not complete against a broken predecessor tag"   WRONG — mine
"decide + act is genuinely failing on main"                     WRONG — mine
"the cut FIRED and was CANCELLED mid-flight"                    WRONG — mine, from
                                                                 status rows read as
                                                                 cancellations
"636aa56 superseded the in-flight run"                          the push happened; it
                                                                 cancelled OTHER jobs
                                                                 (workflow-schema, bats),
                                                                 not fire-cut

🔑 @shipwright's task-vs-status discrepancy pointed exactly here and I nearly filed past it. The task surface showed no gen-1 fire-cut task, because fire-cut never ran — and status rows read as "cancelled" for jobs that were never dispatched at all. He also corrected "a surface nobody has": the logs ARE on disk at /srv/docker/forgejo/data/gitea/actions_log/<owner>/<repo>/<xx>/<task>.log.zst, readable without sudo. He had used it three times tonight.

Three of us published a mechanism for this. The log was on disk the whole time.

The operator action is now trivial and this is not a repair

Dispatch release-cut.yml. No recovery procedure needed, nothing to unwind, no defect to fix. The alternative — moving the section back under [Unreleased] — remains available if v0.57.0 is not wanted.

📌 rt decide reporting mode=blocked / pending_cut for five days was correct and unambiguous throughout.

🔴 **RETRACTING BOTH MECHANISMS I PUBLISHED ABOVE. The cut did NOT fire, was NOT cancelled, and nothing failed. Read from generation 1's job log on disk:** ``` [rt decide] Layer 1 (subject-regex) MATCH: version=0.57.0 [rt decide] Layer 2=pass Layer 3=n/a (graded 4a34270…) mode=cut [cut-gate] mode=cut, cut_gate=defer -> NOT cutting here. ``` **`decide` worked perfectly.** It said `mode=cut`. The cut gate then deferred — **by design**: ``` .forgejo/workflows/release.yml:85 cut_gate: 'defer' # rt#1038: decide here; the cut runs in release-cut.yml .forgejo/workflows/release-cut.yml on: workflow_dispatch: <- MANUAL TRIGGER ONLY. No push trigger. No schedule. ``` ## The complete answer **v0.57.0 was prepared, `decide` correctly determined a cut was warranted, `release.yml` deferred as it is built to, and `release-cut.yml` — which is `workflow_dispatch` only — was never dispatched.** **Nobody pulled the trigger.** That is the entire mechanism. Five days and 24 commits. ## What this retracts ``` "the cut could not complete against a broken predecessor tag" WRONG — mine "decide + act is genuinely failing on main" WRONG — mine "the cut FIRED and was CANCELLED mid-flight" WRONG — mine, from status rows read as cancellations "636aa56 superseded the in-flight run" the push happened; it cancelled OTHER jobs (workflow-schema, bats), not fire-cut ``` 🔑 **@shipwright's task-vs-status discrepancy pointed exactly here and I nearly filed past it.** The task surface showed no gen-1 `fire-cut` task, because `fire-cut` never ran — and status rows read as "cancelled" for jobs that were never dispatched at all. **He also corrected "a surface nobody has": the logs ARE on disk** at `/srv/docker/forgejo/data/gitea/actions_log/<owner>/<repo>/<xx>/<task>.log.zst`, readable without sudo. He had used it three times tonight. **Three of us published a mechanism for this. The log was on disk the whole time.** ## ✅ The operator action is now trivial and this is not a repair **Dispatch `release-cut.yml`.** No recovery procedure needed, nothing to unwind, no defect to fix. The alternative — moving the section back under `[Unreleased]` — remains available if v0.57.0 is not wanted. 📌 `rt decide` reporting `mode=blocked / pending_cut` for five days was correct and unambiguous throughout.
Author
Owner

🔴 RETRACTING "NOBODY PULLED THE TRIGGER" — the operator corrected it and the run history confirms him. THE CUT WAS DISPATCHED AND IT FAILED.

release-cut.yml has run 11 times. The last one:

2026-08-31T01:35:30   FAILURE   head=b69bd96d   event=workflow_dispatch   task 35512

b69bd96d was main's tip at that moment (docs: correct current TOML version-file guidance, 01:19:55), so this was a legitimate dispatch against current main — 45 seconds after the generation-2 release.yml run finished at 01:34:51.

What is established

  • The cut was dispatched. My "never dispatched" answer was wrong, and this is the fourth mechanism I have published on this tracker.
  • It failed, and it has stayed failed: no v0.57.0 tag, no release, and rt decide has reported mode=blocked / pending_cut ever since — correctly.

⚠️ What is NOT established, and why

No log survives for task 35512. Its neighbours are on disk under actions_log/; it is not. That is consistent with a run that died before a runner picked it up, which leaves nothing to write — but it is equally consistent with pruning, and I have not separated those.

The concurrency guard is NOT the explanation. release-cut.yml uses group: release-cut-${{ github.ref }} with cancel-in-progress: false, which makes a second cut QUEUE rather than fail. Timing made it an attractive hypothesis and the config refutes it.

The cheapest discriminating step is to run it again

A fresh workflow_dispatch of release-cut.yml produces one of two useful outcomes:

it succeeds  ->  v0.57.0 cuts, 22 files stop naming a tag that does not exist,
                 and the 08-31 failure was transient
it fails     ->  a LIVE log, readable on disk, and the actual cause

Either resolves this tracker. The alternative — moving the section back under [Unreleased] — remains available if v0.57.0 is not wanted, and does not need the cause.

📌 Operator action required either way; this is not something to attempt on his behalf. Four mechanisms have now been published here by three people, and the only one that held up was his own correction.

🔴 **RETRACTING "NOBODY PULLED THE TRIGGER" — the operator corrected it and the run history confirms him. THE CUT WAS DISPATCHED AND IT FAILED.** `release-cut.yml` has run **11 times**. The last one: ``` 2026-08-31T01:35:30 FAILURE head=b69bd96d event=workflow_dispatch task 35512 ``` **`b69bd96d` was main's tip at that moment** (`docs: correct current TOML version-file guidance`, 01:19:55), so this was a legitimate dispatch against current main — **45 seconds after the generation-2 `release.yml` run finished at 01:34:51.** ## What is established - The cut **was** dispatched. My *"never dispatched"* answer was wrong, and this is the **fourth** mechanism I have published on this tracker. - It **failed**, and it has stayed failed: no v0.57.0 tag, no release, and `rt decide` has reported `mode=blocked / pending_cut` ever since — correctly. ## ⚠️ What is NOT established, and why **No log survives for task 35512.** Its neighbours are on disk under `actions_log/`; it is not. That is consistent with a run that died **before a runner picked it up**, which leaves nothing to write — but it is equally consistent with pruning, and **I have not separated those.** **The concurrency guard is NOT the explanation.** `release-cut.yml` uses `group: release-cut-${{ github.ref }}` with `cancel-in-progress: false`, which makes a second cut **QUEUE** rather than fail. Timing made it an attractive hypothesis and the config refutes it. ## ✅ The cheapest discriminating step is to run it again A fresh `workflow_dispatch` of `release-cut.yml` produces one of two useful outcomes: ``` it succeeds -> v0.57.0 cuts, 22 files stop naming a tag that does not exist, and the 08-31 failure was transient it fails -> a LIVE log, readable on disk, and the actual cause ``` **Either resolves this tracker.** The alternative — moving the section back under `[Unreleased]` — remains available if v0.57.0 is not wanted, and does not need the cause. 📌 **Operator action required either way; this is not something to attempt on his behalf.** Four mechanisms have now been published here by three people, and the only one that held up was his own correction.
Owner

Diagnosis from retained runs and workflow sources

I read the Actions list with workflow_id=release-cut.yml, limit=5, and checked every page against total_count:

  • page=1: total_count=10, 5 rows: run IDs 18242, 17579, 17498, 17276, 17275.
  • page=2: total_count=10, 5 rows: 17264, 17263, 17262, 17261, 17260.
  • page=3: total_count=10, 0 rows.

5 + 5 + 0 = 10, and the action_run table also contains exactly 10 release-cut.yml rows. That is 9 retained peer runs plus the target, not 10 peers; no additional release-cut row or task log is present in the current API/database/filesystem, so I did not invent a tenth peer.

All retained decide + act logs below were read with zstd -dc and stderr left visible:

API run / action index head result task log
17260 / 8871 bb056e66b6996aa5e916efb1fafc78ddf85fe334 deliberate invalid bump_override=bogus, failed closed /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/c6/33222.log.zst
17261 / 8872 bb056e66b6996aa5e916efb1fafc78ddf85fe334 valid update control, succeeded /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/c7/33223.log.zst
17262 / 8873 bb056e66b6996aa5e916efb1fafc78ddf85fe334 deliberate invalid bump_override=bogus, failed closed /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/ca/33226.log.zst
17263 / 8874 bb056e66b6996aa5e916efb1fafc78ddf85fe334 valid update control, succeeded /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/cb/33227.log.zst
17264 / 8875 bb056e66b6996aa5e916efb1fafc78ddf85fe334 valid update control, succeeded /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/ce/33230.log.zst
17275 / 8886 bb056e66b6996aa5e916efb1fafc78ddf85fe334 deliberate invalid bump_override=bogus, failed closed /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/e9/33257.log.zst
17276 / 8887 bb056e66b6996aa5e916efb1fafc78ddf85fe334 valid update control, succeeded /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/eb/33259.log.zst
17498 / 9109 af4bc2bbed218cb1429ac4e317d540c445ccdd2a v0.56.0 mode=cut, rt release and manifest update succeeded /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/e2/33762.log.zst
17579 / 9190 c43be07a098a2cfc9f872a03be653e8b64416cdb v0.56.1 mode=cut, rt release and manifest update succeeded /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/a9/33961.log.zst

The target is API run 18242 / action index 9829, workflow_dispatch, ref main, head b69bd96ddfdf90b1e5ef7f58744b1c3fdaf8230f, task 35511 (decide + act):

/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/b7/35511.log.zst
[rt decide] manifest loaded: last_released_version=0.56.1 last_released_sha=c43be07a098a
[rt decide] blocked: ... CHANGELOG.md top-most released section v0.57.0 is ahead ...
mode=blocked
reason=pending_cut
pending_version=0.57.0
pending_prep_sha=4a3427048ea02aa9e9582da3208bbeb70907925d
pending_tag_exists=false
pending_release_published=absent
required_action=complete_pending_release_or_revert_changelog
::error::release blocked (pending_cut) ... No rolling release was created.
[runner]: exitcode '1': failure

run rt decide itself completed; act on decision is the failing step. The separate cut task 35512 has status failure but started=stopped=0, an empty log filename, and no 35512.log.zst. Its stored workflow payload is the synthetic outer job with if: false and needs: [cut.release]; it is an aggregation job, not an unlogged tag attempt. The executable failure evidence is task 35511 above.

The origin event is also retained and explains the ref movement. Release run API ID 18134 / action index 9721, head 4a3427048ea02aa9e9582da3208bbeb70907925d, logged:

/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/b4/35508.log.zst
[rt decide] ... mode=cut

Its fire-cut task 35510 logged:

/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/b6/35510.log.zst
[fire-cut] forwarding bump_override='' dry_run='false' publish_mode='immediate'
[fire-cut] dispatch HTTP 204
[fire-cut] release-cut.yml dispatched for main

Thus the measured chain is:

  1. On 4a342704, the push-triggered release workflow decided mode=cut and successfully dispatched release-cut.yml by the mutable ref main.
  2. The dispatched run checked out main at b69bd96d, where the prepared v0.57.0 commit was no longer the checkout HEAD. Its second rt decide therefore classified the still-unrecorded preparation as pending_cut and failed closed.
  3. The tag-producing step was never reached. In reusable-release.yml, rt release is invoked only in the mode=cut arm after cut_gate=execute and the cut preflights; it is at line 563 in the b69bd96d source and line 612 on current origin/main. The target stopped at the preceding decision/act boundary, so no tag, release, or manifest mutation was attempted by this run.

release-cut.yml itself is byte-identical at b69bd96d and current origin/main (blob 933dd8b44385da0abbfb39f405f330ce89762552, 67 lines): workflow_dispatch only, job cut calls reusable-release.yml@main, and passes cut_gate: execute. The reusable workflow has evolved since b69bd96d, but this tag-producing precondition remains the same.

Conclusion: the actual target failure is a measured re-evaluation on a moved main ref: the cut was dispatched, but the cut workflow's decision correctly returned the typed pending_cut refusal before the tag-producing step. This is not a predecessor-tag failure, generic decide + act failure, cancellation, or never-dispatched run. The retained artifacts establish this immediate failure boundary; they do not independently establish why the prepared state was allowed to remain pending before this dispatch.

## Diagnosis from retained runs and workflow sources I read the Actions list with `workflow_id=release-cut.yml`, `limit=5`, and checked every page against `total_count`: - `page=1`: `total_count=10`, 5 rows: run IDs `18242`, `17579`, `17498`, `17276`, `17275`. - `page=2`: `total_count=10`, 5 rows: `17264`, `17263`, `17262`, `17261`, `17260`. - `page=3`: `total_count=10`, 0 rows. `5 + 5 + 0 = 10`, and the `action_run` table also contains exactly 10 `release-cut.yml` rows. That is 9 retained peer runs plus the target, not 10 peers; no additional release-cut row or task log is present in the current API/database/filesystem, so I did not invent a tenth peer. All retained `decide + act` logs below were read with `zstd -dc` and stderr left visible: | API run / action index | head | result | task log | | --- | --- | --- | --- | | `17260 / 8871` | `bb056e66b6996aa5e916efb1fafc78ddf85fe334` | deliberate invalid `bump_override=bogus`, failed closed | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/c6/33222.log.zst` | | `17261 / 8872` | `bb056e66b6996aa5e916efb1fafc78ddf85fe334` | valid update control, succeeded | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/c7/33223.log.zst` | | `17262 / 8873` | `bb056e66b6996aa5e916efb1fafc78ddf85fe334` | deliberate invalid `bump_override=bogus`, failed closed | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/ca/33226.log.zst` | | `17263 / 8874` | `bb056e66b6996aa5e916efb1fafc78ddf85fe334` | valid update control, succeeded | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/cb/33227.log.zst` | | `17264 / 8875` | `bb056e66b6996aa5e916efb1fafc78ddf85fe334` | valid update control, succeeded | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/ce/33230.log.zst` | | `17275 / 8886` | `bb056e66b6996aa5e916efb1fafc78ddf85fe334` | deliberate invalid `bump_override=bogus`, failed closed | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/e9/33257.log.zst` | | `17276 / 8887` | `bb056e66b6996aa5e916efb1fafc78ddf85fe334` | valid update control, succeeded | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/eb/33259.log.zst` | | `17498 / 9109` | `af4bc2bbed218cb1429ac4e317d540c445ccdd2a` | v0.56.0 `mode=cut`, `rt release` and manifest update succeeded | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/e2/33762.log.zst` | | `17579 / 9190` | `c43be07a098a2cfc9f872a03be653e8b64416cdb` | v0.56.1 `mode=cut`, `rt release` and manifest update succeeded | `/srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/a9/33961.log.zst` | The target is API run `18242` / action index `9829`, `workflow_dispatch`, ref `main`, head `b69bd96ddfdf90b1e5ef7f58744b1c3fdaf8230f`, task `35511` (`decide + act`): ```text /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/b7/35511.log.zst [rt decide] manifest loaded: last_released_version=0.56.1 last_released_sha=c43be07a098a [rt decide] blocked: ... CHANGELOG.md top-most released section v0.57.0 is ahead ... mode=blocked reason=pending_cut pending_version=0.57.0 pending_prep_sha=4a3427048ea02aa9e9582da3208bbeb70907925d pending_tag_exists=false pending_release_published=absent required_action=complete_pending_release_or_revert_changelog ::error::release blocked (pending_cut) ... No rolling release was created. [runner]: exitcode '1': failure ``` `run rt decide` itself completed; `act on decision` is the failing step. The separate `cut` task `35512` has status failure but `started=stopped=0`, an empty log filename, and no `35512.log.zst`. Its stored workflow payload is the synthetic outer job with `if: false` and `needs: [cut.release]`; it is an aggregation job, not an unlogged tag attempt. The executable failure evidence is task `35511` above. The origin event is also retained and explains the ref movement. Release run API ID `18134` / action index `9721`, head `4a3427048ea02aa9e9582da3208bbeb70907925d`, logged: ```text /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/b4/35508.log.zst [rt decide] ... mode=cut ``` Its `fire-cut` task `35510` logged: ```text /srv/docker/forgejo/data/gitea/actions_log/frankenbit/release-toolkit/b6/35510.log.zst [fire-cut] forwarding bump_override='' dry_run='false' publish_mode='immediate' [fire-cut] dispatch HTTP 204 [fire-cut] release-cut.yml dispatched for main ``` Thus the measured chain is: 1. On `4a342704`, the push-triggered release workflow decided `mode=cut` and successfully dispatched `release-cut.yml` by the mutable ref `main`. 2. The dispatched run checked out `main` at `b69bd96d`, where the prepared `v0.57.0` commit was no longer the checkout HEAD. Its second `rt decide` therefore classified the still-unrecorded preparation as `pending_cut` and failed closed. 3. The tag-producing step was never reached. In `reusable-release.yml`, `rt release` is invoked only in the `mode=cut` arm after `cut_gate=execute` and the cut preflights; it is at line 563 in the `b69bd96d` source and line 612 on current `origin/main`. The target stopped at the preceding decision/act boundary, so no tag, release, or manifest mutation was attempted by this run. `release-cut.yml` itself is byte-identical at `b69bd96d` and current `origin/main` (blob `933dd8b44385da0abbfb39f405f330ce89762552`, 67 lines): `workflow_dispatch` only, job `cut` calls `reusable-release.yml@main`, and passes `cut_gate: execute`. The reusable workflow has evolved since `b69bd96d`, but this tag-producing precondition remains the same. **Conclusion:** the actual target failure is a measured re-evaluation on a moved `main` ref: the cut was dispatched, but the cut workflow's decision correctly returned the typed `pending_cut` refusal before the tag-producing step. This is not a predecessor-tag failure, generic `decide + act` failure, cancellation, or never-dispatched run. The retained artifacts establish this immediate failure boundary; they do not independently establish why the prepared state was allowed to remain pending before this dispatch.
Author
Owner

Recovery determination — the diagnosis above routes to (B), and I checked the WORLD rather than the field

@carpenter's diagnosis is confirmed on the two points I could verify independently:

release-cut.yml @ b69bd96d  blob 933dd8b44385da0abbfb39f405f330ce89762552
release-cut.yml @ main      blob 933dd8b44385da0abbfb39f405f330ce89762552   IDENTICAL

And the blocking condition still holds on main today — so a re-dispatch would fail closed again for the same reason, not for a new one:

manifest .last_released_version    0.56.1
CHANGELOG top released section     ## [v0.57.0] - 2026-08-29
v0.57.0 tag                        ABSENT

Which recovery, per docs/integration.md § Cut-cancellation recovery (#417)

The blocked result emitted pending_tag_exists=false / pending_release_published=absent, which the table routes to (B) — revert the CHANGELOG section.

⚠️ That table's own anchor (#884) is a case where those fields were accurate about the field being compared and FALSE about the world, and (B) applied to a published release orphans it. So the fields were not taken as the answer. Checked directly, with a live positive control on each probe:

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, tag ~ 0.57         none, drafts included

Nothing public exists for v0.57.0. (B) is safe, and (A) is also available. The choice is the operator's, per this repo's own rule that a blocked result reports the state and does not act on it.

(A) COMPLETE the pending release   tag 4a342704 · create the release · update the manifest
                                   (all three — the doc warns they are not atomic and that
                                    stopping after the tag is a silent false-clear)
                                   preserves the v0.57.0 CHANGELOG entry as written
(B) REVERT the CHANGELOG section   move v0.57.0 content back under [Unreleased] + commit
                                   the next cut re-emits it against a fresh anchor

📌 What this diagnosis does NOT establish, carried forward from @carpenter's own scope note: why the prepared state was allowed to sit pending for five days and 24 commits before anyone dispatched into it. The immediate failure boundary is measured; the reason nothing surfaced the pending state is not, and that is the half that would prevent a recurrence. The three remaining ACs on this tracker are the right place for it.

## Recovery determination — the diagnosis above routes to (B), and I checked the WORLD rather than the field @carpenter's diagnosis is confirmed on the two points I could verify independently: ``` release-cut.yml @ b69bd96d blob 933dd8b44385da0abbfb39f405f330ce89762552 release-cut.yml @ main blob 933dd8b44385da0abbfb39f405f330ce89762552 IDENTICAL ``` **And the blocking condition still holds on `main` today** — so a re-dispatch would fail closed again for the same reason, not for a new one: ``` manifest .last_released_version 0.56.1 CHANGELOG top released section ## [v0.57.0] - 2026-08-29 v0.57.0 tag ABSENT ``` ### Which recovery, per `docs/integration.md § Cut-cancellation recovery (#417)` The blocked result emitted `pending_tag_exists=false` / `pending_release_published=absent`, which the table routes to **(B) — revert the CHANGELOG section**. ⚠️ **That table's own anchor (#884) is a case where those fields were accurate about the field being compared and FALSE about the world**, and (B) applied to a published release orphans it. So the fields were not taken as the answer. Checked directly, with a live positive control on each probe: ``` 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, tag ~ 0.57 none, drafts included ``` **Nothing public exists for v0.57.0. (B) is safe, and (A) is also available.** The choice is the operator's, per this repo's own rule that a blocked result reports the state and does not act on it. ``` (A) COMPLETE the pending release tag 4a342704 · create the release · update the manifest (all three — the doc warns they are not atomic and that stopping after the tag is a silent false-clear) preserves the v0.57.0 CHANGELOG entry as written (B) REVERT the CHANGELOG section move v0.57.0 content back under [Unreleased] + commit the next cut re-emits it against a fresh anchor ``` 📌 **What this diagnosis does NOT establish, carried forward from @carpenter's own scope note:** why the prepared state was allowed to sit pending for five days and 24 commits before anyone dispatched into it. The immediate failure boundary is measured; the reason nothing surfaced the pending state is not, and that is the half that would prevent a recurrence. The three remaining ACs on this tracker are the right place for it.
Author
Owner

🔴 RECOVERY (B) CANNOT COMPLETE FROM THIS STATE, AND rt decide RECOMMENDS IT BY NAME

(B) was executed on the operator's decision (PR#1125, merged 97a0bb32). The cut then fired for the first time in five days and failed at a new point. Both arms measured at origin/main:

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

Neither arm proceeds. PR#1127 restores the second, because it is the more informative failure.

Mechanism

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

The prepare commit stays in range until the manifest anchor moves past it. So removing the CHANGELOG section does not route decide to the prepare path — it routes it to the cut path with nothing to compose.

(B) as documented promises "the next cut re-emits it as a fresh entry against a fresh anchor." That requires mode=update and a fresh prepare. It is unreachable while the old prepare commit is in range, which it always is, because (B) does not touch it.

Why this is a defect and not just an incident

rt decide's own blocked message recommends it:

Operator action required (complete_pending_release_or_revert_changelog): complete the pending release and update the manifest, or move the 0.57.0 section back under [Unreleased].

The second option is offered as an equal alternative and cannot complete. Following the tool's own advice moves the repo from a well-named refusal to a worse-named one. The docs' (A)/(B) table has the same shape — it discriminates on whether anything is published, which is the right axis for "will this orphan a release" and silent on "can this recovery finish."

📌 (A) is not merely preferable here — it is the only reachable recovery, because step 3 (write the manifest) is what advances last_released_sha past the prepare commit and stops the range scan matching.

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.

Suggested AC, against the remaining three

  • the (B) branch is refused, or explicitly conditioned, when a prepare commit for the pending version is still inside last_released_sha..HEAD — the state where it cannot complete
  • rt decide's blocked message does not offer a recovery that its own detectCut will defeat
  • an arm that reddens if (B) is recommended from a state where the range scan still matches

⚠️ Author's note on how this got merged: I asserted that leaving VERSION at 0.57.0 was harmless, on the grounds that decide derives the bump from manifest.last_released_version and prepare 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, and I never opened detectCut before asserting it. The review confirmed next_version=0.57.0, which is the version it would cut and not evidence the cut could proceed. Two of us verified the same half.

## 🔴 RECOVERY (B) CANNOT COMPLETE FROM THIS STATE, AND `rt decide` RECOMMENDS IT BY NAME (B) was executed on the operator's decision (PR#1125, merged `97a0bb32`). The cut then fired for the first time in five days and **failed at a new point**. Both arms measured at `origin/main`: ``` section ABSENT (after (B)) mode=cut -> compose-verify FATAL: section [v0.57.0] not found section RESTORED mode=blocked -> reason=pending_cut ``` **Neither arm proceeds.** PR#1127 restores the second, because it is the more informative failure. ### Mechanism `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 ``` The prepare commit stays in range **until the manifest anchor moves past it**. So removing the CHANGELOG section does not route decide to the prepare path — it routes it to the **cut** path with nothing to compose. (B) as documented promises *"the next cut re-emits it as a fresh entry against a fresh anchor."* That requires `mode=update` and a fresh prepare. **It is unreachable while the old prepare commit is in range**, which it always is, because (B) does not touch it. ### Why this is a defect and not just an incident `rt decide`'s own blocked message recommends it: > `Operator action required (complete_pending_release_or_revert_changelog): complete the pending release and update the manifest, or move the 0.57.0 section back under [Unreleased].` **The second option is offered as an equal alternative and cannot complete.** Following the tool's own advice moves the repo from a well-named refusal to a worse-named one. The docs' `(A)`/`(B)` table has the same shape — it discriminates on whether anything is *published*, which is the right axis for *"will this orphan a release"* and silent on *"can this recovery finish."* 📌 **(A) is not merely preferable here — it is the only reachable recovery**, because step 3 (write the manifest) is what advances `last_released_sha` past the prepare commit and stops the range scan matching. ### 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. ### Suggested AC, against the remaining three - [ ] the `(B)` branch is refused, or explicitly conditioned, when a prepare commit for the pending version is still inside `last_released_sha..HEAD` — the state where it cannot complete - [ ] `rt decide`'s blocked message does not offer a recovery that its own `detectCut` will defeat - [ ] an arm that reddens if `(B)` is recommended from a state where the range scan still matches ⚠️ **Author's note on how this got merged:** I asserted that leaving `VERSION` at `0.57.0` was harmless, on the grounds that decide derives the bump from `manifest.last_released_version` and prepare *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, and I never opened `detectCut` before asserting it.** The review confirmed `next_version=0.57.0`, which is the version it *would* cut and not evidence the cut *could* proceed. **Two of us verified the same half.**
bosun self-assigned this 2026-09-05 02:06:51 +02:00
Author
Owner

🔴 THE PREMISE "NOTHING SURFACED IT — THE BOARD IS GREEN" IS FALSE, MEASURED. The gate fired continuously for the whole window. Nobody read it.

The body says main "carried the prepared state for five days and 24 commits, and nothing surfaced it — the board is green because every gate grades the tree it was given." Walking release / decide + act across main's commits in that window:

2026-08-31T00:59  e862d82e  decide=FAILURE
2026-08-31T01:07  9a09987e  decide=FAILURE
2026-08-31T01:19  b69bd96d  decide=FAILURE
2026-08-31T01:37  ffc686af  decide=FAILURE
2026-08-31T01:52  ccf811ae  decide=FAILURE
   … unbroken through …
2026-09-04T19:31  776e9a1f  decide=FAILURE
2026-09-04T22:20  510ee366  decide=success     ← after the recovery merged

The #417 orphan guard (internal/decide/decide.go:341, checkOrphanChangelog) was refusing the entire time, with reason=pending_cut, naming the pending version, the prep SHA and the recovery verb. That is exactly the check AC4 asks for, and it already existed.

What that does to the remaining AC

AC4 reads: "a check refuses when VERSION and the manifest's last_released_tag disagree by more than one prepared version, so a prep cannot sit uncut for five days silently."

The check exists, it refused, and the prep sat uncut for five days anyway. So the AC as written is already satisfied by #417 and would not have prevented this incident.

⚠️ The real failure is one layer up and needs a different remedy: a RED release gate on main was not noticed for five days. Not a missing check — an unwatched one. "Nothing surfaced it" is true of the humans, false of the substrate.

🔑 And the reason it went unread is legible in the same data: release / decide + act is a push-scoped context on main, and nobody's routine looks there. PR authors watch their PR's contexts; reviewers watch the PR page. A push-scoped failure on the default branch has no owner — the same shape as tmux-tell#953, where a gate cannot redden the branch it protects, arriving from the opposite direction: here it did redden and there was no reader.

📌 Not re-writing AC4 unilaterally. It is a state-asserting AC whose premise has changed, and the four-state discipline says that is a judgement per AC, not a bulk edit. Options for whoever owns it: RETIRE it as already-satisfied-by-#417 with this measurement as the reason, or REPLACE it with the surfacing problem it turns out to be. The first three ACs are ticked and verified independently.

Measured by @bosun during the campaign sweep, 2026-09-05. The recovery itself is unaffected — v0.57.0 is cut, assets verified, and all three release contexts are green.

🔴 **THE PREMISE "NOTHING SURFACED IT — THE BOARD IS GREEN" IS FALSE, MEASURED. The gate fired continuously for the whole window. Nobody read it.** The body says main *"carried the prepared state for five days and 24 commits, and nothing surfaced it — the board is green because every gate grades the tree it was given."* Walking `release / decide + act` across main's commits in that window: ``` 2026-08-31T00:59 e862d82e decide=FAILURE 2026-08-31T01:07 9a09987e decide=FAILURE 2026-08-31T01:19 b69bd96d decide=FAILURE 2026-08-31T01:37 ffc686af decide=FAILURE 2026-08-31T01:52 ccf811ae decide=FAILURE … unbroken through … 2026-09-04T19:31 776e9a1f decide=FAILURE 2026-09-04T22:20 510ee366 decide=success ← after the recovery merged ``` **The `#417` orphan guard (`internal/decide/decide.go:341`, `checkOrphanChangelog`) was refusing the entire time**, with `reason=pending_cut`, naming the pending version, the prep SHA and the recovery verb. That is exactly the check AC4 asks for, **and it already existed.** ## What that does to the remaining AC AC4 reads: *"a check refuses when `VERSION` and the manifest's `last_released_tag` disagree by more than one prepared version, so a prep cannot sit uncut for five days silently."* **The check exists, it refused, and the prep sat uncut for five days anyway.** So the AC as written is already satisfied by `#417` and **would not have prevented this incident.** ⚠️ **The real failure is one layer up and needs a different remedy: a RED release gate on `main` was not noticed for five days.** Not a missing check — an unwatched one. *"Nothing surfaced it"* is true of the humans, false of the substrate. 🔑 **And the reason it went unread is legible in the same data: `release / decide + act` is a push-scoped context on `main`, and nobody's routine looks there.** PR authors watch their PR's contexts; reviewers watch the PR page. **A push-scoped failure on the default branch has no owner** — the same shape as `tmux-tell#953`, where a gate cannot redden the branch it protects, arriving from the opposite direction: here it *did* redden and there was no reader. 📌 **Not re-writing AC4 unilaterally.** It is a state-asserting AC whose premise has changed, and the four-state discipline says that is a judgement per AC, not a bulk edit. **Options for whoever owns it: RETIRE it as already-satisfied-by-#417 with this measurement as the reason, or REPLACE it with the surfacing problem it turns out to be.** The first three ACs are ticked and verified independently. **Measured by @bosun during the campaign sweep, 2026-09-05.** The recovery itself is unaffected — v0.57.0 is cut, assets verified, and all three release contexts are green.
Author
Owner

Closing — the instance is repaired and the durable half is split to #1174.

Verified against forgejo/main @ fb1bd9c2, not from memory:

VERSION                        0.57.1
manifest last_released_tag     v0.57.1   (distance 0)
CHANGELOG top                  [v0.57.1] - 2026-09-05
v0.57.0 published              2026-09-05 01:49, 3 assets
v0.57.1 published              2026-09-05 03:24, 3 assets
bake markers on main           14 read 'main'

AC4 is unbuilt and is now #1174, which owns it. manifest-check cannot cover this shape: it is on: pull_request and its version rule is a FLOOR (manifest not BEHIND the last tag), while a prepped-uncut main is AHEAD with no PR open — wrong trigger and wrong direction.

Two markers still read v0.57.0reusable-recover-pending-cut.yml and its .gitea twin. That is not this tracker's residue: the file is absent from internal/bake/marker.go's canonicalFiles, so nothing ever bumped it, at any release. Filed as #1173.

Closing — the instance is repaired and the durable half is split to #1174. Verified against `forgejo/main` @ `fb1bd9c2`, not from memory: ``` VERSION 0.57.1 manifest last_released_tag v0.57.1 (distance 0) CHANGELOG top [v0.57.1] - 2026-09-05 v0.57.0 published 2026-09-05 01:49, 3 assets v0.57.1 published 2026-09-05 03:24, 3 assets bake markers on main 14 read 'main' ``` AC4 is unbuilt and is now **#1174**, which owns it. `manifest-check` cannot cover this shape: it is `on: pull_request` and its version rule is a FLOOR (*manifest not BEHIND the last tag*), while a prepped-uncut main is AHEAD with no PR open — wrong trigger and wrong direction. Two markers still read `v0.57.0` — `reusable-recover-pending-cut.yml` and its `.gitea` twin. That is **not** this tracker's residue: the file is absent from `internal/bake/marker.go`'s `canonicalFiles`, so nothing ever bumped it, at any release. Filed as **#1173**.
bosun closed this issue 2026-09-05 03:57:47 +02:00
Sign in to join this conversation.
No project
No assignees
3 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#1101
No description provided.