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
Labels
No labels
bump
major
bump
minor
bump
patch
kind
bug
kind
chore
kind
docs
kind
epic
kind
feature
loop/paused
mirror-drift
priority
critical
priority
high
priority
low
priority
medium
size
L
size
M
size
S
size
XL
status
blocked
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
frankenbit/release-toolkit#1541
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #1530. Implements ADR-0013 (
docs/adr/0013-one-pr-gates-reusable.md, accepted 2026-10-04 via #1544; details in0013-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 onereusable-pr-gates.yml, wired from onepr.yml. Itslayoutinput decides how the gates map to jobs:combined(one job) suits scarce runner slots, andsplit(one job per gate) suits free ones. The old reusables and wrappers keep running until #1542.What ADR-0013 decided, as it applies here
on|off.layout: combined | splitinput;combinedis the default.combined: everyongate is a step in one job, with one checkout and onertbuild. A last step requires each gate's verdict file.split: one job perongate, plus a result job thatneeds:all of them, runs withif: always(), and checksneeds.<job>.resultfor everyongate. A skipped job postssuccess, so withoutalways()a wrongly skipped gate would leave everything green.layoutvalue refuses.pr.yml, e.g.pr / gates (pull_request). That name is the same in both layouts.pr.yml.pr.yml. Thepull_requesttypes includeedited, because body-reading gates must re-run, and there is nopaths:filter (#644 / #629).onfor manifest and fragments only, and no advisory mode.main, no caller pin is found, and the caller is not toolkit-self.@mainarm, the #336 SHA arm and the #1222 fallback.Measured at origin/main
e5fcf1d, extended at6b5d554by 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.batsrepeats that list twice, at lines 254 and 644.if len(scripts) != 18(lines 1389 and 1656), which is 9 reusables times 2 providers;wrapper_namesmaps (lines 1322 and 1600);casespairs (lines 855-860);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.ymlreads.forgejo/workflows/manifest-check.yml. Apr.ymlmatches 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 givepr-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.
v*tags there.fromJSON. If that does not work here, use the ADR's fallback forsplit: static per-gate jobs withif:.splitsaves 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.ymlruns manifest, fragments, changelog body, register, ac-closure and workflow parse. Each gate ison|off; defaults areonfor manifest and fragments only. The job is red if anyongate fails. — DONE @ #1547 (855b34e, reviewed as855b34e). On the final head,pr / gates(run 19826) ran all six gates and attested.layout: combined | splitworks as D2 describes:combinedis the default, an unknown value refuses, and both layouts run the same per-gate steps. Mutation-verified in BOTH layouts: a skipped, deleted or failingongate turns the run red. — DONE @ #1547 (855b34e, reviewed as855b34e).splitwith the real gates ran live on the second head (run 19770, every gate on and success). Both layouts are mutation-verified intests/pr-gates.bats(review 7235).split's result job runs withif: always()and checksneeds.<job>.resultfor everyongate. A mutant withoutalways()shows the hole it closes. — DONE @ #1547 (855b34e, reviewed as855b34e). The no-always()mutant was measured live on the probe repo: the caller stayed green. The reviewer killed mutant Ma (on:skippedaccepted).pr.ymlfollows 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 as855b34e). Measured onfrankenbit/pr-gates-probe-1541; see comment 115075. A single non-matrix inner failure turns the caller red, skipped inner jobs leave it green, and afromJSONmatrix works.gitea-twin-checkpasses. — DONE @ #1547 (855b34e, reviewed as855b34e)..gitea/workflows/reusable-pr-gates.yml.gitea-twin-checkis green on the head and on push:main (run 19832).tests/workflows.batsis either derived from the bake marker or deliberately left fixed, with the reason in the PR. Mutation-verified: a reusable left out ofcanonicalFilesturns the registration check red. — DONE @ #1547 (855b34e, reviewed as855b34e). The registration list is derived from the bake marker, and thecanonicalFilesomission mutant is killed. The PR's decision tree gives each fixed list's reason.@main, SHA and #1222 arms still pass their tests. — DONE @ #1547 (855b34e, reviewed as855b34e). 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).pr.yml. — DONE @ #1547 (855b34e, reviewed as855b34e).pr.ymluseslayout: combined,types: [opened, synchronize, reopened, edited]and nopaths:. The caller context ispr / gates (pull_request).on,layout: combined, and follows the wiring rule (editedin the types, nopaths:).tests / checks(the job that now runs the workflow-schema step, after #1540). — DONE @ #1547 (855b34e, reviewed as855b34e). It is step c13 oftests / checks, and run 19774 logsnegative control OK.docs/architecture/contracts/workflow-api.md) documentsreusable-pr-gates.yml's inputs, includinglayoutand every gate switch, with the bake, compose andNeedsSecretslists updated. — DONE @ #1547 (855b34e, reviewed as855b34e).workflow-api.mddocuments all 21 inputs and thepr / gates (pull_request)status.addednames the new reusable. — DONE @ #1547 (855b34e, reviewed as855b34e).changelog.d/1541-reusable-pr-gates.added.md.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.
reusable-pr-gates.ymltakes alayout: combined | splitinput (ADR-0013 D2). Both layouts run the same per-gate step logic, and both are mutation-verified.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 withif: always()(or!cancelled()). A job whoseneedsfailed or was skipped is itself skipped, and a skipped job postssuccess. Withoutalways(), a mistakenly skippedongate leaves the result job and the caller green (comparegoreleaser.yml:506). This needs a mutant.combinedrelies on exactly this case.fromJSON. No workflow here uses one. The ADR gives a fallback that needs none.split's wall-clock saving on a runner with more than one free slot.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 callerfragment-check / checkfailed 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.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.
.forgejo/workflows,.gitea/workflowsor.github/workflowsin the repo (404 each).probebranch only, with no tags, and opened PR #1 there (head21feae0). After the runs,mainstill has none of the three directories, andprobeholds exactly those 10 files.reusable-pr-gates.yml's job graph:gatesjob;aandbjobs thatneedit;resultjob, withif: ${{ always() && inputs.layout == 'split' }}.echoorexit.<wf> / gates)REFUSED b:on:skipped)always()fromJSON(inputs.gates), and one fromfromJSON(needs.plan.outputs.gates)What this answers.
fromJSONmatrix works, both from an input and from a job output.split, so that the result job can readneeds.<job>.resultfor each gate.Closing by hand: all eleven acceptance criteria are ticked.
855b34e76b386d6aeb791ebdd1e9176ede36307a, which is exactly the reviewed head. Alex merged it on 2026-10-05 at 19:58, and the head branch is deleted.855b34e, posted as a COMMENT), plus Alex's approval on the forge.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.offis refused, in both layouts;pr / gates (pull_request).frankenbit/pr-gates-probe-1541is still there for the operator to delete. Its measurements are in comment 115075.