feat(workflows): one reusable-pr-gates.yml runs every PR-time gate in one job, and a tag-pinned adopter cannot silently run main #1541

Closed
opened 2026-10-04 10:53:48 +02:00 by claude · 4 comments
Member

Part of #1530. Implements ADR-0013 (docs/adr/0013-one-pr-gates-reusable.md, accepted 2026-10-04 via #1544; details in 0013-one-pr-gates-reusable-details.md). This issue adds the new reusable alongside the old ones; #1542 removes the old ones.

Why

Adopters wire up to four gate workflows besides release.yml (#1530: storey 1, tmux-tell 2, ember 2, cellblock 2, purser 3). ADR-0013 replaces them with one reusable-pr-gates.yml, wired from one pr.yml. Its layout input decides how the gates map to jobs: combined (one job) suits scarce runner slots, and split (one job per gate) suits free ones. The old reusables and wrappers keep running until #1542.

What ADR-0013 decided, as it applies here

  • D1, the adoption surface. One reusable runs manifest, fragments, changelog body, register, ac-closure and workflow parse. Each gate is on|off.
  • D2, the job layout. A layout: combined | split input; combined is the default.
    • combined: every on gate is a step in one job, with one checkout and one rt build. A last step requires each gate's verdict file.
    • split: one job per on gate, plus a result job that needs: all of them, runs with if: always(), and checks needs.<job>.result for every on gate. A skipped job posts success, so without always() a wrongly skipped gate would leave everything green.
    • Both layouts run the same per-gate steps, so they cannot drift. An unknown layout value refuses.
  • D3, one required status. A repository requires only its caller job's status from pr.yml, e.g. pr / gates (pull_request). That name is the same in both layouts.
  • D4, the parse gate. It runs inside pr.yml.
  • The wiring rule for pr.yml. The pull_request types include edited, because body-reading gates must re-run, and there is no paths: filter (#644 / #629).
  • D5, gate defaults. on for manifest and fragments only, and no advisory mode.
  • D6, the fail-closed resolve rule.
    • Refuse when the baked value is empty or main, no caller pin is found, and the caller is not toolkit-self.
    • Find the caller pin by scanning EVERY top-level workflow file.
    • Refuse two different pins, or a local-path call mixed with a remote pin.
    • Recognise toolkit-self by its local-path call.
    • Keep the #456 @main arm, the #336 SHA arm and the #1222 fallback.
    • Branch pins (#1534) are unchanged.

Measured at origin/main e5fcf1d, extended at 6b5d554 by the plan review (#1530 comment 114846)

  • The bake list is the safety net, and it is hand-kept in many places.

    • canonicalFiles (internal/bake/marker.go:18) lists 9 reusables.
    • tests/workflows.bats repeats that list twice, at lines 254 and 644.
    • The same file also hard-codes:
      • if len(scripts) != 18 (lines 1389 and 1656), which is 9 reusables times 2 providers;
      • the wrapper_names maps (lines 1322 and 1600);
      • the reusable-to-wrapper cases pairs (lines 855-860);
      • the gates=(...) array in "all 4 PR-CI gates consume the shared bootstrap artifact" (line 2111).

    Line numbers may have moved since #1540 (#1546) landed; re-read them.

  • The current pin lookup reads a wrapper by a fixed file name. For example, reusable-manifest-check.yml reads .forgejo/workflows/manifest-check.yml. A pr.yml matches no such name.

  • The bats resolve-ref harness assumes the same fixed name. It derives the caller file as wrapper_names.get(reusable, reusable.removeprefix("reusable-")) (lines 1394 and 1661), which would give pr-gates.yml. Under D6 it needs rewriting for the new reusable, not a new map entry.

To measure before relying on it (ADR-0013 details, "Open measurements"; comments 114955 and 114994)

Use a throwaway repo with no workflows in the way, following AGENTS.md section 14's pattern: verify on the forge that it has no workflows, both right before use and after.

  • Ask the coordinator before creating the repo.
  • The operator deletes it afterwards. Agents do not delete repositories.
  • Push no v* tags there.
  1. A single non-matrix inner job fails. Does the caller's status fail? This was seen once, on #1430's run 17092. Confirm it.
  2. An inner job is skipped. Does the caller treat it as passed?
  3. A matrix is built from an input with fromJSON. If that does not work here, use the ADR's fallback for split: static per-gate jobs with if:.
  4. How much time split saves on a runner with more than one free slot. Record it if it can be measured; do not block on it.

Acceptance criteria

  • reusable-pr-gates.yml runs manifest, fragments, changelog body, register, ac-closure and workflow parse. Each gate is on|off; defaults are on for manifest and fragments only. The job is red if any on gate fails. — DONE @ #1547 (855b34e, reviewed as 855b34e). On the final head, pr / gates (run 19826) ran all six gates and attested.
  • layout: combined | split works as D2 describes: combined is the default, an unknown value refuses, and both layouts run the same per-gate steps. Mutation-verified in BOTH layouts: a skipped, deleted or failing on gate turns the run red. — DONE @ #1547 (855b34e, reviewed as 855b34e). split with the real gates ran live on the second head (run 19770, every gate on and success). Both layouts are mutation-verified in tests/pr-gates.bats (review 7235).
  • split's result job runs with if: always() and checks needs.<job>.result for every on gate. A mutant without always() shows the hole it closes. — DONE @ #1547 (855b34e, reviewed as 855b34e). The no-always() mutant was measured live on the probe repo: the caller stayed green. The reviewer killed mutant Ma (on:skipped accepted).
  • The caller status from pr.yml follows the inner jobs in both layouts. This is measured on the throwaway repo for items 1-3 above, and the results are recorded in the PR and on this issue. — DONE @ #1547 (855b34e, reviewed as 855b34e). Measured on frankenbit/pr-gates-probe-1541; see comment 115075. A single non-matrix inner failure turns the caller red, skipped inner jobs leave it green, and a fromJSON matrix works.
  • The Gitea twin is generated, and gitea-twin-check passes. — DONE @ #1547 (855b34e, reviewed as 855b34e). .gitea/workflows/reusable-pr-gates.yml. gitea-twin-check is green on the head and on push:main (run 19832).
  • Every hand-kept "all baked reusables" list in tests/workflows.bats is either derived from the bake marker or deliberately left fixed, with the reason in the PR. Mutation-verified: a reusable left out of canonicalFiles turns the registration check red. — DONE @ #1547 (855b34e, reviewed as 855b34e). The registration list is derived from the bake marker, and the canonicalFiles omission mutant is killed. The PR's decision tree gives each fixed list's reason.
  • The D6 resolve rule is implemented: the scan of every top-level workflow, the refusals for two pins and for mixed local and remote pins, and the toolkit-self local-path arm. The bats resolve-ref harness covers it. Mutation-verified, and the @main, SHA and #1222 arms still pass their tests. — DONE @ #1547 (855b34e, reviewed as 855b34e). There are 20 cases on both providers and 12 mutants killed. The reviewer re-killed Mb (a remote pin reaching the workspace script) and Mc (quote stripping).
  • Toolkit-self calls it from a local pr.yml. — DONE @ #1547 (855b34e, reviewed as 855b34e). pr.yml uses layout: combined, types: [opened, synchronize, reopened, edited] and no paths:. The caller context is pr / gates (pull_request).
    • It has every gate on, layout: combined, and follows the wiring rule (edited in the types, no paths:).
    • It is green on its own PR.
    • The PR names the exact caller context for #1542's swap of the required checks.
  • The parse-self mutation control moves into tests / checks (the job that now runs the workflow-schema step, after #1540). — DONE @ #1547 (855b34e, reviewed as 855b34e). It is step c13 of tests / checks, and run 19774 logs negative control OK.
  • C3 (docs/architecture/contracts/workflow-api.md) documents reusable-pr-gates.yml's inputs, including layout and every gate switch, with the bake, compose and NeedsSecrets lists updated. — DONE @ #1547 (855b34e, reviewed as 855b34e). workflow-api.md documents all 21 inputs and the pr / gates (pull_request) status.
  • A changelog fragment of kind added names the new reusable. — DONE @ #1547 (855b34e, reviewed as 855b34e). changelog.d/1541-reusable-pr-gates.added.md.
Part of #1530. Implements ADR-0013 (`docs/adr/0013-one-pr-gates-reusable.md`, accepted 2026-10-04 via #1544; details in `0013-one-pr-gates-reusable-details.md`). This issue adds the new reusable alongside the old ones; #1542 removes the old ones. ## Why Adopters wire up to four gate workflows besides `release.yml` (#1530: storey 1, tmux-tell 2, ember 2, cellblock 2, purser 3). ADR-0013 replaces them with one `reusable-pr-gates.yml`, wired from one `pr.yml`. Its `layout` input decides how the gates map to jobs: `combined` (one job) suits scarce runner slots, and `split` (one job per gate) suits free ones. The old reusables and wrappers keep running until #1542. ## What ADR-0013 decided, as it applies here - **D1, the adoption surface.** One reusable runs manifest, fragments, changelog body, register, ac-closure and workflow parse. Each gate is `on|off`. - **D2, the job layout.** A `layout: combined | split` input; `combined` is the default. - **`combined`:** every `on` gate is a step in one job, with one checkout and one `rt` build. A last step requires each gate's verdict file. - **`split`:** one job per `on` gate, plus a result job that `needs:` all of them, runs with `if: always()`, and checks `needs.<job>.result` for every `on` gate. A skipped job posts `success`, so without `always()` a wrongly skipped gate would leave everything green. - **Both layouts** run the same per-gate steps, so they cannot drift. An unknown `layout` value refuses. - **D3, one required status.** A repository requires only its caller job's status from `pr.yml`, e.g. `pr / gates (pull_request)`. That name is the same in both layouts. - **D4, the parse gate.** It runs inside `pr.yml`. - **The wiring rule for `pr.yml`.** The `pull_request` types include `edited`, because body-reading gates must re-run, and there is no `paths:` filter (#644 / #629). - **D5, gate defaults.** `on` for manifest and fragments only, and no advisory mode. - **D6, the fail-closed resolve rule.** - Refuse when the baked value is empty or `main`, no caller pin is found, and the caller is not toolkit-self. - Find the caller pin by scanning EVERY top-level workflow file. - Refuse two different pins, or a local-path call mixed with a remote pin. - Recognise toolkit-self by its local-path call. - Keep the #456 `@main` arm, the #336 SHA arm and the #1222 fallback. - Branch pins (#1534) are unchanged. ## Measured at origin/main `e5fcf1d`, extended at `6b5d554` by the plan review (#1530 comment 114846) - **The bake list is the safety net, and it is hand-kept in many places.** - `canonicalFiles` (`internal/bake/marker.go:18`) lists 9 reusables. - `tests/workflows.bats` repeats that list twice, at lines 254 and 644. - The same file also hard-codes: - `if len(scripts) != 18` (lines 1389 and 1656), which is 9 reusables times 2 providers; - the `wrapper_names` maps (lines 1322 and 1600); - the reusable-to-wrapper `cases` pairs (lines 855-860); - the `gates=(...)` array in "all 4 PR-CI gates consume the shared bootstrap artifact" (line 2111). Line numbers may have moved since #1540 (#1546) landed; re-read them. - **The current pin lookup reads a wrapper by a fixed file name.** For example, `reusable-manifest-check.yml` reads `.forgejo/workflows/manifest-check.yml`. A `pr.yml` matches no such name. - **The bats resolve-ref harness assumes the same fixed name.** It derives the caller file as `wrapper_names.get(reusable, reusable.removeprefix("reusable-"))` (lines 1394 and 1661), which would give `pr-gates.yml`. Under D6 it needs rewriting for the new reusable, not a new map entry. ## To measure before relying on it (ADR-0013 details, "Open measurements"; comments 114955 and 114994) Use a throwaway repo with no workflows in the way, following AGENTS.md section 14's pattern: verify on the forge that it has no workflows, both right before use and after. - **Ask the coordinator before creating the repo.** - **The operator deletes it afterwards.** Agents do not delete repositories. - **Push no `v*` tags there.** 1. **A single non-matrix inner job fails.** Does the caller's status fail? This was seen once, on #1430's run 17092. Confirm it. 2. **An inner job is skipped.** Does the caller treat it as passed? 3. **A matrix is built from an input with `fromJSON`.** If that does not work here, use the ADR's fallback for `split`: static per-gate jobs with `if:`. 4. **How much time `split` saves** on a runner with more than one free slot. Record it if it can be measured; do not block on it. ## Acceptance criteria - [x] `reusable-pr-gates.yml` runs manifest, fragments, changelog body, register, ac-closure and workflow parse. Each gate is `on|off`; defaults are `on` for manifest and fragments only. The job is red if any `on` gate fails. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). On the final head, `pr / gates` (run 19826) ran all six gates and attested. - [x] `layout: combined | split` works as D2 describes: `combined` is the default, an unknown value refuses, and both layouts run the same per-gate steps. Mutation-verified in BOTH layouts: a skipped, deleted or failing `on` gate turns the run red. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). `split` with the real gates ran live on the second head (run 19770, every gate on and success). Both layouts are mutation-verified in `tests/pr-gates.bats` (review 7235). - [x] `split`'s result job runs with `if: always()` and checks `needs.<job>.result` for every `on` gate. A mutant without `always()` shows the hole it closes. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). The no-`always()` mutant was measured live on the probe repo: the caller stayed green. The reviewer killed mutant Ma (`on:skipped` accepted). - [x] The caller status from `pr.yml` follows the inner jobs in both layouts. This is measured on the throwaway repo for items 1-3 above, and the results are recorded in the PR and on this issue. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). Measured on `frankenbit/pr-gates-probe-1541`; see comment 115075. A single non-matrix inner failure turns the caller red, skipped inner jobs leave it green, and a `fromJSON` matrix works. - [x] The Gitea twin is generated, and `gitea-twin-check` passes. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). `.gitea/workflows/reusable-pr-gates.yml`. `gitea-twin-check` is green on the head and on push:main (run 19832). - [x] Every hand-kept "all baked reusables" list in `tests/workflows.bats` is either derived from the bake marker or deliberately left fixed, with the reason in the PR. Mutation-verified: a reusable left out of `canonicalFiles` turns the registration check red. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). The registration list is derived from the bake marker, and the `canonicalFiles` omission mutant is killed. The PR's decision tree gives each fixed list's reason. - [x] The D6 resolve rule is implemented: the scan of every top-level workflow, the refusals for two pins and for mixed local and remote pins, and the toolkit-self local-path arm. The bats resolve-ref harness covers it. Mutation-verified, and the `@main`, SHA and #1222 arms still pass their tests. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). There are 20 cases on both providers and 12 mutants killed. The reviewer re-killed Mb (a remote pin reaching the workspace script) and Mc (quote stripping). - [x] Toolkit-self calls it from a local `pr.yml`. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). `pr.yml` uses `layout: combined`, `types: [opened, synchronize, reopened, edited]` and no `paths:`. The caller context is `pr / gates (pull_request)`. - It has every gate `on`, `layout: combined`, and follows the wiring rule (`edited` in the types, no `paths:`). - It is green on its own PR. - The PR names the exact caller context for #1542's swap of the required checks. - [x] The parse-self mutation control moves into `tests / checks` (the job that now runs the workflow-schema step, after #1540). — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). It is step c13 of `tests / checks`, and run 19774 logs `negative control OK`. - [x] C3 (`docs/architecture/contracts/workflow-api.md`) documents `reusable-pr-gates.yml`'s inputs, including `layout` and every gate switch, with the bake, compose and `NeedsSecrets` lists updated. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). `workflow-api.md` documents all 21 inputs and the `pr / gates (pull_request)` status. - [x] A changelog fragment of kind `added` names the new reusable. — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). `changelog.d/1541-reusable-pr-gates.added.md`.
Author
Member

Scope addition from ADR-0013 (#1544, review 7224). It takes effect once the ADR is accepted; the acceptance criteria will be updated in the same pass.

  • Two layouts. reusable-pr-gates.yml takes a layout: combined | split input (ADR-0013 D2). Both layouts run the same per-gate step logic, and both are mutation-verified.
  • One required status. Each repository requires only the status of the caller job in pr.yml (D3). This was measured over 260 closed PR heads: in 1,287 of 1,287 caller/inner pairs, the caller's status followed the inner jobs.
  • split's result job must run with if: always() (or !cancelled()). A job whose needs failed or was skipped is itself skipped, and a skipped job posts success. Without always(), a mistakenly skipped on gate leaves the result job and the caller green (compare goreleaser.yml:506). This needs a mutant.
  • To measure before relying on them, on a throwaway repo with no workflows in the way (AGENTS.md section 14's pattern):
    1. A single inner job failing. Does a caller fail when one non-matrix inner job fails? Only matrix-leg failures have ever been seen (#1471, #1470). combined relies on exactly this case.
    2. A skipped inner job. Does the caller treat it as passed?
    3. A matrix built from an input with fromJSON. No workflow here uses one. The ADR gives a fallback that needs none.
    4. split's wall-clock saving on a runner with more than one free slot.
Scope addition from ADR-0013 (#1544, review 7224). It takes effect once the ADR is accepted; the acceptance criteria will be updated in the same pass. - **Two layouts.** `reusable-pr-gates.yml` takes a `layout: combined | split` input (ADR-0013 D2). Both layouts run the same per-gate step logic, and both are mutation-verified. - **One required status.** Each repository requires only the status of the caller job in `pr.yml` (D3). This was measured over 260 closed PR heads: in 1,287 of 1,287 caller/inner pairs, the caller's status followed the inner jobs. - **`split`'s result job must run with `if: always()`** (or `!cancelled()`). A job whose `needs` failed or was skipped is itself skipped, and a skipped job posts `success`. Without `always()`, a mistakenly skipped `on` gate leaves the result job and the caller green (compare `goreleaser.yml:506`). This needs a mutant. - **To measure before relying on them, on a throwaway repo with no workflows in the way** (AGENTS.md section 14's pattern): 1. **A single inner job failing.** Does a caller fail when one non-matrix inner job fails? Only matrix-leg failures have ever been seen (#1471, #1470). `combined` relies on exactly this case. 2. **A skipped inner job.** Does the caller treat it as passed? 3. **A matrix built from an input with `fromJSON`.** No workflow here uses one. The ADR gives a fallback that needs none. 4. **`split`'s wall-clock saving** on a runner with more than one free slot.
Author
Member

Correction to comment 114955, item 1 of the measure list. A single non-matrix inner job failing HAS been observed once.

PR #1430's head cbf70184 (run 17092): the only inner job, fragment-check / changelog fragment-kind, failed ("Failing after 7s"), and the caller fragment-check / check failed with it ("Failing after 0s"). I read this at source today.

So combined's case has one positive observation, alongside the two matrix-leg cases (#1471 and #1470). Three heads are still a small sample. Keep the throwaway-repo measurement in #1541, but read it as confirming a behaviour already seen once, not as an untested one.

Correction to comment 114955, item 1 of the measure list. A single non-matrix inner job failing HAS been observed once. PR #1430's head `cbf70184` (run 17092): the only inner job, `fragment-check / changelog fragment-kind`, failed ("Failing after 7s"), and the caller `fragment-check / check` failed with it ("Failing after 0s"). I read this at source today. So `combined`'s case has one positive observation, alongside the two matrix-leg cases (#1471 and #1470). Three heads are still a small sample. Keep the throwaway-repo measurement in #1541, but read it as confirming a behaviour already seen once, not as an untested one.
Author
Member

Measurements for items 1-3 of "To measure", taken on the throwaway repo frankenbit/pr-gates-probe-1541 (private, created 2026-10-04; the operator deletes it). PR #1547 carries the same table.

The setup.

  • Before use, the API showed no .forgejo/workflows, .gitea/workflows or .github/workflows in the repo (404 each).
  • I pushed 10 synthetic workflow files to a probe branch only, with no tags, and opened PR #1 there (head 21feae0). After the runs, main still has none of the three directories, and probe holds exactly those 10 files.
  • The workflows reproduce reusable-pr-gates.yml's job graph:
    • a gates job;
    • static a and b jobs that need it;
    • a result job, with if: ${{ always() && inputs.layout == 'split' }}.
  • Every step is echo or exit.
  • I read the job logs in the browser for every run, as well as the statuses.
Caller (<wf> / gates) Inner jobs Caller status
c1 combined, pass plan job success; a, b and result skipped success
c2 combined, the one inner job fails plan job failure; a, b and result skipped failure
c3 split, a on, b off a success; b skipped; result success success
c4 split, a fails a failure; b success; result failure failure
c5 split, b on but wrongly skipped b skipped; result failure (REFUSED b:on:skipped) failure
c6 the same, with the result job without always() b skipped; result skipped success (the hole)
c7 a matrix from fromJSON(inputs.gates), and one from fromJSON(needs.plan.outputs.gates) all four legs ran success

What this answers.

  • Item 1: a single non-matrix inner job that fails turns the caller red (c2). This confirms the observation on #1430's run 17092.
  • Item 2: skipped inner jobs leave the caller green (c1, c3).
  • Item 3: a fromJSON matrix works, both from an input and from a job output.
    • #1547 still uses static per-gate jobs for split, so that the result job can read needs.<job>.result for each gate.
  • Item 4, split's time saving: not measured, because the runner has one slot.
Measurements for items 1-3 of "To measure", taken on the throwaway repo `frankenbit/pr-gates-probe-1541` (private, created 2026-10-04; the operator deletes it). PR #1547 carries the same table. **The setup.** - Before use, the API showed no `.forgejo/workflows`, `.gitea/workflows` or `.github/workflows` in the repo (404 each). - I pushed 10 synthetic workflow files to a `probe` branch only, with no tags, and opened PR #1 there (head `21feae0`). After the runs, `main` still has none of the three directories, and `probe` holds exactly those 10 files. - The workflows reproduce `reusable-pr-gates.yml`'s job graph: - a `gates` job; - static `a` and `b` jobs that `need` it; - a `result` job, with `if: ${{ always() && inputs.layout == 'split' }}`. - Every step is `echo` or `exit`. - I read the job logs in the browser for every run, as well as the statuses. | Caller (`<wf> / gates`) | Inner jobs | Caller status | |---|---|---| | c1 combined, pass | plan job success; a, b and result skipped | success | | c2 combined, the one inner job fails | plan job failure; a, b and result skipped | **failure** | | c3 split, a on, b off | a success; b skipped; result success | success | | c4 split, a fails | a failure; b success; result failure | failure | | c5 split, b on but wrongly skipped | b skipped; result failure (`REFUSED b:on:skipped`) | failure | | c6 the same, with the result job **without** `always()` | b skipped; result **skipped** | **success** (the hole) | | c7 a matrix from `fromJSON(inputs.gates)`, and one from `fromJSON(needs.plan.outputs.gates)` | all four legs ran | success | **What this answers.** - **Item 1:** a single non-matrix inner job that fails turns the caller red (c2). This confirms the observation on #1430's run 17092. - **Item 2:** skipped inner jobs leave the caller green (c1, c3). - **Item 3:** a `fromJSON` matrix works, both from an input and from a job output. - #1547 still uses static per-gate jobs for `split`, so that the result job can read `needs.<job>.result` for each gate. - **Item 4**, split's time saving: not measured, because the runner has one slot.
Author
Member

Closing by hand: all eleven acceptance criteria are ticked.

  • Landed: #1547, fast-forwarded to 855b34e76b386d6aeb791ebdd1e9176ede36307a, which is exactly the reviewed head. Alex merged it on 2026-10-05 at 19:58, and the head branch is deleted.
  • Review: 7235 from the reviewer session (verdict APPROVE at 855b34e, posted as a COMMENT), plus Alex's approval on the forge.
  • CI on the head: 30 of 30 green. In pr / gates (run 19826), all six gates ran and attested, the landing-tree replay passed, and the register gate ran the file scan only. bats ran 1..246 with no failures.
  • Operator decisions taken during the work (2026-10-04):
    • every gate off is refused, in both layouts;
    • the landing-tree replay runs, and the commit-subject scan is retired pending #1548.
  • For #1542: require pr / gates (pull_request).
  • Throwaway repo: frankenbit/pr-gates-probe-1541 is still there for the operator to delete. Its measurements are in comment 115075.
Closing by hand: all eleven acceptance criteria are ticked. - **Landed:** #1547, fast-forwarded to `855b34e76b386d6aeb791ebdd1e9176ede36307a`, which is exactly the reviewed head. Alex merged it on 2026-10-05 at 19:58, and the head branch is deleted. - **Review:** 7235 from the reviewer session (verdict APPROVE at `855b34e`, posted as a COMMENT), plus Alex's approval on the forge. - **CI on the head:** 30 of 30 green. In `pr / gates` (run 19826), all six gates ran and attested, the landing-tree replay passed, and the register gate ran the file scan only. bats ran 1..246 with no failures. - **Operator decisions taken during the work (2026-10-04):** - every gate `off` is refused, in both layouts; - the landing-tree replay runs, and the commit-subject scan is retired pending #1548. - **For #1542:** require `pr / gates (pull_request)`. - **Throwaway repo:** `frankenbit/pr-gates-probe-1541` is still there for the operator to delete. Its measurements are in comment 115075.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
frankenbit/release-toolkit#1541
No description provided.