Skip to content

ci: run the heavy workflows on pull_request for any base branch - #16893

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-16482-heavy-ci-any-base
Sep 8, 2026
Merged

ci: run the heavy workflows on pull_request for any base branch#16893
baozhoutao merged 1 commit into
mainfrom
claude/issue-16482-heavy-ci-any-base

Conversation

@claude

@claude claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Part of #16482

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 reported nothing on every such PR.

The whole workflow diff removes exactly four lines — the two branches: / - main pairs under pull_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 is created descending, 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:

base ref PRs
main 3994
claude/issue-14478-duration-unit-in-key-name 5
claude/issue-15679-system-duration-unit-in-key-name 1

6 of 4000 PRs (0.15%) had a non-main base, 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, plus filter, 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_request runs for each workflow (merge_group 4999, push 3584 — unaffected either way). That is 7008 / 3994 = 1.75 runs per PR, the measured multiplier for pushes after the first.

  • Central estimate: 6 PRs × 1.75 = ~10.5 extra runs per workflow per 30 days ≈ 2.5/week, or ~57 job-runs/week across both files at 23 job-runs per PR event.
  • Pessimistic bound: those 6 PRs carry 115 commits between them; if every commit produced an uncancelled run (it cannot — concurrency.cancel-in-progress collapses bursts), 115 × 23 = 2645 job-runs / 30 days ≈ 617/week, still only 1.6% of current pull_request spend.

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_group is untouched — obligation 3

ci.yml:12-15 states that every workflow producing a required check must keep its merge_group trigger or queue builds wait forever. Read before editing on:, and honoured:

  • git diff over both files shows no - line touching merge_group, concurrency, group: or cancel-in-progress. The only removals are the four branches: lines quoted above.
  • merge_group: is still present exactly once in each file.
  • Both concurrency blocks are byte-identical. Their key is github.event.pull_request.number || github.ref; a PR event still groups by PR number, a queue build still falls through to github.ref. Removing a base filter changes neither branch of that expression.
  • check:required-contexts assertion 6 already fails the build if either file ever loses merge_group:, so this is machine-held from here on, not just reviewed.

