ci: run the heavy workflows on pull_request for any base branch - #16893
Conversation
`ci.yml` and `lint.yml` both filtered their `pull_request:` trigger to `branches: [main]`, so a PR whose base is a feature branch started neither workflow. A workflow that does not run publishes no check run at all -- an absence, not a skip -- so all six branch-protection-required contexts (`Lint & Repo Gates`, `TypeScript Type Check`, `Test Core`, `Build Core`, `Dogfood Regression Gate`, `Temporal Conformance`) reported nothing on every such PR. Remove the base filter from `pull_request:` in both files. `push:` keeps its `branches: [main]` filter (it is per-branch, not per-PR; unfiltering it would run on every push to every agent branch), and the `merge_group:` trigger is untouched in both files. Runner cost, measured over 2026-08-09 -> 2026-09-08: 6 of 4000 PRs had a base other than `main` (0.15%), all six the #14478/#15679 stack. Against the window's 7008 pull_request runs per workflow that is ~2.5 extra runs/week per workflow, ~57 job-runs/week across both files; the pessimistic one-run-per-commit bound is 1.6% of current pull_request spend. Small, so the trigger is widened unconditionally rather than gated on a label. Pin: `check-required-contexts.mjs` grows assertion (7c) -- no `branches:` / `branches-ignore:` on the `pull_request:` trigger of any workflow publishing a required context -- beside its existing `paths:` and `types:` guards, with a (7c) self-test battery pinning both directions. The registry it already derives from supplies the context list, so nothing is transcribed. The MEASURED 2026-08-25 (#12211) block in `lint.yml` is preserved verbatim; a note above the trigger records why that merge_group-coverage result does not bind on a `pull_request` base filter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
ACCEPT in substance — PR #16893, reviewed against GitHub and the tree, ⛔ not against the reportFlip + arm withheld until the four still-running checks report (below). Everything else is verified. What I re-drove myself
⭐ The file-surface breach was FORCED, not discretionary — and I confirmed the mechanismThe claim fenced the surface to the two workflows plus a pin; the pin landed in
assert(source !== sources[file], `fixture '${label}': its ${file} anchor no longer matches — the assertion below would judge the pristine workflow`);and five fixtures anchored on the literal ⇒ Breach accepted. ⛔ The defect is mine: the surface was scoped from the card's Pin bullet, which named a non-existent file. Logged as dispatch-brief defect 5. ⛔ My brief's commit-trailer instruction was WRONG and the dev was right to override itI wrote "Reference the card as
and its self-test at
|
|
Nothing else in the ACCEPT changes: the review, the six re-driven verifications and the two open items stand as posted. Generated by Claude Code |
Part of #16482
ci.ymlandlint.ymlboth filtered theirpull_request:trigger tobranches: [main], so a PR whose base is a feature branch started neither workflow. A workflow that does not run publishes no check run at all — an absence, not a skip — so all six branch-protection-required contexts reported nothing on every such PR.The whole workflow diff removes exactly four lines — the two
branches:/- mainpairs underpull_request:. Nothing else is removed from either file.1. The measurement, taken before the shape was chosen
The card's⚠️ requires the runner-cost delta before landing, and the shape of the fix depends on it.
Window — 2026-08-09 → 2026-09-08 (30 days), complete.
GET /pulls?state=all&sort=created&direction=desc, paged to 4000 PRs; because the sort iscreateddescending, every PR created at or after the oldest row (2026-08-09T01:24:30Z) is in the set.Count — 3 distinct base refs in the window:
mainclaude/issue-14478-duration-unit-in-key-nameclaude/issue-15679-system-duration-unit-in-key-name6 of 4000 PRs (0.15%) had a non-
mainbase, and all six are the #14478 / #15679 stack this card was split out of. There is no second population.Heavy matrix width, read from the workflow files rather than assumed — 23 job-runs per PR event:
ci.yml→ 17 (test×6 shards,dogfood×3 shards, plusfilter,test-gate,temporal-conformance,dogfood-verify,dogfood-gate,build-core,build-docs,console-pin)lint.yml→ 6 (lint,typecheck-source-gates,typecheck-workspace,typecheck-debt,typecheck-consumers,typecheck)Arithmetic. Current spend in the same window, from the Actions API: 7008
pull_requestruns for each workflow (merge_group4999,push3584 — unaffected either way). That is 7008 / 3994 = 1.75 runs per PR, the measured multiplier for pushes after the first.concurrency.cancel-in-progresscollapses bursts), 115 × 23 = 2645 job-runs / 30 days ≈ 617/week, still only 1.6% of currentpull_requestspend.Conclusion → unconditional widening. Both bounds are small, so the trigger is widened outright rather than gated on a label or on "the base branch is an open PR's head". Those fallbacks buy at most 1.6% of one event class and cost a conditional that can itself fail open.
One honest caveat: this is the population under the current regime, where stacks are ruled an unsupported form (#16149). It is a lower bound if that ever changes.
2.
merge_groupis untouched — obligation 3ci.yml:12-15states that every workflow producing a required check must keep itsmerge_grouptrigger or queue builds wait forever. Read before editingon:, and honoured:git diffover both files shows no-line touchingmerge_group,concurrency,group:orcancel-in-progress. The only removals are the fourbranches:lines quoted above.merge_group:is still present exactly once in each file.concurrencyblocks are byte-identical. Their key isgithub.event.pull_request.number || github.ref; a PR event still groups by PR number, a queue build still falls through togithub.ref. Removing a base filter changes neither branch of that expression.check:required-contextsassertion 6 already fails the build if either file ever losesmerge_group:, so this is machine-held from here on, not just reviewed.push:deliberately keeps itsbranches: [main]filter.push:is per-branch, not per-PR; unfiltering it would run the heavy matrix on every push to every agent branch — a far larger population than the 6 PRs measured above, and not what this card asks for.3.
lint.yml's MEASURED block — obligation 4Read in full before touching
lint.yml, and preserved verbatim; the diff only adds lines above it.It does not bind on this change, and the reason is what it measured. That block is a
merge_groupcoverage result — "is there a queue-build gap to close" — answered no, plus a ⛔ against addingmerge_groupto the advisory workflows. It says nothing about thepull_request:trigger'sbranches:filter. Its own stated residual is two repository settings (a commit reaching main with no queue build;strict_required_status_checks_policy: false), which it calls "neither reachable from this file". Removing a base filter frompull_request:changes nomerge_groupbehaviour at all. A note above the trigger records that reasoning in place, so the next reader does not have to re-derive it.4. The pin — obligation 5
scripts/check-required-contexts.mjsgrows assertion (7c): nobranches:/branches-ignore:on thepull_request:trigger of any workflow publishing a required context. It sits beside the existingpaths:(7) andtypes:(7b) guards, which is the same wedge reached through the two other axes of the same trigger.The context list is derived, not transcribed — the script's existing
REQUIRED_CONTEXTSregistry already drives the file/job/context scan, so (7c) inherits it and no new list is introduced.A
(7c)self-test battery pins both directions: re-introducingbranches: [main]onci.ymlandbranches-ignore:onlint.ymleach go red, and the checked-in state is asserted green explicitly rather than left implicit. Every fixture is non-vacuous by the harness's own anchor assertion.Reverse verification on the real tree (mutate → prove it reached disk → read the gate → restore under a
trap, restoring withgit checkout HEAD --and proving it by blob hash):5. Gates
Derived mechanically from the diff with
node scripts/pm/dispatch-gates.mjs, then reconciled with--ran. 58 families derived; 53 run green, 4 NOT-MEASURED with reasons, 1 UNRUN.Not measured, each because the gate refused with its own stated prerequisite (a built tree), and each unreachable from this diff — it moves no package source byte, so none of them can change:
check:dts-closure— exit 3, "NOT a pass and NOT a finding: nothing was swept"check:dual-build-cjs-loads— exit 3, "Runpnpm buildfirst. NOT a pass: nothing was measured"check:sourcemap-no-sources-content— exit 3, same shapecheck:type-check-debt— exit 3, same shapeUnrun, declared:
check:pm-dispatch-gateswas cap-killed at 240s and again at 540s, with no failing assertion anywhere in its output. A cap kill is not a refused prerequisite — the tool's own docblock names that exact substitution as how an unfinished run hides — so it is recorded UNRUN rather than written off, and CI owns it.Exit codes were captured before any pipe (
cmd > log 2>&1; EXIT=$?), never through| tail.6. Changeset
skip-changeset: nothing published moves. The root package isprivate: true, and no package'sfiles[]shipsscripts/or.github/— verified across every workspace manifest, not assumed.Verification residue — what this PR cannot show
main, so its own checks prove only that the widened triggers still fire on the ordinary path.What would confirm it: after this lands on
main, open a throwaway PR whose base is a feature branch (any branch other thanmain) and read its checks list — the six contextsLint & Repo Gates,TypeScript Type Check,Test Core,Build Core,Dogfood Regression Gate,Temporal Conformance (live PG + MySQL)must all appear and report. Until that run exists, row 1 is open. Left to the PM seat.Two smaller residues, noted and not filed:
pull_requestsynchronizefires on head movement, not base movement. A stacked PR still does not re-run when its base branch is updated. Out of scope for this card, and moot while stacks are an unsupported form (finding: a stacked PR series has no model in this repo's tooling — three costs measured end-to-end on the #14478 six-card stack #16149).lint.yml's MEASURED block are maintainer-only and unchanged by this PR.Generated by Claude Code