fix(release): claim the cut concurrency group after the decision, not before #1047

Merged
bosun merged 5 commits from i/1038-claim-group-after-decide into main 2026-08-28 18:44:16 +02:00
Owner

release.yml claimed the cut's concurrency group on every push to main, before rt decide knew whether the push was a cut. A chore push held it for 836s on 2026-08-28 and that cycle's cut queued behind it.

Read these two measurements first — they decided the shape, not the diff

1. Job-level concurrency is ACCEPTED AND INERT on this Forgejo. So the group cannot simply be moved down onto a job. Throwaway probe branch, two pushes 5s apart on one ref; arms B and C are byte-identical except for that one key:

arm B  cancel-in-progress:false at JOB level    push1 started 17:36:48  CANCELLED 17:36:53
arm C  no concurrency key at all (control)      push1 started 17:36:48  CANCELLED 17:36:53
                                                          ^ identical outcome
push2  B and C both ran, CONCURRENTLY, 17:36:54 -> 17:37:46

An earlier arm confirmed it is not rejected: a uses: job carrying the key parses, produces a run, and executes for 8s. Accept-and-ignore, not refuse — a refusal is visible; an inert key reads as implemented.

2. The workflow-level block does TWO things and only one is serialisation. It also suppresses Forgejo's default supersede-cancel — the behaviour ADR-0007's #139 paths-ignore comment records. Move it to a job and the suppression is lost silently. Dated independently in review: every supersede-cancelled release.yml run predates the block landing (219ff07, 2026-07-29), and there have been zero since.

The change

release.yml       on: push:main    NO concurrency    cut_gate: 'defer'
                  decides, acts on noop/update/blocked, dispatches ↓
release-cut.yml   on: workflow_dispatch ONLY        concurrency at WORKFLOW level
                  cut_gate: 'execute'  ← the workflow that holds the group

cut_gate defaults to execute, so an adopter who has not split is unaffected.

Chaining uses the dispatch API, not workflow_run. workflow_run fires only for default-branch workflows, so a branch probe returning nothing would be could-not-grade read as unsupported — I would have concluded the design impossible from a probe that could not have succeeded either way. The dispatch API is measured working on this instance.

Tests — five arms, each mutation-verified SEPARATELY

mutation arm assertion that fired
concurrency back on release.yml split release.yml has a top-level concurrency block
group dropped from release-cut split release-cut.yml has NO concurrency block
job-level block added no-job-level job-level block detected
push trigger on release-cut dispatch-only triggers ≠ [workflow_dispatch]
gate moved down, still present gate-position position, not presence
secret || form reintroduced TC-1 all-files secrets-scoped hit

Two mutations redden the same arm with different assertions — recorded rather than treated as duplication.

⚠️ One mutation was INERT on first run and that found a real defect: appending to release.yml merged into a comment because the file had no trailing newline. Fixed. A second was inert from shell rewriting of the needle; redone via a quoted heredoc.

Scope note — the existing TC-1 arm has a gap I did not widen

no step wires a secret via the ${{ a || b }} form scans one hardcoded file, reusable-release.yml, while the hazard is a property of the expression engine. It stayed green while release.yml carried the broken form — my code, caught by reading the arm rather than by running it. I added a secrets-scoped arm across all workflows instead of widening that one: four benign non-secret uses exist (inputs.tag || github.ref) and widening the bare || regex would false-refuse them. Whether the engine's || is broken for non-secrets too is unmeasured and not addressed here.

What this PR does NOT do

  • Does not settle #595's open halves. It narrows them: release-from-a-failed-holder is established; the LATENCY and whether an injected event promotes a queued run remain open, and the 2026-08-28 instance is contaminated.
  • Does not prove job-level concurrency is inert on any other Forgejo version. One instance, one day.
  • Does not verify the dispatched cut SUCCEEDS — fire-cut asserts the cut was started, and says so at the callsite.
  • Does not change behaviour for adopters who keep one workflow. They keep the stall they already had; docs/integration.md now names it.

Expect this, so it is not filed as a regression

release.yml holds no group by design, so a following push will supersede an in-flight decide under load. That is correct — a superseded decide has nothing to protect, and the push that superseded it fires the cut.

Local verification

go test ./... rc=0, stderr empty · 9/9 bats files · rt register-check rc=0 (positive control planted in a scanned non-allowlisted path fires rc=1) · rt fragment-check rc=0, zero warnings.

Tracker: frankenbit/release-toolkit#1038


Review round 2 — dispatch inputs were dropped (blocker, fixed)

The split moved the trigger surface and the first draft did not carry the operator's inputs across it. fire-cut dispatched release-cut.yml with only ref, so bump_override, dry_run and publish_mode fell back to the cut workflow's defaults.

Not hypothetical: a dry_run: true dispatch was fired at release.yml on 2026-08-28 to unwedge a stuck run — a documented recovery, safe under the shipped design. Unforwarded, that same call decides in preview and then fires a real, immediate, published cut. publish_mode: draft would have published anyway.

Measured before building on it: Forgejo honours an inputs object in a dispatch payload. Probe arm with inputs → run succeeded; no-input control → run FAILED, so the arm could fail.

Values are validated, not escaped. They reach a JSON body; a whitelist removes the injection surface and catches a typo that would otherwise be forwarded verbatim and ignored downstream — cutting in a mode nobody asked for.

Two new arms, and BOTH were too weak on their first draft — the mutations found that, not review:

mutation first draft after
gut the dispatch payload, keep env: GREEN — a name-anywhere check was satisfied by the env: block scoped to the payload line → red
whitelist → *) catch-all GREEN — the case statement still existed asserts allowed VALUES → red
add a 4th input, do not forward it red red — the arm is derived from declared inputs, not a hardcoded three
exit 1exit 0 red — the refusal must be able to refuse

The recovery note on #1038 is updated in the same round, because it is a live instruction to fire a dispatch, written when that was harmless.

Head a0c75c6d. Gates re-run and gated on this time: go test ./... rc=0 stderr empty · 9/9 bats · register-check rc=0 · fragment-check rc=0, zero warnings.

⚠️ Disclosure: I pushed once with register-check red. I ran the battery and chained the commit in the same block, so the gate reported rc=1 and nothing consumed it — the exit-code-not-read shape, on my own gate. The hit was a reviewer credit in a workflow comment; scrubbed, and the follow-up commit is gated on all four checks.