push: deliberately keeps its branches: [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 4

Read 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_group coverage result — "is there a queue-build gap to close" — answered no, plus a ⛔ against adding merge_group to the advisory workflows. It says nothing about the pull_request: trigger's branches: 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 from pull_request: changes no merge_group behaviour 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.mjs grows assertion (7c): no branches: / branches-ignore: on the pull_request: trigger of any workflow publishing a required context. It sits beside the existing paths: (7) and types: (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_CONTEXTS registry 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-introducing branches: [main] on ci.yml and branches-ignore: on lint.yml each 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 with git checkout HEAD -- and proving it by blob hash):

HEAD blob hash: f0b24f6b00b6217e8f5f73079cb62508995041a8
injected '    branches:' lines: 2      (1 before, 2 after — mutation is on disk)
mutated blob hash: e58de99e82da687edc4792ced5f643f3875da57a
MUTATED GATE EXIT=1
  - .github/workflows/ci.yml's `pull_request:` trigger carries `branches:`. A base-filtered
    trigger does not run at all on a PR whose base branch is outside the filter, so it
    publishes NO check run there - not a skip, an absence - and every required context in
    this file sits permanently pending on every PR based on a feature branch (#16482 ...)
restored blob hash: f0b24f6b00b6217e8f5f73079cb62508995041a8   (== HEAD)
git diff HEAD: EMPTY (clean)

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.

✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s)
✓ check-required-contexts --self-test: 158 assertions
✓ check-nul-bytes: OK (scanned 8350 text file(s); no raw ASCII control bytes)
✓ check-partof-closing-keyword self-test: 95 cases pass
  ... 49 further derived families, all EXIT=0

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, "Run pnpm build first. NOT a pass: nothing was measured"
  • check:sourcemap-no-sources-content — exit 3, same shape
  • check:type-check-debt — exit 3, same shape

Unrun, declared: check:pm-dispatch-gates was 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 is private: true, and no package's files[] ships scripts/ or .github/ — verified across every workspace manifest, not assumed.

Verification residue — what this PR cannot show

⚠️ Acceptance row 1 of the card, "a PR based on a feature branch shows all six required contexts", is not demonstrated here and is not claimed. This PR's base is 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 than main) and read its checks list — the six contexts Lint & 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:


Generated by Claude Code

`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
@github-actions github-actions Bot added the size/m label Sep 8, 2026
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 8, 2026
@github-actions github-actions Bot added the ci/cd label Sep 8, 2026
@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT in substance — PR #16893, reviewed against GitHub and the tree, ⛔ not against the report

Flip + arm withheld until the four still-running checks report (below). Everything else is verified.

What I re-drove myself

claim how I checked it result
the workflow diff removes exactly four lines and touches nothing else read both patches from the API ci.yml -2, lint.yml -2 — the two branches:/- main pairs. Every added line is a comment. merge_group: and both concurrency blocks untouched; push: keeps its branches: [main] with an explicit ⛔ note against mirroring the widening onto it
the runner-cost measurement (acceptance row 3) paged /pulls?state=all&sort=created to the window edge myself — 41 pages, 4013 PRs created 2026-08-09→2026-09-08 6 non-main-base PRs, and the same six: #16022, #15988, #15938, #15906, #15837, #15814, bases claude/issue-14478-… ×5 and claude/issue-15679-… ×1. Base histogram main: 4007. ⇒ 0.15%, and unconditional widening is the right shape rather than the card's label-gate fallback
ci-cd-pipeline-doc.test exists searched origin/main by name 0 files — control fired (9 paths match pipeline). ⚠️ The card's Pin bullet and my own claim both named a file that does not exist. That is a defect in the card and in my brief, not in the PR
the commit carries no card-relation trailer read the commit message from the API ✅ only the two harness trailers. Fixes/Closes/Resolves/Refs # all 0 in the body too; the relation is declared once as Part of #16482 — ⛔ correctly not a closing keyword, since acceptance row 1 is unverified and merging must not auto-close the card
skip-changeset is honest read all 82 workspace manifests myself 70 carry a files[] (the instrument is not silent) and zero name scripts/ or .github/ ⇒ nothing published moves
the pin derives rather than transcribes read :1737 baseline.pinned.length === REQUIRED_CONTEXTS.length — the context list comes from the registry

⭐ The file-surface breach was FORCED, not discretionary — and I confirmed the mechanism

The claim fenced the surface to the two workflows plus a pin; the pin landed in scripts/check-required-contexts.mjs. The dev's decisive reason checks out in the code:

:1727-1731 — the self-test's fixture() helper asserts non-vacuity:

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 ' pull_request:\n branches:\n - main\n'. ⇒ deleting that block from ci.yml makes all five anchors miss, and the self-test reds at :1729 whether or not a pin is added. The stated surface was not sufficient to land the card. ⭐ That guard is also the thing that makes the whole battery non-decorative — it is the false-zero trap closed in code.

⇒ 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 it

I wrote "Reference the card as Refs #16482 in the commit." scripts/check-partof-closing-keyword.mjs RULE 2, read at :12-13:

No commit on the pull request may carry a card-relation trailer at all: no closing keyword, no Part-of and no Refs

and its self-test at :868 drives all five spellings — ['Fixes', 'Closes', 'Resolves', 'Part of', 'Refs'] — as findings. So my instruction would have red-ed the gate exactly as Fixes #N did on PR #16880 this morning. The dev committed it, tested it, found commitRelations() returned a finding, and amended before pushing — the right order, and it flagged rather than silently choosing. Dispatch-brief defect 4 is hereby corrected (it previously said "write Refs #N"): a commit carries no card relation at all. The correction has been sent to both in-flight devs.

⚠️ What is NOT claimed

  • Acceptance row 1 — "a PR based on a feature branch shows all six required contexts" — is open, correctly not claimed by the dev. Its own PR is based on main, so its green proves only that the widened trigger still fires on the ordinary path. Confirming it needs a throwaway PR on a non-main base after this lands. ⇒ This seat's, and it will be paid on this card.
  • I could not re-drive assertion (7c) locally. This container has no node_modules at all (import('yaml')ERR_MODULE_NOT_FOUND from the primary checkout), and the gate refuses with exit 3, PREREQUISITE NOT MET rather than a false green — the honest behaviour. ⇒ ⛔ That is a failed read, not a reading, and I am not counting the dev's ablation as mine. The in-seat substitute is Lint & Repo Gates on this PR, which runs check:required-contexts and its self-test on real infrastructure — it is one of the four still pending.
  • check:pm-dispatch-gates is UNRUN, declared rather than hidden: cap-killed at 240 s and again at 540 s (exit 124) with no failing assertion. A cap kill is not a refused prerequisite. Recorded.

Checks at review time

25 success · 3 skipped · 4 in progress (Lint & Repo Gates, Type Check · workspace, Type Check · consumer gates, Type Check · debt ledger) · 0 failures. Flip + arm on green.


Generated by Claude Code

@claude

claude Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor Author

⚠️ Correction to my ACCEPT above, one line of it. I wrote that dispatch-brief defect 4 "previously said 'write Refs #N'". It did not — the seat post's defect 4 has said "a commit message carries NO card trailer" since it was written this morning. The wrong instruction was in my three round-2 dispatch prompts, which contradicted my own recorded rule. The seat post needed no correction on that point; it needed the note that the rule was written down and then not followed. That is what it now carries, together with the fact that ⛔ Refs #N is not a safe substitute for Fixes #N — RULE 2's self-test drives all five spellings.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants