chore(ci): one PR-gates reusable and fewer toolkit-self jobs, because the runner is down to one slot (#1304's hold condition) #1530

Open
opened 2026-10-03 01:14:01 +02:00 by claude · 3 comments
Member

The runner queue is the binding constraint again. #1304 closed its consolidation tracker as won't-do and named the condition for reopening it: "a future state where the runner queue is the binding constraint again". That state is now measured, and the operator has confirmed it will last. This tracker covers a smaller adopter workflow surface and fewer toolkit-self jobs. It goes further than #1304 in one respect: the operator has also accepted breaking the reusable-workflow API, because every adopter is a repo under the same control.

Tracker since 2026-10-04. The work is split into the sub-issues under "Acceptance criteria". The corrections found in review are under "Corrections".

Measured

Data comes from the live forge through the public API, on 2026-10-02/03, at origin/main dd871e8 to 8086700.

Concurrency is 1.

  • Of 1,500 tasks, never more than one ran at a time. A re-check covered 277 tasks between 2026-10-02 22:24 and 2026-10-03 00:55: the maximum concurrency was 1.
  • On Sep 7-8 a second runner (caymans-fedora) also took jobs. That was the operator's workstation booted into Fedora, and it no longer runs one.
  • Alcatraz has no room for more parallel builds (operator, 2026-10-03, after asking the Alcatraz admin).

One toolkit-self PR (runs 19373-19387; median over 25 heads):

What Measured
jobs that run 22 (173 s summed)
required contexts on main 25
rt builds 15
wall clock from first job start to last finish 200 s median
time until all checks have reported 246 s

A small job takes about 5.5 s, and about 87% of that is setup:

  • container setup 0.95 s;
  • checkout 0.79-1.87 s;
  • replay 1.06 s;
  • rt build 2.05 s.

The gates themselves took 28 s of the 224 s that small jobs spent. The real work sits in two jobs: bats at about 34 s and go-ci at about 30 s.

The adopters are five repos plus toolkit-self. Each wires up to six workflow files, at mixed pins (re-read on 2026-10-04; unchanged):

Adopter Workflows wired Pin(s)
storey release, manifest-check @v0.63.0
tmux-tell release, manifest-check, fragment-check @v0.63.1
ember release, manifest-check @v0.42.0
fragment-check @v0.56.0
cellblock release, manifest-check, fragment-check @v0.42.0
purser release, manifest-check, fragment-check, changelog-body-check @v0.36.0

Why #1304's reasons no longer decide this

#1304 comment 109121 gave three reasons. Here is where each stands now.

  1. "Consolidation trades wall-clock for slots, and only pays when slots are contended." Slots are now contended permanently, so every job removed saves its full prefix. Merged jobs stay shorter than the critical path (bats, about 34 s), so wall-clock does not get worse at any slot count.
  2. "The triggers do not form a union." That is still true, and the plan respects it:
    • Only pull_request gates merge.
    • fork-pr-approval-notice (pull_request_target, the trust boundary) stays separate.
    • So do the push, schedule, release and dispatch workflows.
    • So do the release.yml / release-cut.yml concurrency split (ADR-0010) and recover-pending-cut, which stays dispatch-only because nothing may fire it without a person.
  3. The residual risk: gates in one job share one checkout and one $RUNNER_TEMP, and nothing enforces that they do not collide. The plan enforces it:
    • each gate writes a verdict file in its own namespace;
    • the attest step requires the exact expected set.

Plan (reviewed design, sizes estimated)

  1. PR-A (S, no-changelog), now #1539. The next free ADR (0013), written as a decision tree. It supersedes the frozen-API statement in docs/architecture/contracts/workflow-api.md and records why #1304's hold condition is met.

  2. PR-B (L, added, additive), now #1541. Adds reusable-pr-gates.yml and its twin. Its job runs every PR-time gate as a step, with one checkout and one rt build:

    • manifest, fragments, changelog body, register, ac-closure and workflow parse;
    • each gate is on or off, and the job is red if any on gate fails. There is no in-job "advisory" mode: this forge has no job-log API, so a warning inside a green job is invisible (#1177);
    • defaults are on only for manifest and fragments.

    Toolkit-self calls it from a local pr.yml with all gates on. The PR also carries:

    • the bake, compose and NeedsSecrets lists;
    • a bake-marker-derived list for the workflows.bats registration guard (today a hard-coded list of 9);
    • a fail-closed resolve step: refuse when the baked value is main, no caller pin is found, and the repository is not toolkit-self. Otherwise a tag-pinned adopter would silently run main;
    • the parse-self mutation control, moved into tests / workflow-schema;
    • mutation-verified tests.

    The old wrappers keep running.

  3. PR-C (M, additive), now #1540.

    • Merge tests.yml's four short jobs into one tests / checks job, porting the build, tree-state and attest guards from toolkit-self-gates (#1323).
    • Grow toolkit-self-gates by the PR halves of check-self-bootstrap and gitea-twin-check, by prep-order-check and by readme-pin digest.
    • The advisory checks (readme-pin-check / mirror, base-divergence-check) stay separate jobs.
  4. One admin edit: swap the required-context list on main just before PR-D merges, after approving PR-D. (Corrected below: two swaps.)

  5. PR-D (M-L, removed), now #1542. It needs a BREAKING CHANGE line naming the renamed contexts (#1479). It deletes:

    • the 6 gate wrappers;
    • the 6 gate reusables and their 6 twins, in one PR, since gitea_twin.go fails on orphans;
    • the stale pre_push.go rows;
    • the matching docs, examples and README text.
  6. One cut (the next minor, v0.66.0 or later; v0.65.0 was cut on 2026-10-04). Old tags keep working. Part of #1542.

  7. Adopters, one PR each, one issue each in the adopter's repo:

    • tmux-tell first. It is the only live probe of the adopter path: tag fetch, baked ref and cross-repo call. Read its job log, not just its status (AGENTS.md section 8, Axis C).
    • Then storey, ember, cellblock and purser. Set RELEASE_TOOLKIT_MINISIGN_PUBLIC_KEY first for adopters jumping past v0.57.0 (#1479).
    • Each PR: deletes the gate wrappers, adds a pr.yml with only the gates that adopter already ran, and bumps release.yml's pin.
    • If #1529 ships in the same cut, storey adds its release_notes key in the same PR. Moot (see Corrections).

Expected result (estimated, one slot):

Today After
toolkit-self jobs per PR 22 8
required contexts 25 6
rt builds 15 7
PR wall clock about 200 s about 120-135 s
adopter-facing reusables 9 4 (2 routine)
files per adopter up to 6 2
Gitea twins 11 6

Corrections (review, 2026-10-04, at origin/main e5fcf1d; branch protection read live)

  • Two swaps of the required checks, not one. #1540 removes eight contexts that main requires today: tests / workflow-schema, tests / dated-examples, tests / contract-paths, tests / shellcheck, check-self-bootstrap / check, gitea-twin-check / check, prep-order-check / check and readme-pin-check / digest. It needs its own swap at merge. #1542 swaps the 14 gate-wrapper contexts.
  • The parse gate's own contract conflicts with folding it into pr.yml. workflow-api.md says the parse check's wrapper "must be a sibling workflow, since a malformed workflow cannot run its own observer". A malformed pr.yml would drop every gate silently on an adopter with no required checks. #1539 decides this.
  • The frozen contract is a v1 target. workflow-api.md applies it "Starting at v1.0.0", and the latest cut is v0.65.0. So the ADR amends the v1 target; it does not break a promise in force.
  • The ADR number is 0013. #1529's ADR-0012 landed as e5fcf1d, inside v0.65.0. #1529 shipped with no config key: TestLoad_releaseNotesIsAnUnknownKey pins release_notes as unknown. So storey needs no release_notes key, and plan step 7's last bullet is moot.
  • The hard-coded reusable list appears twice in tests/workflows.bats, at lines 254 and 644.
  • The fail-closed resolve step is needed for a second reason. Each gate reusable finds the caller's pin by reading a wrapper with a fixed file name (e.g. manifest-check.yml). An adopter calling from pr.yml matches none, so the bake list and the fail-closed step close one hole together (#1541).
  • cellblock and purser are public repos, not private. All five are under the frankenbit org, which is what accepting the API break rests on.

Acceptance criteria

Each row ticks when its sub-issue's own criteria are done. This tracker is closed by hand once every row is ticked.

Order (revised 2026-10-05, operator decision): #1540, #1539 and #1541 are done.

  1. v0.66.0 is cut from rolling PR #1549. It ships reusable-pr-gates.yml alongside the old reusables.
  2. frankenbit/tmux-tell#963 migrates to pr-gates@v0.66.0 as the live probe of the tag-pinned adopter path, and its job log is read.
  3. #1542 removes the old reusables and swaps the required checks to pr / gates (pull_request), and v0.67.0 is cut.
  4. The other four adopters can migrate at v0.66.0 or later, once tmux-tell's log is read; they do not wait for #1542.

Issues that wait on another carry status/blocked. Found along the way: #1548 (the old reusables never ran the landing-tree replay).

  • #1539: ADR-0013 (Proposed, then Accepted by the operator) amends the v1 target in workflow-api.md (C3), cites #1304's hold condition, and decides where the parse gate runs. — DONE @ #1544 (44bdba5, reviewed as 44bdba5). Decided by the operator on 2026-10-04: D1-D8 as recommended, including the layout: combined | split input (default combined), the caller's status as the one required check, the parse gate inside pr.yml with required checks on adopters, and recover-pending-cut left out of v1.
  • #1540: toolkit-self consolidation: tests / checks, the grown toolkit-self-gates and its own swap of the required checks. — DONE @ #1546 (9968ed4, reviewed as 9968ed4). Measured: 22 to 15 jobs and 207 s to 173 s wall clock per toolkit-self PR; main requires 18 checks.
  • #1541: reusable-pr-gates.yml: — DONE @ #1547 (855b34e, reviewed as 855b34e). The caller context is pr / gates (pull_request). Also found there: #1548, where the old reusables never ran the replay.
    • every PR-time gate in one job, each on|off, verdicts namespaced and attested;
    • a generated twin;
    • a registration list derived from the bake marker and a fail-closed resolve step, both mutation-verified.
  • #1542: — DONE @ #1552 (0de3876, reviewed as 0de3876), cut as v0.67.0 (#1553, tag 8e59136). Measured: 8 jobs, 114 s summed, 122 s wall clock, 5 required checks per toolkit-self PR, against 22 jobs, 181 s and 207 s before.
    • the old gate reusables, wrappers and twins are gone;
    • the required-check swap leaves no red window;
    • toolkit-self runs at most 8 jobs and 6 required checks per PR, measured on the live forge;
    • the next minor is cut.
  • frankenbit/tmux-tell#963: the first adopter on two workflow files, with its job log read. — DONE @ tmux-tell#964 (af77cf0, reviewed as af77cf0), on pr-gates@v0.66.0. pr / gates is required on tmux-tell main, and the probe result is in comment 115189.
  • frankenbit/storey#69
  • frankenbit/ember#166
  • frankenbit/cellblock#181
  • frankenbit/purser#63
  • These stay unchanged throughout: fork-pr-approval-notice, recover-pending-cut, the release/release-cut split, and the scheduled, release and dispatch workflows. — DONE: git diff 44bdba5 8e59136 (from ADR-0013's landing to the v0.67.0 tag) is empty for fork-pr-approval-notice.yml, recover-pending-cut.yml, release-cut.yml, release.yml, goreleaser.yml, mirror-release.yml, mirror-drift-check.yml and prepared-uncut-check.yml. reusable-release.yml and reusable-recover-pending-cut.yml change only their baked-ref marker (main to v0.67.0), which is the cut's own bake. #1540 kept the push and schedule halves of the workflows it folded (review 7222).

Out of scope

  • Turning on register-check --git-log. It has been inert since a06cd67. Turning it on hits 4 historical commits, and its remedy is a force-push, which AGENTS.md section 15 forbids. That is a policy issue of its own.
  • The rebase-replay step, which never runs inside the reusables ("triggered by event: workflow_call"). It needs its own issue.
  • Folding recovery into the release workflow, inlining prepared-uncut or mirror, pruning reusable-release.yml's outputs, a generalized caller scan for the release workflow, and retiring the Gitea twins.

Anchor

Measurement and design workflows of 2026-10-02/03, during the release-notes work for frankenbit/storey (#1478, #1529). Prior art: #1253 (5 gates into toolkit-self-gates), #1304 (won't-do while slots were free), #1177 (advisory checks need their own contexts).

The runner queue is the binding constraint again. #1304 closed its consolidation tracker as won't-do and named the condition for reopening it: "a future state where the runner queue is the binding constraint again". That state is now measured, and the operator has confirmed it will last. This tracker covers a smaller adopter workflow surface and fewer toolkit-self jobs. It goes further than #1304 in one respect: the operator has also accepted breaking the reusable-workflow API, because every adopter is a repo under the same control. **Tracker since 2026-10-04.** The work is split into the sub-issues under "Acceptance criteria". The corrections found in review are under "Corrections". ## Measured Data comes from the live forge through the public API, on 2026-10-02/03, at origin/main `dd871e8` to `8086700`. **Concurrency is 1.** - Of 1,500 tasks, never more than one ran at a time. A re-check covered 277 tasks between 2026-10-02 22:24 and 2026-10-03 00:55: the maximum concurrency was 1. - On Sep 7-8 a second runner (`caymans-fedora`) also took jobs. That was the operator's workstation booted into Fedora, and it no longer runs one. - Alcatraz has no room for more parallel builds (operator, 2026-10-03, after asking the Alcatraz admin). **One toolkit-self PR** (runs 19373-19387; median over 25 heads): | What | Measured | |---|---| | jobs that run | 22 (173 s summed) | | required contexts on `main` | 25 | | `rt` builds | 15 | | wall clock from first job start to last finish | 200 s median | | time until all checks have reported | 246 s | **A small job takes about 5.5 s**, and about 87% of that is setup: - container setup 0.95 s; - checkout 0.79-1.87 s; - replay 1.06 s; - `rt` build 2.05 s. The gates themselves took 28 s of the 224 s that small jobs spent. The real work sits in two jobs: `bats` at about 34 s and `go-ci` at about 30 s. **The adopters** are five repos plus toolkit-self. Each wires up to six workflow files, at mixed pins (re-read on 2026-10-04; unchanged): | Adopter | Workflows wired | Pin(s) | |---|---|---| | storey | release, manifest-check | `@v0.63.0` | | tmux-tell | release, manifest-check, fragment-check | `@v0.63.1` | | ember | release, manifest-check | `@v0.42.0` | | | fragment-check | `@v0.56.0` | | cellblock | release, manifest-check, fragment-check | `@v0.42.0` | | purser | release, manifest-check, fragment-check, changelog-body-check | `@v0.36.0` | ## Why #1304's reasons no longer decide this #1304 comment 109121 gave three reasons. Here is where each stands now. 1. **"Consolidation trades wall-clock for slots, and only pays when slots are contended."** Slots are now contended permanently, so every job removed saves its full prefix. Merged jobs stay shorter than the critical path (`bats`, about 34 s), so wall-clock does not get worse at any slot count. 2. **"The triggers do not form a union."** That is still true, and the plan respects it: - Only `pull_request` gates merge. - `fork-pr-approval-notice` (`pull_request_target`, the trust boundary) stays separate. - So do the push, schedule, release and dispatch workflows. - So do the `release.yml` / `release-cut.yml` concurrency split (ADR-0010) and `recover-pending-cut`, which stays dispatch-only because nothing may fire it without a person. 3. **The residual risk: gates in one job share one checkout and one `$RUNNER_TEMP`, and nothing enforces that they do not collide.** The plan enforces it: - each gate writes a verdict file in its own namespace; - the attest step requires the exact expected set. ## Plan (reviewed design, sizes estimated) 1. **PR-A (S, `no-changelog`)**, now #1539. The next free ADR (0013), written as a decision tree. It supersedes the frozen-API statement in `docs/architecture/contracts/workflow-api.md` and records why #1304's hold condition is met. 2. **PR-B (L, `added`, additive)**, now #1541. Adds `reusable-pr-gates.yml` and its twin. Its job runs every PR-time gate as a step, with one checkout and one `rt` build: - manifest, fragments, changelog body, register, ac-closure and workflow parse; - each gate is `on` or `off`, and the job is red if any `on` gate fails. There is no in-job "advisory" mode: this forge has no job-log API, so a warning inside a green job is invisible (#1177); - defaults are `on` only for manifest and fragments. Toolkit-self calls it from a local `pr.yml` with all gates on. The PR also carries: - the bake, compose and `NeedsSecrets` lists; - a bake-marker-derived list for the `workflows.bats` registration guard (today a hard-coded list of 9); - a fail-closed resolve step: refuse when the baked value is `main`, no caller pin is found, and the repository is not toolkit-self. Otherwise a tag-pinned adopter would silently run `main`; - the parse-self mutation control, moved into `tests / workflow-schema`; - mutation-verified tests. The old wrappers keep running. 3. **PR-C (M, additive)**, now #1540. - Merge `tests.yml`'s four short jobs into one `tests / checks` job, porting the build, tree-state and attest guards from `toolkit-self-gates` (#1323). - Grow `toolkit-self-gates` by the PR halves of check-self-bootstrap and gitea-twin-check, by prep-order-check and by readme-pin `digest`. - The advisory checks (`readme-pin-check / mirror`, `base-divergence-check`) stay separate jobs. 4. **One admin edit:** swap the required-context list on `main` just before PR-D merges, after approving PR-D. *(Corrected below: two swaps.)* 5. **PR-D (M-L, `removed`)**, now #1542. It needs a `BREAKING CHANGE` line naming the renamed contexts (#1479). It deletes: - the 6 gate wrappers; - the 6 gate reusables and their 6 twins, in one PR, since `gitea_twin.go` fails on orphans; - the stale `pre_push.go` rows; - the matching docs, examples and README text. 6. **One cut** (the next minor, v0.66.0 or later; v0.65.0 was cut on 2026-10-04). Old tags keep working. Part of #1542. 7. **Adopters, one PR each**, one issue each in the adopter's repo: - **tmux-tell first.** It is the only live probe of the adopter path: tag fetch, baked ref and cross-repo call. Read its job log, not just its status (AGENTS.md section 8, Axis C). - **Then storey, ember, cellblock and purser.** Set `RELEASE_TOOLKIT_MINISIGN_PUBLIC_KEY` first for adopters jumping past v0.57.0 (#1479). - **Each PR:** deletes the gate wrappers, adds a `pr.yml` with only the gates that adopter already ran, and bumps `release.yml`'s pin. - ~~If #1529 ships in the same cut, storey adds its `release_notes` key in the same PR.~~ Moot (see Corrections). Expected result (estimated, one slot): | | Today | After | |---|---|---| | toolkit-self jobs per PR | 22 | 8 | | required contexts | 25 | 6 | | `rt` builds | 15 | 7 | | PR wall clock | about 200 s | about 120-135 s | | adopter-facing reusables | 9 | 4 (2 routine) | | files per adopter | up to 6 | 2 | | Gitea twins | 11 | 6 | ## Corrections (review, 2026-10-04, at origin/main `e5fcf1d`; branch protection read live) - **Two swaps of the required checks, not one.** #1540 removes eight contexts that `main` requires today: `tests / workflow-schema`, `tests / dated-examples`, `tests / contract-paths`, `tests / shellcheck`, `check-self-bootstrap / check`, `gitea-twin-check / check`, `prep-order-check / check` and `readme-pin-check / digest`. It needs its own swap at merge. #1542 swaps the 14 gate-wrapper contexts. - **The parse gate's own contract conflicts with folding it into `pr.yml`.** `workflow-api.md` says the parse check's wrapper "must be a sibling workflow, since a malformed workflow cannot run its own observer". A malformed `pr.yml` would drop every gate silently on an adopter with no required checks. #1539 decides this. - **The frozen contract is a v1 target.** `workflow-api.md` applies it "Starting at v1.0.0", and the latest cut is v0.65.0. So the ADR amends the v1 target; it does not break a promise in force. - **The ADR number is 0013.** #1529's ADR-0012 landed as `e5fcf1d`, inside v0.65.0. #1529 shipped with no config key: `TestLoad_releaseNotesIsAnUnknownKey` pins `release_notes` as unknown. So storey needs no `release_notes` key, and plan step 7's last bullet is moot. - **The hard-coded reusable list appears twice** in `tests/workflows.bats`, at lines 254 and 644. - **The fail-closed resolve step is needed for a second reason.** Each gate reusable finds the caller's pin by reading a wrapper with a fixed file name (e.g. `manifest-check.yml`). An adopter calling from `pr.yml` matches none, so the bake list and the fail-closed step close one hole together (#1541). - **cellblock and purser are public repos, not private.** All five are under the `frankenbit` org, which is what accepting the API break rests on. ## Acceptance criteria Each row ticks when its sub-issue's own criteria are done. This tracker is closed by hand once every row is ticked. **Order (revised 2026-10-05, operator decision):** #1540, #1539 and #1541 are done. 1. v0.66.0 is cut from rolling PR #1549. It ships `reusable-pr-gates.yml` alongside the old reusables. 2. frankenbit/tmux-tell#963 migrates to `pr-gates@v0.66.0` as the live probe of the tag-pinned adopter path, and its job log is read. 3. #1542 removes the old reusables and swaps the required checks to `pr / gates (pull_request)`, and v0.67.0 is cut. 4. The other four adopters can migrate at v0.66.0 or later, once tmux-tell's log is read; they do not wait for #1542. Issues that wait on another carry `status/blocked`. Found along the way: #1548 (the old reusables never ran the landing-tree replay). - [x] #1539: ADR-0013 (Proposed, then Accepted by the operator) amends the v1 target in `workflow-api.md` (C3), cites #1304's hold condition, and decides where the parse gate runs. — DONE @ #1544 (`44bdba5`, reviewed as `44bdba5`). Decided by the operator on 2026-10-04: D1-D8 as recommended, including the `layout: combined | split` input (default `combined`), the caller's status as the one required check, the parse gate inside `pr.yml` with required checks on adopters, and recover-pending-cut left out of v1. - [x] #1540: toolkit-self consolidation: `tests / checks`, the grown `toolkit-self-gates` and its own swap of the required checks. — DONE @ #1546 (`9968ed4`, reviewed as `9968ed4`). Measured: 22 to 15 jobs and 207 s to 173 s wall clock per toolkit-self PR; `main` requires 18 checks. - [x] #1541: `reusable-pr-gates.yml`: — DONE @ #1547 (`855b34e`, reviewed as `855b34e`). The caller context is `pr / gates (pull_request)`. Also found there: #1548, where the old reusables never ran the replay. - every PR-time gate in one job, each `on|off`, verdicts namespaced and attested; - a generated twin; - a registration list derived from the bake marker and a fail-closed resolve step, both mutation-verified. - [x] #1542: — DONE @ #1552 (`0de3876`, reviewed as `0de3876`), cut as **v0.67.0** (#1553, tag `8e59136`). Measured: **8 jobs, 114 s summed, 122 s wall clock, 5 required checks** per toolkit-self PR, against 22 jobs, 181 s and 207 s before. - the old gate reusables, wrappers and twins are gone; - the required-check swap leaves no red window; - toolkit-self runs at most 8 jobs and 6 required checks per PR, measured on the live forge; - the next minor is cut. - [x] frankenbit/tmux-tell#963: the first adopter on two workflow files, with its job log read. — DONE @ tmux-tell#964 (`af77cf0`, reviewed as `af77cf0`), on `pr-gates@v0.66.0`. `pr / gates` is required on tmux-tell main, and the probe result is in comment 115189. - [ ] frankenbit/storey#69 - [ ] frankenbit/ember#166 - [ ] frankenbit/cellblock#181 - [ ] frankenbit/purser#63 - [x] These stay unchanged throughout: `fork-pr-approval-notice`, `recover-pending-cut`, the `release`/`release-cut` split, and the scheduled, release and dispatch workflows. — DONE: `git diff 44bdba5 8e59136` (from ADR-0013's landing to the v0.67.0 tag) is empty for `fork-pr-approval-notice.yml`, `recover-pending-cut.yml`, `release-cut.yml`, `release.yml`, `goreleaser.yml`, `mirror-release.yml`, `mirror-drift-check.yml` and `prepared-uncut-check.yml`. `reusable-release.yml` and `reusable-recover-pending-cut.yml` change only their baked-ref marker (`main` to `v0.67.0`), which is the cut's own bake. #1540 kept the push and schedule halves of the workflows it folded (review 7222). ## Out of scope - Turning on `register-check --git-log`. It has been inert since `a06cd67`. Turning it on hits 4 historical commits, and its remedy is a force-push, which AGENTS.md section 15 forbids. That is a policy issue of its own. - The rebase-replay step, which never runs inside the reusables ("triggered by event: workflow_call"). It needs its own issue. - Folding recovery into the release workflow, inlining prepared-uncut or mirror, pruning `reusable-release.yml`'s outputs, a generalized caller scan for the release workflow, and retiring the Gitea twins. ## Anchor Measurement and design workflows of 2026-10-02/03, during the release-notes work for frankenbit/storey (#1478, #1529). Prior art: #1253 (5 gates into `toolkit-self-gates`), #1304 (won't-do while slots were free), #1177 (advisory checks need their own contexts).
claude changed title from chore(ci): one PR-gates reusable and fewer toolkit-self jobs — the runner is down to one slot, which is #1304's hold condition to chore(ci): one PR-gates reusable and fewer toolkit-self jobs, because the runner is down to one slot (#1304's hold condition) 2026-10-04 10:56:53 +02:00
Author
Member

Plan review of this tracker and its sub-issues, read at origin/main 6b5d554 with branch protection read live today.

Summary: the plan holds. Its numbers check out: the 25 contexts on main, the 8 that #1540 removes, the 14 from the gate wrappers, and the adopter wrappers and pins. I found four gaps worth fixing in the issue bodies before the implementers reach them. None of them changes the design.

Verified: main requires exactly 25 contexts. #1540's eight (tests / workflow-schema, dated-examples, contract-paths, shellcheck, check-self-bootstrap / check, gitea-twin-check / check, prep-order-check / check, readme-pin-check / digest) are all among them. The gate wrappers account for 14 more (2+2+2+2+3+3), and the remaining three are tests / bats, go-ci / lint + build + test and toolkit-self-gates. canonicalFiles has 9 entries, and the two tests/workflows.bats lists are at 254 and 644, as stated.

1. #1540 needs a carry-forward list (AGENTS.md section 1)

Folding jobs into one job drops per-job preconditions unless each one is carried over. These are load-bearing today:

  • check-self-bootstrap.yml skips release-prep/ heads with a job-level if: (its comment explains why). toolkit-self-gates has no such skip. Moved as a plain step, the check starts running on the rolling PR.
  • fetch-depth: 0 is load-bearing for dated-examples (tests.yml:299-304 says a shallow clone gets an empty population and refuses), for contract-paths (tests.yml:625) and for check-self-bootstrap. workflow-schema and shellcheck use depth 1. The merged tests / checks job needs the deepest of them.
  • prep-order-check needs pull-requests: read and FORGEJO_TOKEN: secrets.GITHUB_TOKEN. readme-pin-check / digest deliberately runs with no token (readme-pin-check.yml:105). In one job the permissions become the union, so keep the token-less property at step level and say so.
  • toolkit-self-gates fires on edited; the four checks moving into it did not. After the move they re-run on every PR-body edit. That is harmless, but it costs a slot. Also, readme-pin-check has workflow_dispatch, and digest loses it if it moves.
  • The required context is tests / checks only if the job id is checks with no name: (or name: checks).

2. #1541 undercounts the hand-kept lists in tests/workflows.bats

Besides lines 254 and 644:

  • if len(scripts) != 18 (about line 1320) is 9 reusables times 2 providers. A tenth reusable makes it 20.
  • The wrapper_names maps (about 851, 1323, 1601) and wrapper_names.get(reusable, reusable.removeprefix("reusable-")) (1394, 1661) derive the caller file as pr-gates.yml for reusable-pr-gates.yml. ADR-0013 D4 scans every workflow file instead, so this resolve-ref harness needs rewriting for the new reusable, not just a new entry.
  • The reusable-to-wrapper pairs at 855-860 and the gates=(...) array at 2111 ("all 4 PR-CI gates consume the shared bootstrap artifact").

3. #1542's "at most 6 required checks" depends on ADR-0013 D2

A reusable called from a wrapper posts two contexts: the caller job and the reusable's job. The 6 counts tests / bats, tests / checks, go-ci, toolkit-self-gates and the two pr contexts. That holds only with the parse gate inside pr.yml, which is D2's recommendation. A sibling parse workflow adds two more. Step 1 of #1542 ("requires the pr check") also needs to name which of the two contexts, or both.

4. The adopter issues do not carry D2's required-status step

If D2 is accepted as recommended, the safety net for a pr.yml that fails to parse is a required status on each adopter's main. None of tmux-tell#963, storey#69, ember#166, cellblock#181 or purser#63 mentions it. Each needs an AC for the admin edit: enable status checks and require the PR-gates context, but only after that context has posted on the adopter's rolling release PR (#809's shape). Each pr.yml also needs edited in its pull_request types for the body-reading gates, and no paths: filter (#644). If D2 goes the other way, the adopter issues instead need the sibling parse workflow.

My review of the ADR PR itself is on #1544 (review 7220).

Plan review of this tracker and its sub-issues, read at origin/main 6b5d554 with branch protection read live today. Summary: the plan holds. Its numbers check out: the 25 contexts on `main`, the 8 that #1540 removes, the 14 from the gate wrappers, and the adopter wrappers and pins. I found four gaps worth fixing in the issue bodies before the implementers reach them. None of them changes the design. **Verified:** `main` requires exactly 25 contexts. #1540's eight (`tests / workflow-schema`, `dated-examples`, `contract-paths`, `shellcheck`, `check-self-bootstrap / check`, `gitea-twin-check / check`, `prep-order-check / check`, `readme-pin-check / digest`) are all among them. The gate wrappers account for 14 more (2+2+2+2+3+3), and the remaining three are `tests / bats`, `go-ci / lint + build + test` and `toolkit-self-gates`. `canonicalFiles` has 9 entries, and the two `tests/workflows.bats` lists are at 254 and 644, as stated. ## 1. #1540 needs a carry-forward list (AGENTS.md section 1) Folding jobs into one job drops per-job preconditions unless each one is carried over. These are load-bearing today: - `check-self-bootstrap.yml` skips `release-prep/` heads with a job-level `if:` (its comment explains why). `toolkit-self-gates` has no such skip. Moved as a plain step, the check starts running on the rolling PR. - `fetch-depth: 0` is load-bearing for `dated-examples` (`tests.yml:299-304` says a shallow clone gets an empty population and refuses), for `contract-paths` (`tests.yml:625`) and for `check-self-bootstrap`. `workflow-schema` and `shellcheck` use depth 1. The merged `tests / checks` job needs the deepest of them. - `prep-order-check` needs `pull-requests: read` and `FORGEJO_TOKEN: secrets.GITHUB_TOKEN`. `readme-pin-check / digest` deliberately runs with no token (`readme-pin-check.yml:105`). In one job the permissions become the union, so keep the token-less property at step level and say so. - `toolkit-self-gates` fires on `edited`; the four checks moving into it did not. After the move they re-run on every PR-body edit. That is harmless, but it costs a slot. Also, `readme-pin-check` has `workflow_dispatch`, and `digest` loses it if it moves. - The required context is `tests / checks` only if the job id is `checks` with no `name:` (or `name: checks`). ## 2. #1541 undercounts the hand-kept lists in `tests/workflows.bats` Besides lines 254 and 644: - `if len(scripts) != 18` (about line 1320) is 9 reusables times 2 providers. A tenth reusable makes it 20. - The `wrapper_names` maps (about 851, 1323, 1601) and `wrapper_names.get(reusable, reusable.removeprefix("reusable-"))` (1394, 1661) derive the caller file as `pr-gates.yml` for `reusable-pr-gates.yml`. ADR-0013 D4 scans every workflow file instead, so this resolve-ref harness needs rewriting for the new reusable, not just a new entry. - The reusable-to-wrapper pairs at 855-860 and the `gates=(...)` array at 2111 ("all 4 PR-CI gates consume the shared bootstrap artifact"). ## 3. #1542's "at most 6 required checks" depends on ADR-0013 D2 A reusable called from a wrapper posts two contexts: the caller job and the reusable's job. The 6 counts `tests / bats`, `tests / checks`, `go-ci`, `toolkit-self-gates` and the two `pr` contexts. That holds only with the parse gate inside `pr.yml`, which is D2's recommendation. A sibling parse workflow adds two more. Step 1 of #1542 ("requires the `pr` check") also needs to name which of the two contexts, or both. ## 4. The adopter issues do not carry D2's required-status step If D2 is accepted as recommended, the safety net for a `pr.yml` that fails to parse is a required status on each adopter's `main`. None of tmux-tell#963, storey#69, ember#166, cellblock#181 or purser#63 mentions it. Each needs an AC for the admin edit: enable status checks and require the PR-gates context, but only after that context has posted on the adopter's rolling release PR (#809's shape). Each `pr.yml` also needs `edited` in its `pull_request` types for the body-reading gates, and no `paths:` filter (#644). If D2 goes the other way, the adopter issues instead need the sibling parse workflow. My review of the ADR PR itself is on #1544 (review 7220).
Author
Member

Plan review 114846 is now folded into the issue bodies. I verified each point at origin/main 6b5d554 before editing.

  1. #1540: a new "Carry-forward" section and an acceptance criterion for the PR's carry-forward table. The implementer has the list.

  2. #1541: the hand-kept lists in tests/workflows.bats are now all named: lines 254 and 644, len(scripts) != 18 at 1389 and 1656, wrapper_names at 1322 and 1600, the removeprefix derivation at 1394 and 1661, the pairs at 855-860 and the gates array at 2111. A new criterion covers the resolve-ref harness rewrite.

  3. #1542: the job and check counts now depend on ADR-0013 D2. The swap must name the exact PR-gates context or contexts, since a wrapper call posts two (caller and reusable job).

  4. tmux-tell#963, storey#69, ember#166, cellblock#181 and purser#63: each now has two new criteria.

    • the pr.yml wiring rule: edited in the types, and no paths: filter;
    • the parse safety net from ADR-0013 D2: a required PR-gates context, added only after it has posted on the rolling release PR, or a sibling parse workflow.

    Each also states its repo's current required-status setting.

The ADR's own findings from review 7220 are with its author for a follow-up commit on #1544.

Plan review 114846 is now folded into the issue bodies. I verified each point at origin/main `6b5d554` before editing. 1. **#1540:** a new "Carry-forward" section and an acceptance criterion for the PR's carry-forward table. The implementer has the list. 2. **#1541:** the hand-kept lists in `tests/workflows.bats` are now all named: lines 254 and 644, `len(scripts) != 18` at 1389 and 1656, `wrapper_names` at 1322 and 1600, the `removeprefix` derivation at 1394 and 1661, the pairs at 855-860 and the gates array at 2111. A new criterion covers the resolve-ref harness rewrite. 3. **#1542:** the job and check counts now depend on ADR-0013 D2. The swap must name the exact PR-gates context or contexts, since a wrapper call posts two (caller and reusable job). 4. **tmux-tell#963, storey#69, ember#166, cellblock#181 and purser#63:** each now has two new criteria. - the `pr.yml` wiring rule: `edited` in the types, and no `paths:` filter; - the parse safety net from ADR-0013 D2: a required PR-gates context, added only after it has posted on the rolling release PR, or a sibling parse workflow. Each also states its repo's current required-status setting. The ADR's own findings from review 7220 are with its author for a follow-up commit on #1544.
Author
Member

The adopter probe passed. frankenbit/tmux-tell#964 landed as af77cf0, exactly the reviewed head (review 7241); Alex merged it on 2026-10-05 at 21:20. tmux-tell is the first adopter on reusable-pr-gates.yml@v0.66.0.

From the job logs, read in the built-in browser:

  • PR run 2306 (pr / gates):
    • resolved toolkit ref: v0.66.0 (source=baked), with the per-gate script taken from .release-toolkit/;
    • bootstrap rt: FETCH published asset @ v0.66.0, then minisign signature OK, signed for v0.66.0 (trusted comment matches) and checksum OK (189ebf7f…);
    • the landing-tree replay passed (#1548);
    • manifest and fragments passed, and the four off gates were skipped;
    • the tree was unchanged, and attest passed.
  • Push run 2310 (reusable-release.yml@v0.66.0): resolved v0.66.0 the same way, then FETCHed and verified rt. The v0.64.0 PAT scope check passed as release-bot, and the run opened rolling PR tmux-tell#965 (v0.40.0).
  • Cost: the first run after a pin move pays about 3 minutes to save the rt cache. That is now #1550, and later runs take about 7 s.

For the other adopters: storey, ember, cellblock and purser can migrate at v0.66.0. Copy tmux-tell's pr.yml, setting the gates each one already ran, and bump release.yml. ember, cellblock and purser need RELEASE_TOOLKIT_MINISIGN_PUBLIC_KEY first, because they jump past v0.57.0. It is now also set at the org scope; check that per repo.

Still open on tmux-tell#963: pr / gates must post on rolling PR #965. Then the operator makes it required on tmux-tell's main (ADR-0013 D4).

**The adopter probe passed.** frankenbit/tmux-tell#964 landed as `af77cf0`, exactly the reviewed head (review 7241); Alex merged it on 2026-10-05 at 21:20. tmux-tell is the first adopter on `reusable-pr-gates.yml@v0.66.0`. From the job logs, read in the built-in browser: - **PR run 2306 (`pr / gates`):** - `resolved toolkit ref: v0.66.0 (source=baked)`, with the per-gate script taken from `.release-toolkit/`; - `bootstrap rt: FETCH published asset @ v0.66.0`, then `minisign signature OK`, `signed for v0.66.0 (trusted comment matches)` and `checksum OK (189ebf7f…)`; - the landing-tree replay passed (#1548); - manifest and fragments passed, and the four off gates were skipped; - the tree was unchanged, and attest passed. - **Push run 2310 (`reusable-release.yml@v0.66.0`):** resolved `v0.66.0` the same way, then FETCHed and verified `rt`. The v0.64.0 PAT scope check passed as release-bot, and the run opened rolling PR tmux-tell#965 (v0.40.0). - **Cost:** the first run after a pin move pays about 3 minutes to save the `rt` cache. That is now #1550, and later runs take about 7 s. **For the other adopters:** storey, ember, cellblock and purser can migrate at `v0.66.0`. Copy tmux-tell's `pr.yml`, setting the gates each one already ran, and bump `release.yml`. ember, cellblock and purser need `RELEASE_TOOLKIT_MINISIGN_PUBLIC_KEY` first, because they jump past v0.57.0. It is now also set at the org scope; check that per repo. **Still open on tmux-tell#963:** `pr / gates` must post on rolling PR #965. Then the operator makes it required on tmux-tell's main (ADR-0013 D4).
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#1530
No description provided.