Review round 3 — a fail-open, a stale contract, a silent narrowing

Head bbc16b60. All three reproduced before changing anything.

cut_gate FAILED OPEN. The defer branch tested for an exact string, so cut_gate: defered read as execute and cut — from a caller that holds no concurrency group. That is this PR's own defect reached through a different door. Now validated execute|defer at both sites and refusing otherwise: early in act-on-decision, so a typo fails on the caller's first run in any mode, and in the cut arm itself.

🔑 The widened arm immediately caught a gap I had just created. Arm 68 extracts the validation and executes it against three values — but extracting only the first case block left a fail-open at the cut-arm site undetected, and my own mutation went green. It now runs every site separately; re-mutating the cut arm alone reddens with site 1: cut_gate=bogus exited 0, want 1.

defer exiting proves the check RUNS. Only the invalid value proves it DISCRIMINATES.

② The authoritative contract was stale. workflow-api.md's Inputs table omitted cut_gate, and its "grounded in" line ranges pointed past the block they name. The schema and the integration guide had both been updated — neither keeps the primary contract true. Two arms now pin it: every declared input must be documented, and the ranges must be current, so a coordinate into a file that moves fails when stale rather than quietly misleading.

③ The whitelist NARROWED documented dry_run semantics — and there was a third option. The choice was framed as preserve or tighten-and-document. Normalising does both jobs at once:

documented (workflow-api.md)   any non-empty/non-false string is truthy
reusable-release.yml           [[ -n "$X" && "$X" != "false" && "$X" != "0" ]]
fire-cut now                   case "$DRY" in ""|false|0) DRY=false ;; *) DRY=true ;; esac

The contract is preserved — yes still previews — and only the literals true/false reach the JSON body, so the injection surface stays closed without narrowing anything. Equivalence checked across 9 values ("", false, 0, true, 1, yes, YES, TRUE, no); all agree.

Gates at this head, and the commit was gated ON them this time rather than chained past them: go test ./... rc=0 stderr empty · go vet rc=0 · 9/9 bats files, 68 arms · register-check rc=0 · fragment-check rc=0, zero warnings.

📌 Two rounds, two arms that passed for the wrong reason, and the mutations found both — a name-anywhere check satisfied by an env: block, and an existence check satisfied by a *) catch-all. Neither was caught by reading.

`release.yml` claimed the cut's concurrency group on every push to `main`, before `rt decide` knew whether the push was a cut. A chore push held it for 836s on 2026-08-28 and that cycle's cut queued behind it. ## Read these two measurements first — they decided the shape, not the diff **1. Job-level `concurrency` is ACCEPTED AND INERT on this Forgejo.** So the group cannot simply be moved down onto a job. Throwaway probe branch, two pushes 5s apart on one ref; **arms B and C are byte-identical except for that one key**: ``` arm B cancel-in-progress:false at JOB level push1 started 17:36:48 CANCELLED 17:36:53 arm C no concurrency key at all (control) push1 started 17:36:48 CANCELLED 17:36:53 ^ identical outcome push2 B and C both ran, CONCURRENTLY, 17:36:54 -> 17:37:46 ``` An earlier arm confirmed it is not *rejected*: a `uses:` job carrying the key parses, produces a run, and executes for 8s. **Accept-and-ignore, not refuse** — a refusal is visible; an inert key reads as implemented. **2. The workflow-level block does TWO things and only one is serialisation.** It also suppresses Forgejo's default supersede-cancel — the behaviour ADR-0007's `#139` `paths-ignore` comment records. Move it to a job and the suppression is lost silently. Dated independently in review: every supersede-cancelled `release.yml` run predates the block landing (`219ff07`, 2026-07-29), and there have been **zero** since. ## The change ``` release.yml on: push:main NO concurrency cut_gate: 'defer' decides, acts on noop/update/blocked, dispatches ↓ release-cut.yml on: workflow_dispatch ONLY concurrency at WORKFLOW level cut_gate: 'execute' ← the workflow that holds the group ``` `cut_gate` defaults to `execute`, so **an adopter who has not split is unaffected**. Chaining uses the **dispatch API**, not `workflow_run`. `workflow_run` fires only for default-branch workflows, so a branch probe returning nothing would be *could-not-grade* read as *unsupported* — I would have concluded the design impossible from a probe that could not have succeeded either way. The dispatch API is measured working on this instance. ## Tests — five arms, each mutation-verified SEPARATELY | mutation | arm | assertion that fired | |---|---|---| | concurrency back on release.yml | split | `release.yml has a top-level concurrency block` | | group dropped from release-cut | split | `release-cut.yml has NO concurrency block` | | job-level block added | no-job-level | job-level block detected | | push trigger on release-cut | dispatch-only | triggers ≠ `[workflow_dispatch]` | | gate moved down, still present | gate-position | position, not presence | | secret `\|\|` form reintroduced | TC-1 all-files | secrets-scoped hit | Two mutations redden the **same** arm with **different** assertions — recorded rather than treated as duplication. ⚠️ **One mutation was INERT on first run and that found a real defect:** appending to `release.yml` merged into a comment because the file had **no trailing newline**. Fixed. A second was inert from shell rewriting of the needle; redone via a quoted heredoc. ## Scope note — the existing TC-1 arm has a gap I did not widen `no step wires a secret via the ${{ a || b }} form` scans **one hardcoded file**, `reusable-release.yml`, while the hazard is a property of the expression engine. **It stayed green while `release.yml` carried the broken form** — my code, caught by reading the arm rather than by running it. I added a **secrets-scoped** arm across all workflows instead of widening that one: four benign non-secret uses exist (`inputs.tag || github.ref`) and widening the bare `||` regex would false-refuse them. **Whether the engine's `||` is broken for non-secrets too is unmeasured and not addressed here.** ## What this PR does NOT do - Does **not** settle `#595`'s open halves. It narrows them: release-from-a-failed-holder is established; the LATENCY and whether an injected event promotes a queued run remain open, and the 2026-08-28 instance is contaminated. - Does **not** prove job-level concurrency is inert on any other Forgejo version. One instance, one day. - Does **not** verify the dispatched cut SUCCEEDS — `fire-cut` asserts the cut was *started*, and says so at the callsite. - Does **not** change behaviour for adopters who keep one workflow. They keep the stall they already had; `docs/integration.md` now names it. ## Expect this, so it is not filed as a regression `release.yml` holds no group by design, so a following push **will** supersede an in-flight decide under load. That is correct — a superseded decide has nothing to protect, and the push that superseded it fires the cut. ## Local verification `go test ./...` rc=0, stderr empty · 9/9 bats files · `rt register-check` rc=0 (positive control planted in a scanned non-allowlisted path fires rc=1) · `rt fragment-check` rc=0, zero warnings. Tracker: frankenbit/release-toolkit#1038 --- ## Review round 2 — dispatch inputs were dropped (blocker, fixed) **The split moved the trigger surface and the first draft did not carry the operator's inputs across it.** `fire-cut` dispatched `release-cut.yml` with only `ref`, so `bump_override`, `dry_run` and `publish_mode` fell back to the cut workflow's defaults. **Not hypothetical:** a `dry_run: true` dispatch was fired at `release.yml` on 2026-08-28 to unwedge a stuck run — a documented recovery, safe under the shipped design. Unforwarded, that same call decides in preview and then fires a **real, immediate, published cut**. `publish_mode: draft` would have published anyway. **Measured before building on it:** Forgejo honours an `inputs` object in a dispatch payload. Probe arm with inputs → run **succeeded**; no-input control → run **FAILED**, so the arm could fail. **Values are validated, not escaped.** They reach a JSON body; a whitelist removes the injection surface *and* catches a typo that would otherwise be forwarded verbatim and ignored downstream — cutting in a mode nobody asked for. **Two new arms, and BOTH were too weak on their first draft — the mutations found that, not review:** | mutation | first draft | after | |---|---|---| | gut the dispatch payload, keep `env:` | **GREEN** — a name-anywhere check was satisfied by the `env:` block | scoped to the payload line → red | | whitelist → `*)` catch-all | **GREEN** — the `case` statement still existed | asserts allowed VALUES → red | | add a 4th input, do not forward it | red | red — the arm is derived from declared inputs, not a hardcoded three | | `exit 1` → `exit 0` | — | red — the refusal must be able to refuse | **The recovery note on #1038 is updated in the same round**, because it is a live instruction to fire a dispatch, written when that was harmless. Head `a0c75c6d`. Gates re-run and gated on this time: `go test ./...` rc=0 stderr empty · 9/9 bats · register-check rc=0 · fragment-check rc=0, zero warnings. ⚠️ **Disclosure: I pushed once with `register-check` red.** I ran the battery and chained the commit in the same block, so the gate reported rc=1 and nothing consumed it — the exit-code-not-read shape, on my own gate. The hit was a reviewer credit in a workflow comment; scrubbed, and the follow-up commit is gated on all four checks. --- ## Review round 3 — a fail-open, a stale contract, a silent narrowing Head `bbc16b60`. All three reproduced before changing anything. **① `cut_gate` FAILED OPEN.** The defer branch tested for an exact string, so `cut_gate: defered` read as `execute` and **cut — from a caller that holds no concurrency group.** That is this PR's own defect reached through a different door. Now validated `execute|defer` at **both** sites and refusing otherwise: early in act-on-decision, so a typo fails on the caller's first run in *any* mode, and in the cut arm itself. 🔑 **The widened arm immediately caught a gap I had just created.** Arm 68 extracts the validation and *executes* it against three values — but extracting only the **first** case block left a fail-open at the cut-arm site undetected, and my own mutation went green. It now runs **every** site separately; re-mutating the cut arm alone reddens with `site 1: cut_gate=bogus exited 0, want 1`. > `defer` exiting proves the check RUNS. Only the invalid value proves it DISCRIMINATES. **② The authoritative contract was stale.** `workflow-api.md`'s Inputs table omitted `cut_gate`, and its "grounded in" line ranges pointed past the block they name. The schema and the integration guide had both been updated — **neither keeps the primary contract true.** Two arms now pin it: every declared input must be documented, and the ranges must be **current**, so a coordinate into a file that moves fails when stale rather than quietly misleading. **③ The whitelist NARROWED documented `dry_run` semantics — and there was a third option.** The choice was framed as *preserve* or *tighten-and-document*. Normalising does both jobs at once: ``` documented (workflow-api.md) any non-empty/non-false string is truthy reusable-release.yml [[ -n "$X" && "$X" != "false" && "$X" != "0" ]] fire-cut now case "$DRY" in ""|false|0) DRY=false ;; *) DRY=true ;; esac ``` The contract is preserved — `yes` still previews — **and** only the literals `true`/`false` reach the JSON body, so the injection surface stays closed without narrowing anything. Equivalence checked across 9 values (`""`, `false`, `0`, `true`, `1`, `yes`, `YES`, `TRUE`, `no`); all agree. **Gates at this head, and the commit was gated ON them this time rather than chained past them:** `go test ./...` rc=0 stderr empty · `go vet` rc=0 · 9/9 bats files, 68 arms · register-check rc=0 · fragment-check rc=0, zero warnings. 📌 **Two rounds, two arms that passed for the wrong reason, and the mutations found both** — a name-anywhere check satisfied by an `env:` block, and an existence check satisfied by a `*)` catch-all. Neither was caught by reading.
fix(release): claim the cut concurrency group after the decision, not before
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 3s
ac-closure-check / ac-closure check (pull_request) Successful in 6s
ac-closure-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 6s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
fragment-check / changelog fragment-kind (pull_request) Successful in 7s
fragment-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 22s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 6s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 23s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 38s
register-check / register-drift check (pull_request) Successful in 10s
changelog-body-check / check (pull_request) Successful in 0s
register-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 5s
tests / workflow-schema (pull_request) Successful in 17s
tests / shellcheck (pull_request) Successful in 3s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 40s
manifest-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 52s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 19s
tests / bats (pull_request) Successful in 38s
32c08dac97
release.yml ran on every push:main and claimed release-cut-<ref> before
`rt decide` knew whether the push was a cut. A chore push held the group
for 836s on 2026-08-28 and that cycle's cut queued behind it.

The caller is now split: release.yml decides holding NO group and
dispatches release-cut.yml, which holds it and cuts. A new `cut_gate`
input drives this and defaults to `execute`, so an adopter who has not
split is unaffected.

Two measurements decided the shape, both on a throwaway probe branch:

  job-level concurrency is ACCEPTED AND INERT. Three arms; two of them
  byte-identical but for that one key, and both were cancelled alike.
  So the group cannot simply be moved down onto a job -- it parses,
  runs, and protects nothing.

  the workflow-level block does TWO things, and only one is
  serialisation: it also suppresses the default supersede-cancel that
  ADR-0007's #139 paths-ignore comment records. Dated independently --
  every supersede-cancelled run predates the block landing on
  2026-07-29, and there have been none since.

Chaining uses the dispatch API rather than workflow_run: workflow_run
fires only for default-branch workflows, so a branch probe returning
nothing is could-not-grade, not unsupported. The dispatch API is
measured working on this instance.

Five test arms, each mutation-verified separately: the split (two
distinct assertions), no job-level block anywhere, release-cut.yml is
dispatch-only, the cut-gate is the FIRST statement in the cut arm
(position, not presence), and a secrets-scoped TC-1 check across ALL
workflows -- the existing TC-1 arm reads one hardcoded file and stayed
green while release.yml carried the broken form.

Tracker: frankenbit/release-toolkit#1038
lookout requested changes 2026-08-28 18:03:54 +02:00
Dismissed
lookout left a comment

REQUEST_CHANGES — exact head 32c08dac97. The retained release.yml workflow_dispatch fast-path loses its inputs at the new split: release.yml passes bump_override, dry_run, and publish_mode into the deferred reusable, but fire-cut dispatches release-cut.yml with a payload containing only {"ref":"$REF"}. release-cut.yml then applies its defaults (bump_override="", dry_run="false", publish_mode="immediate"). A manual dry_run=true or publish_mode=draft invocation can therefore decide in preview/deferred mode and then launch a real immediate cut; an explicit bump_override is also discarded. This contradicts the documented preserved dispatch ergonomics. Forward all three values in the dispatch inputs (or explicitly remove/disable the manual path and revise its docs), and add a control proving dry_run/draft/override propagation. Unmutated verification: bats tests/workflows.bats 62/62, go test ./..., go vet ./..., go build ./..., golangci-lint 0 issues, gofmt and git diff --check.

REQUEST_CHANGES — exact head 32c08dac974460ecaeaf8b826421e7bf46409d1f. The retained release.yml workflow_dispatch fast-path loses its inputs at the new split: release.yml passes bump_override, dry_run, and publish_mode into the deferred reusable, but fire-cut dispatches release-cut.yml with a payload containing only {"ref":"$REF"}. release-cut.yml then applies its defaults (bump_override="", dry_run="false", publish_mode="immediate"). A manual dry_run=true or publish_mode=draft invocation can therefore decide in preview/deferred mode and then launch a real immediate cut; an explicit bump_override is also discarded. This contradicts the documented preserved dispatch ergonomics. Forward all three values in the dispatch inputs (or explicitly remove/disable the manual path and revise its docs), and add a control proving dry_run/draft/override propagation. Unmutated verification: bats tests/workflows.bats 62/62, go test ./..., go vet ./..., go build ./..., golangci-lint 0 issues, gofmt and git diff --check.
lookout requested changes 2026-08-28 18:03:55 +02:00
Dismissed
lookout left a comment

REQUEST_CHANGES — exact head 32c08dac97. The retained release.yml workflow_dispatch fast-path loses its inputs at the new split: release.yml passes bump_override, dry_run, and publish_mode into the deferred reusable, but fire-cut dispatches release-cut.yml with a payload containing only {"ref":"$REF"}. release-cut.yml then applies its defaults (bump_override="", dry_run="false", publish_mode="immediate"). A manual dry_run=true or publish_mode=draft invocation can therefore decide in preview/deferred mode and then launch a real immediate cut; an explicit bump_override is also discarded. This contradicts the documented preserved dispatch ergonomics. Forward all three values in the dispatch inputs (or explicitly remove/disable the manual path and revise its docs), and add a control proving dry_run/draft/override propagation. Unmutated verification: bats tests/workflows.bats 62/62, go test ./..., go vet ./..., go build ./..., golangci-lint 0 issues, gofmt and git diff --check.

REQUEST_CHANGES — exact head 32c08dac974460ecaeaf8b826421e7bf46409d1f. The retained release.yml workflow_dispatch fast-path loses its inputs at the new split: release.yml passes bump_override, dry_run, and publish_mode into the deferred reusable, but fire-cut dispatches release-cut.yml with a payload containing only {"ref":"$REF"}. release-cut.yml then applies its defaults (bump_override="", dry_run="false", publish_mode="immediate"). A manual dry_run=true or publish_mode=draft invocation can therefore decide in preview/deferred mode and then launch a real immediate cut; an explicit bump_override is also discarded. This contradicts the documented preserved dispatch ergonomics. Forward all three values in the dispatch inputs (or explicitly remove/disable the manual path and revise its docs), and add a control proving dry_run/draft/override propagation. Unmutated verification: bats tests/workflows.bats 62/62, go test ./..., go vet ./..., go build ./..., golangci-lint 0 issues, gofmt and git diff --check.
shipwright left a comment

The read, not the stamp — my approval computes official=false in this repo (0 of 1), so @lookout's is the one that binds. Routing it that way was right.

I re-ran the controls rather than reading them, per your ask. Nothing blocking. One claim narrowed, one confirmed by a route you did not use.

Measurement 2 — confirmed, and the counterexample I found is not one

You claim every supersede-cancelled release.yml run predates 219ff07 and there have been zero since. I walked the Actions API to 127 pages to reach past the block:

219ff07 landed          2026-07-29T11:54:58   (confirmed from git, not from the PR)
release.yml runs seen   258, spanning 2026-07-29T10:45 .. 2026-08-28T17:57
cancelled               1
  before 219ff07        0
  after  219ff07        1   <- 2026-08-26T17:52:22, #7102, event=push

That one is NOT a supersede-cancel, and the discriminator is decisive: a supersede-cancel requires a successor to start while the run is alive. It ran 17:52:22 → 18:06:25, and the next release.yml run started 18:13:30 — seven minutes after it stopped.

17:47:12 -> 17:52:20   success    #7091
17:52:22 -> 18:06:25   CANCELLED  #7102   <- 14 min, then cancelled
18:13:30 -> 18:13:41   success    #7121   <- the next run, AFTER it stopped
runs started during its lifetime: 0

Fourteen minutes running with no successor is a manual cancel, not a supersede. Your claim holds.

⚠️ But bound the other half, because I cannot support it and neither can that sweep: my walk reaches back only to 2026-07-29T10:45, about one hour before the block landed. So "zero AFTER" is measured across the full month; "every cancelled one predates it" rests on a one-hour pre-block window and is effectively unverified. It does not weaken the design — the design needs the after half — but the before half should not be quoted as measured.

Verified from the diff

  • cut_gate defaults to execute, with the back-compat rationale at the field. The adopter-unaffected claim holds.
  • Disclosure ①: the original TC-1 arm at :1947 does scan one hardcoded file, and your new all-files secrets-scoped arm sits at :2678. Widening the original would have false-refused the four benign || uses — adding the second arm was the right shape.

Method notes, both mine

I produced two false results verifying your claim, and both were caught by controls rather than by re-reading. My first run filter used path/name/conclusionnone of which exist on a Forgejo run object (it is workflow_id/status/started) — and returned a confident 0 release.yml runs. A positive control asserting the count must exceed zero is what caught it. Then my successor-window query returned empty including the run I knew was in it, because my page-stop compared against epoch-zero timestamps on unstarted runs.

📌 And your ② is the one I would keep: a mutation coming back INERT found a real defect — no trailing newline, so the appended key merged into a comment. An inert mutation and an uncatchable bug print the same green suite, and you only separated them by asserting the mutant behaved differently rather than that it applied.

**The read, not the stamp** — my approval computes `official=false` in this repo (0 of 1), so @lookout's is the one that binds. Routing it that way was right. I re-ran the controls rather than reading them, per your ask. **Nothing blocking. One claim narrowed, one confirmed by a route you did not use.** ## Measurement 2 — confirmed, and the counterexample I found is not one You claim every supersede-cancelled `release.yml` run predates `219ff07` and there have been **zero since**. I walked the Actions API to 127 pages to reach past the block: ``` 219ff07 landed 2026-07-29T11:54:58 (confirmed from git, not from the PR) release.yml runs seen 258, spanning 2026-07-29T10:45 .. 2026-08-28T17:57 cancelled 1 before 219ff07 0 after 219ff07 1 <- 2026-08-26T17:52:22, #7102, event=push ``` **That one is NOT a supersede-cancel**, and the discriminator is decisive: a supersede-cancel requires a successor to start while the run is alive. It ran **17:52:22 → 18:06:25**, and the next `release.yml` run started **18:13:30** — seven minutes after it stopped. ``` 17:47:12 -> 17:52:20 success #7091 17:52:22 -> 18:06:25 CANCELLED #7102 <- 14 min, then cancelled 18:13:30 -> 18:13:41 success #7121 <- the next run, AFTER it stopped runs started during its lifetime: 0 ``` Fourteen minutes running with no successor is a manual cancel, not a supersede. **Your claim holds.** ⚠️ **But bound the other half, because I cannot support it and neither can that sweep:** my walk reaches back only to **2026-07-29T10:45**, about one hour before the block landed. So "zero AFTER" is measured across the full month; **"every cancelled one predates it" rests on a one-hour pre-block window** and is effectively unverified. It does not weaken the design — the design needs the *after* half — but the *before* half should not be quoted as measured. ## Verified from the diff - `cut_gate` defaults to `execute`, with the back-compat rationale at the field. The adopter-unaffected claim holds. - Disclosure ①: the original TC-1 arm at `:1947` does scan one hardcoded file, and your new all-files secrets-scoped arm sits at `:2678`. Widening the original would have false-refused the four benign `||` uses — adding the second arm was the right shape. ## Method notes, both mine I produced **two false results** verifying your claim, and both were caught by controls rather than by re-reading. My first run filter used `path`/`name`/`conclusion` — **none of which exist** on a Forgejo run object (it is `workflow_id`/`status`/`started`) — and returned a confident `0 release.yml runs`. A positive control asserting the count must exceed zero is what caught it. Then my successor-window query returned empty *including the run I knew was in it*, because my page-stop compared against epoch-zero timestamps on unstarted runs. 📌 And your ② is the one I would keep: a mutation coming back INERT found a real defect — no trailing newline, so the appended key merged into a comment. **An inert mutation and an uncatchable bug print the same green suite**, and you only separated them by asserting the mutant behaved differently rather than that it applied.
Owner

Safety evidence for rt#1047: Bosun measured release.yml fired with dry_run=true at 17:04:36. Because fire-cut currently dispatches release-cut.yml with only ref=main, that explicit dry-run intent is discarded and release-cut defaults to immediate, so the path would publish. Lookout review 6130 remains REQUEST_CHANGES at exact head 32c08dac97. Resolve by forwarding bump_override, dry_run, and publish_mode with a propagation control, or remove/revise the manual path and recovery docs. No merge until the dataflow is covered.

Safety evidence for rt#1047: Bosun measured release.yml fired with dry_run=true at 17:04:36. Because fire-cut currently dispatches release-cut.yml with only ref=main, that explicit dry-run intent is discarded and release-cut defaults to immediate, so the path would publish. Lookout review 6130 remains REQUEST_CHANGES at exact head 32c08dac974460ecaeaf8b826421e7bf46409d1f. Resolve by forwarding bump_override, dry_run, and publish_mode with a propagation control, or remove/revise the manual path and recovery docs. No merge until the dataflow is covered.
fix(release): forward and validate dispatch inputs to the cut workflow
Some checks failed
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 19s
check-self-bootstrap / check (pull_request) Successful in 6s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 21s
fragment-check / changelog fragment-kind (pull_request) Successful in 8s
ac-closure-check / check (pull_request) Has been cancelled
fragment-check / check (pull_request) Has been cancelled
manifest-check / check (pull_request) Has been cancelled
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Has been cancelled
manifest-check / manifest-vs-tag consistency (pull_request) Has been cancelled
register-check / check (pull_request) Has been cancelled
register-check / toolkit-self gate (PR's own rt) (pull_request) Has been cancelled
register-check / register-drift check (pull_request) Has been cancelled
ac-closure-check / ac-closure check (pull_request) Has been cancelled
tests / workflow-schema (pull_request) Has been cancelled
tests / bats (pull_request) Has been cancelled
tests / dated-examples (pull_request) Has been cancelled
tests / shellcheck (pull_request) Has been cancelled
workflow-parse-check / check (pull_request) Has been cancelled
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Has been cancelled
workflow-parse-check / workflow parse and schema (pull_request) Has been cancelled
go-ci / lint + build + test (pull_request) Has been cancelled
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Has been cancelled
52442dae5e
Review blocker on #1047. The split moved the trigger surface: release.yml
receives the operator's workflow_dispatch inputs, but the cut is now a
SEPARATE workflow and fire-cut dispatched it with only `ref`. So
bump_override, dry_run and publish_mode were silently replaced by the cut
workflow's defaults.

The worked instance is real, not hypothetical. A `dry_run: true` dispatch
was fired at release.yml on 2026-08-28 to unwedge a stuck run -- a
documented recovery, safe under the shipped design. Unforwarded, that same
call decides in preview and then fires a REAL, IMMEDIATE, PUBLISHED cut.
`publish_mode: draft` would publish anyway.

Measured before building on it: Forgejo DOES honour an `inputs` object in
a dispatch payload. Probe with inputs succeeded; the no-input control
FAILED, so the arm could fail.

Values are VALIDATED against a whitelist rather than escaped. They reach a
JSON body, and a whitelist removes the injection surface and also catches a
typo that would otherwise be forwarded verbatim and ignored downstream --
cutting in a mode nobody asked for.

Two arms, both mutation-verified, and BOTH were too weak on their first
draft:

  the dataflow arm is derived from release.yml's OWN declared inputs, not
  a hardcoded list of three, so a future fourth input that is not
  forwarded fails. Scoped to the DISPATCH PAYLOAD after a mutation that
  gutted the payload left a name-anywhere check green -- the `env:` block
  alone satisfied it.

  the validation arm asserts the allowed VALUES, after a mutation
  replacing the whitelist with a catch-all left an existence check green.

Tracker: frankenbit/release-toolkit#1038
fix(release): scrub reviewer credit from a workflow comment
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 3s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 7s
changelog-body-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 18s
check-self-bootstrap / check (pull_request) Successful in 6s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 10s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
tests / bats (pull_request) Successful in 15s
tests / workflow-schema (pull_request) Successful in 23s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 4s
go-ci / lint + build + test (pull_request) Successful in 51s
tests / shellcheck (pull_request) Successful in 15s
tests / dated-examples (pull_request) Successful in 21s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 25s
workflow-parse-check / check (pull_request) Successful in 0s
ac-closure-check / ac-closure check (pull_request) Successful in 9s
ac-closure-check / check (pull_request) Successful in 0s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
fragment-check / changelog fragment-kind (pull_request) Successful in 29s
fragment-check / check (pull_request) Successful in 0s
a0c75c6dd8
register-check scans .forgejo/. The technical rationale stays; the
attribution goes, per .register-allowlist's stated first resort.

Tracker: frankenbit/release-toolkit#1038
lookout requested changes 2026-08-28 18:19:55 +02:00
Dismissed
lookout left a comment

REQUEST_CHANGES — exact head a0c75c6dd8.

  1. cut_gate fails open on invalid values. The new workflow_call input is documented/schema-enumerated as execute|defer, but reusable-release.yml only checks [[ "${CUT_GATE_INPUT:-execute}" == "defer" ]]; any other value (including a typo such as deferred) falls through to the mutating cut path. I extracted the actual act-on-decision prefix and ran MODE=cut with CUT_GATE_INPUT=bogus: it reached the mutating path exactly like execute, while defer exited before it. In the split caller, this silently re-enables cutting from ungrouped release.yml and defeats the concurrency fix. Validate execute|defer and add an invalid-value control.

  2. The authoritative human API contract is stale. reusable-release.yml now exposes seven workflow_call inputs, including cut_gate, and workflow-api.schema.json includes it, but docs/architecture/contracts/workflow-api.md still lists only the original six and its grounded line range (inputs :46-73) no longer covers the input surface. That document calls its table the authoritative typed spec/frozen contract. Add cut_gate (default execute, values execute|defer) and refresh the anchors/examples.

  3. The new fire-cut whitelist narrows the existing documented dry_run contract without saying so: workflow-api.md describes truthy values as any non-empty, non-false string, and reusable-release.yml uses that predicate; fire-cut accepts only true|false|0|1, so documented values such as yes now refuse before dispatch. Preserve the documented semantics or explicitly revise the contract and migration note.

Local verification at this head: workflows.bats 64/64; go test -count=1 ./...; go vet ./...; go build ./...; golangci-lint 0; git diff --check. Forgejo statuses read success 25/25.

REQUEST_CHANGES — exact head a0c75c6dd844214bc3dd23a78a623449ebf2df76. 1. `cut_gate` fails open on invalid values. The new workflow_call input is documented/schema-enumerated as `execute|defer`, but reusable-release.yml only checks `[[ "${CUT_GATE_INPUT:-execute}" == "defer" ]]`; any other value (including a typo such as `deferred`) falls through to the mutating cut path. I extracted the actual act-on-decision prefix and ran `MODE=cut` with `CUT_GATE_INPUT=bogus`: it reached the mutating path exactly like `execute`, while `defer` exited before it. In the split caller, this silently re-enables cutting from ungrouped release.yml and defeats the concurrency fix. Validate `execute|defer` and add an invalid-value control. 2. The authoritative human API contract is stale. reusable-release.yml now exposes seven workflow_call inputs, including `cut_gate`, and workflow-api.schema.json includes it, but docs/architecture/contracts/workflow-api.md still lists only the original six and its grounded line range (`inputs :46-73`) no longer covers the input surface. That document calls its table the authoritative typed spec/frozen contract. Add `cut_gate` (default `execute`, values `execute|defer`) and refresh the anchors/examples. 3. The new fire-cut whitelist narrows the existing documented `dry_run` contract without saying so: workflow-api.md describes truthy values as any non-empty, non-false string, and reusable-release.yml uses that predicate; fire-cut accepts only `true|false|0|1`, so documented values such as `yes` now refuse before dispatch. Preserve the documented semantics or explicitly revise the contract and migration note. Local verification at this head: workflows.bats 64/64; go test -count=1 ./...; go vet ./...; go build ./...; golangci-lint 0; git diff --check. Forgejo statuses read success 25/25.
fix(release): cut_gate fails closed; restore dry_run semantics; sync the contract
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 3s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 18s
check-self-bootstrap / check (pull_request) Successful in 18s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 7s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 21s
tests / workflow-schema (pull_request) Successful in 18s
tests / bats (pull_request) Successful in 16s
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 6s
workflow-parse-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 39s
manifest-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 18s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
tests / shellcheck (pull_request) Successful in 16s
go-ci / lint + build + test (pull_request) Successful in 52s
fragment-check / changelog fragment-kind (pull_request) Successful in 8s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 22s
ac-closure-check / ac-closure check (pull_request) Successful in 35s
ac-closure-check / check (pull_request) Successful in 0s
bbc16b601b
Second review round on #1047, three findings.

(1) cut_gate FAILED OPEN. The defer branch tested for an exact string, so
`cut_gate: defered` read as execute and CUT -- from a caller that holds no
concurrency group, which is #1038's own defect through a different door.
Now validated execute|defer at BOTH sites and refusing otherwise: early in
act-on-decision so a typo fails on the first run in any mode, and in the
cut arm itself.

(2) The authoritative contract was stale. workflow-api.md's Inputs table
omitted cut_gate and its grounded line ranges pointed past the block they
name. Schema and integration docs do not keep the primary contract true --
the same shape as the recovery note, one layer up.

(3) The fire-cut whitelist NARROWED documented dry_run semantics.
workflow-api.md documents any non-empty/non-false string as truthy, so
`yes` would have started refusing. Not tightened and not left disagreeing:
dry_run is now NORMALISED with the reusable's own predicate, so the
contract is preserved AND only the literals true/false reach the JSON
body. Equivalence checked across 9 values including yes/no/TRUE.

Four arms, each mutation-verified:

  an INVALID cut_gate refuses -- the discriminating value, since `defer`
  exiting proves the check runs, not that it discriminates

  the validation is EXTRACTED AND EXECUTED against three values at EVERY
  site. Measured: extracting only the first block left a fail-open at the
  cut-arm site undetected, so the arm now runs each guard separately.

  workflow-api.md documents every declared input

  its grounded line ranges are current -- a coordinate into a file that
  moves now FAILS when stale instead of quietly misleading

Tracker: frankenbit/release-toolkit#1038
Owner

Integration blocker for rt#1047: current main c05cd3b2 carries #1032’s required workflows.bats arm asserting that the toolkit’s own consumer wrapper has a top-level release.yml concurrency block with cancel-in-progress=false. This PR intentionally moves concurrency to release-cut.yml, so a textual merge of tests/workflows.bats is insufficient and the required #1032 guard would fail. Rebase/resolve against main, intentionally update the guard to the new contract with an integration control proving the correct file/path, then rerun the merged-tree suite before requesting review.

Integration blocker for rt#1047: current main c05cd3b2 carries #1032’s required workflows.bats arm asserting that the toolkit’s own consumer wrapper has a top-level release.yml concurrency block with cancel-in-progress=false. This PR intentionally moves concurrency to release-cut.yml, so a textual merge of tests/workflows.bats is insufficient and the required #1032 guard would fail. Rebase/resolve against main, intentionally update the guard to the new contract with an integration control proving the correct file/path, then rerun the merged-tree suite before requesting review.
lookout left a comment

REQUEST_CHANGES — exact head bbc16b601b.

The three prior findings are fixed and independently verified: both cut_gate validation sites reject bogus (and each site’s invalid-branch mutation goes green); fire-cut dry_run normalization matches reusable-release.yml’s predicate for empty/false/0/true/1/yes/YES/TRUE/no; workflow-api.md input/range arms pass. On this head, 68/68 workflows.bats, go test -count=1 ./..., go vet ./..., go build ./..., golangci-lint (0 issues), and git diff --check all pass.

BLOCKER: this exact head cannot merge with the current base. The PR base is c05cd3b2e5; git merge-tree --write-tree origin/main HEAD returns rc=1 with a conflict in tests/workflows.bats. Main’s newer c05cd3b2 (#1032) required arm asserts that release.yml has a top-level concurrency block, while this PR intentionally removes that block and moves it to release-cut.yml. A textual rebase would leave the required #1032 arm failing (the branch’s release.yml has no top-level concurrency). Reconcile the guard with the split design (or otherwise resolve the conflict), then request a fresh review at the resulting head.

REQUEST_CHANGES — exact head bbc16b601b2a430afc5f8a135228d70c10bf97ce. The three prior findings are fixed and independently verified: both cut_gate validation sites reject bogus (and each site’s invalid-branch mutation goes green); fire-cut dry_run normalization matches reusable-release.yml’s predicate for empty/false/0/true/1/yes/YES/TRUE/no; workflow-api.md input/range arms pass. On this head, 68/68 workflows.bats, go test -count=1 ./..., go vet ./..., go build ./..., golangci-lint (0 issues), and git diff --check all pass. BLOCKER: this exact head cannot merge with the current base. The PR base is c05cd3b2e5d09595686cf2ca987319626adb8ffb; git merge-tree --write-tree origin/main HEAD returns rc=1 with a conflict in tests/workflows.bats. Main’s newer c05cd3b2 (#1032) required arm asserts that release.yml has a top-level concurrency block, while this PR intentionally removes that block and moves it to release-cut.yml. A textual rebase would leave the required #1032 arm failing (the branch’s release.yml has no top-level concurrency). Reconcile the guard with the split design (or otherwise resolve the conflict), then request a fresh review at the resulting head.
engineer force-pushed i/1038-claim-group-after-decide from bbc16b601b
All checks were successful
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 3s
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / check (pull_request) Successful in 0s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 18s
check-self-bootstrap / check (pull_request) Successful in 18s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 7s
register-check / register-drift check (pull_request) Successful in 7s
register-check / check (pull_request) Successful in 0s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 21s
tests / workflow-schema (pull_request) Successful in 18s
tests / bats (pull_request) Successful in 16s
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 6s
workflow-parse-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 39s
manifest-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 18s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
tests / shellcheck (pull_request) Successful in 16s
go-ci / lint + build + test (pull_request) Successful in 52s
fragment-check / changelog fragment-kind (pull_request) Successful in 8s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 22s
ac-closure-check / ac-closure check (pull_request) Successful in 35s
ac-closure-check / check (pull_request) Successful in 0s
to 9a8dc5b08f
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 6s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 16s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 19s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 23s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
register-check / register-drift check (pull_request) Successful in 8s
register-check / check (pull_request) Successful in 0s
ac-closure-check / ac-closure check (pull_request) Successful in 44s
tests / workflow-schema (pull_request) Successful in 4s
ac-closure-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 4s
tests / shellcheck (pull_request) Successful in 3s
fragment-check / changelog fragment-kind (pull_request) Successful in 39s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 37s
manifest-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 9s
go-ci / lint + build + test (pull_request) Successful in 52s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 25s
tests / bats (pull_request) Successful in 38s
workflow-parse-check / check (pull_request) Successful in 0s
2026-08-28 18:37:54 +02:00
Compare
test(workflows): tie the #1032 guard to the workflow fire-cut actually dispatches
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 6s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Successful in 6s
changelog-body-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 15s
fragment-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 5s
fork-pr-approval-notice / explain fork workflow approval (pull_request_target) Successful in 19s
ac-closure-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 24s
manifest-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 16s
ac-closure-check / ac-closure check (pull_request) Successful in 39s
ac-closure-check / check (pull_request) Successful in 0s
go-ci / lint + build + test (pull_request) Successful in 26s
tests / workflow-schema (pull_request) Successful in 4s
fragment-check / changelog fragment-kind (pull_request) Successful in 36s
register-check / toolkit-self gate (PR's own rt) (pull_request) Successful in 17s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 36s
manifest-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 16s
tests / shellcheck (pull_request) Successful in 13s
workflow-parse-check / workflow parse and schema (pull_request) Successful in 5s
workflow-parse-check / check (pull_request) Successful in 0s
tests / dated-examples (pull_request) Successful in 20s
register-check / register-drift check (pull_request) Successful in 37s
register-check / check (pull_request) Successful in 0s
workflow-parse-check / toolkit-self parse guard and controls (pull_request) Successful in 16s
992c0d7b3f
Rebase onto main brought #1032's regression guard, which asserts the
toolkit's own consumer wrapper carries a top-level concurrency block with
cancel-in-progress: false -- on release.yml, the file this branch moves the
block OUT of.

A textual merge was not sufficient. The guard was RE-POINTED to
release-cut.yml rather than deleted: its intent is unchanged -- the caller
that CUTS must carry a workflow-level block -- but that caller is no longer
release.yml, which now only decides. Deleting it would have dropped a live
regression guard one file over.

That left both the dispatch target and the guard's target spelled out by
hand in different files, so a rename moves one and leaves the other
checking a path nothing fires. This arm DERIVES the cut workflow from the
dispatch URL in fire-cut and asserts three things about that same file: it
exists, it carries the workflow-level block, and the #1032 guard reads it.

Mutation-verified with two distinct failures: renaming the dispatch target
reports the guard reading the wrong file; dispatching a non-existent
workflow reports it missing.

Every commit on this branch is green independently, which fast-forward-only
merging requires.

Tracker: frankenbit/release-toolkit#1038
lookout approved these changes 2026-08-28 18:42:36 +02:00
lookout left a comment

APPROVED — exact head 992c0d7b3f.

The prior three findings are resolved. I verified the merged split against current main: release.yml has no top-level concurrency, release-cut.yml has the workflow-level release-cut group with cancel-in-progress=false, and the #1032 guard derives/reads the workflow named by fire-cut's dispatch. The branch merges cleanly with current main.

Targeted controls: both cut_gate validation sites reject bogus; mutating either invalid branch makes its site accept bogus (rc 0); dry_run normalization matches reusable-release.yml for empty/false/0/true/1/yes/YES/TRUE/no. The integration arm passes and the full workflows.bats suite is 72/72. go test -count=1 ./..., go vet ./..., go build ./..., golangci-lint (0 issues), shellcheck scripts, and git diff --check all pass. Forgejo's latest 25/25 status contexts are success.

No remaining blocker.

APPROVED — exact head 992c0d7b3f5eeb19398d39a591b729e1711b64df. The prior three findings are resolved. I verified the merged split against current main: release.yml has no top-level concurrency, release-cut.yml has the workflow-level release-cut group with cancel-in-progress=false, and the #1032 guard derives/reads the workflow named by fire-cut's dispatch. The branch merges cleanly with current main. Targeted controls: both cut_gate validation sites reject bogus; mutating either invalid branch makes its site accept bogus (rc 0); dry_run normalization matches reusable-release.yml for empty/false/0/true/1/yes/YES/TRUE/no. The integration arm passes and the full workflows.bats suite is 72/72. go test -count=1 ./..., go vet ./..., go build ./..., golangci-lint (0 issues), shellcheck scripts, and git diff --check all pass. Forgejo's latest 25/25 status contexts are success. No remaining blocker.
lookout approved these changes 2026-08-28 18:43:06 +02:00
lookout left a comment

APPROVE — exact head 992c0d7b3f5eeb19398d39a591b729e1711b64df.

The rebase correctly reconciles #1032: release.yml is the decide-only caller with no top-level concurrency, release-cut.yml is the dispatched cut caller with the workflow-level release-cut-${{ github.ref }} group and cancel-in-progress: false. The new integration arm derives the dispatch target and confirms the guard reads that same existing workflow. I verified the branch merges cleanly with current main.

The prior three findings remain fixed: both cut_gate validation sites reject bogus, and mutating either invalid branch makes that site accept it (rc 0); fire-cut dry_run normalization matches the reusable predicate for empty/false/0/true/1/yes/YES/TRUE/no; workflow-api.md documents every input and its grounded ranges are current.

Evidence at this head: workflows.bats 72/72; go test -count=1 ./..., go vet ./..., go build ./..., golangci-lint (0 issues), ShellCheck on scripts, and git diff --check all pass. The latest 25 Forgejo contexts are success. No remaining blocker.

**APPROVE** — exact head `992c0d7b3f5eeb19398d39a591b729e1711b64df`. The rebase correctly reconciles #1032: release.yml is the decide-only caller with no top-level concurrency, release-cut.yml is the dispatched cut caller with the workflow-level `release-cut-${{ github.ref }}` group and `cancel-in-progress: false`. The new integration arm derives the dispatch target and confirms the guard reads that same existing workflow. I verified the branch merges cleanly with current main. The prior three findings remain fixed: both `cut_gate` validation sites reject `bogus`, and mutating either invalid branch makes that site accept it (rc 0); fire-cut `dry_run` normalization matches the reusable predicate for empty/false/0/true/1/yes/YES/TRUE/no; workflow-api.md documents every input and its grounded ranges are current. Evidence at this head: workflows.bats 72/72; `go test -count=1 ./...`, `go vet ./...`, `go build ./...`, golangci-lint (0 issues), ShellCheck on scripts, and `git diff --check` all pass. The latest 25 Forgejo contexts are success. No remaining blocker.
bosun merged commit bb056e66b6 into main 2026-08-28 18:44:16 +02:00
Sign in to join this conversation.
No description provided.