Skip to content

ci: add an always-run roll-up job named CI so the queue's required check matches a real check run (#9318) - #9346

Closed
os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-9318-ci-rollup-job
Closed

os-zhuang wants to merge 1 commit into
mainfrom
claude/issue-9318-ci-rollup-job

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #9318

Implements #9283's maintainer ruling (Option A — the full suite blocks the merge queue) in its durable form: one always-run roll-up job whose check-run name is exactly CI, so the queue's required context finally matches a real check run.

Why

Branch protection matches a required check by check-run name, and a job's check-run name is its name: value. The first attempt registered CI — which is this workflow's workflow name (line 1 of ci.yml), not any job's name. Nothing ever published that context, so it sat permanently "Expected": green merge groups timed out and nothing merged from 11:29Z until the maintainer removed the entry (~13:15Z) on 2026-08-17, ~1h45m. The failure mode had flipped from #9283's "red merges anyway" to "green cannot merge".

With this job, the required set stops being a hand-maintained roster of gate job names: adding a gate to needs: here extends the queue's protection with no settings change.

What changed

.github/workflows/ci.yml — one appended job, plus the comment block the file's conventions call for (the name-is-the-contract note, the #9283/#9318 lineage, and why the results are read explicitly):

  ci-rollup:
    name: CI
    needs: [filter, test-gate, dogfood-gate, temporal-conformance, build-core, build-docs, console-pin]
    if: always()

The needs: list was re-derived from the file, not taken from the card. It matches the card's six gate jobs plus filter, and three jobs are deliberately excluded:

Job In needs:? Why
test-gate, dogfood-gate, temporal-conformance, build-core, build-docs, console-pin yes the six jobs that carry a gate verdict
filter yes not pure plumbing here. If filter fails, every downstream if: evaluates against empty outputs (THE FILTER CONTRACT, #4928), so no scheduling decision in the run was actually made — a green roll-up would certify a run whose shape nobody chose. test-gate/dogfood-gate already read needs.filter.result for that reason; this makes it true for the run as a whole.
test, dogfood, dogfood-verify no their verdicts reach the roll-up through their aggregate gates, which count per-shard attestations rather than reading one aggregate word. #6082 measured that read failing in both directions (run 31120902911: a false red off the undocumented abandoned; run 31114735713: that same value swallowing a shard's real failure). Listing the matrices here would reintroduce exactly that read, one level up.

Verdict logic — computed from needs.*.result in a small bash step over toJSON(needs). Default needs: propagation is not usable: an upstream failure would make this job skip, and a skipped required context is the #3622 deadlock; meanwhile a docs-only PR legitimately skips every core gate and must still publish a green CI.

needs.*.result verdict
success, skipped pass
failure, cancelled fail
anything else fail, naming the value
empty needs context fail (#4690)

Two of those are judgment calls, stated rather than buried:

scripts/check-required-contexts.mjs — one registry row pinning ci.yml:ci-rollup → 'CI'. This is the one file outside the card's declared surface, so the reasoning is here rather than implied: the pin (#6865) exists precisely so a rename cannot silently detach branch protection, and CI is about to be the single most load-bearing check name in the repo. Without the row it would be the only required context with no repo-side protection. The row is enrolled ahead of the settings change deliberately — the registry's own header licenses that ("adding a row here does NOT make a context required … a pin that is merely inert, never wrong"), and the authorized field says so in full. Reviewer's call: dropping this hunk leaves the workflow change fully functional and costs only the rename protection.

Not touched: triggers (pull_request + merge_group were already there), the filter job and what it skips, any other workflow, any repository setting.

Sequencing — the maintainer step that follows

⛔ This PR must merge through the queue before the settings change. After it lands, the maintainer re-adds CI to the required checks (no agent seat can read or write Settings → Rulesets; the API answers 403). At that point the interim per-job entries — and even Test Core / Dogfood Regression Gate — can be pruned to the single CI entry.

Verification

Union re-run after the final commit, at 9b207e0 (= git rev-parse --short HEAD at that run):

✓ check-required-contexts --self-test: 55 assertions
✓ check-required-contexts: 10 required context name(s) pinned across 3 workflow(s).
    ci.yml:ci-rollup → 'CI'                     ← new
✓ check-shard-attestation --self-test: 92 assertions
✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
✓ check-workflow-status-functions --self-test: 34 assertions
check-workflow-status-functions: OK (25 workflow file(s), 45 job(s), 25 job-level if: expression(s);
  9 read needs.*.outputs.*, all naming a status function).
check-node-version: OK (28 setup-node step(s) across 25 workflow(s), all on Node 22).
✓ check-nul-bytes --self-test: 75 assertions
check-nul-bytes: OK (scanned 6076 text file(s); no raw ASCII control bytes).
eslint scripts/check-required-contexts.mjs — clean

Gate families derived, not recalled — node scripts/pm/dispatch-gates.mjs .github/workflows/ci.yml scripts/check-required-contexts.mjs names exactly those five, and re-deriving against the actual changed paths (both files) added nothing beyond the single-path derivation.

Reverse verification of the pin (run from the committed state, then restored and confirmed byte-identical to HEAD). Predicted direction: red, naming the job and the context. Observed:

✗ check-required-contexts — 1 problem(s)
  • ci.yml: job 'ci-rollup' is named "CI Roll-up", but branch protection requires the context 'CI'.
    The name IS the contract: GitHub matches a required check by its check-run name, so this rename
    silently detaches the gate. …

The verdict logic was executed, not eyeballed: the step's real run: text was lifted out of the parsed ci.yml (never retyped) and driven against synthetic needs contexts. All eight scenarios behaved as designed — including the card's three acceptance criteria:

Scenario exit
all gates green 0
docs-only PR (core gates skipped by filter) 0 ✅ acceptance 1
every gate skipped 0
temporal-conformance = failure (the #9283 job) 1 ✅ acceptance 2
a gate cancelled 1
filter itself failed 1
abandoned (#6082's undocumented value) 1
empty needs context (#4690) 1

Acceptance 3 (the name is exactly CI in the check-run list) is what check:required-contexts now pins, and what the parsed YAML reports: ci-rollup => "CI".

Tier derivation

Verbatim from node scripts/pm/dispatch-gates.mjs --tier .github/workflows/ci.yml:

Model tier — no path-derived mandate: the surface hits none of the 1 declared glob(s), derived here, not recalled.
  The tier stays the PM's per-card judgment call (floor sonnet · default opus · ceiling fable).
  Clause ② is NOT reachable from paths: a card that changes contract accept/reject behaviour or widens the public surface is fable-mandatory too, judged from the card CONTENT. This line is a FLOOR, never a clearance.

Changeset

None — workflow + gate-script only, nothing user-facing is published. Declared the way this repo actually declares it, the skip-changeset label (pr-automation.yml's changeset-check exemption), applied at PR-creation time and read back after the labeler settled.


Generated by Claude Code

…check matches a real check run (#9318)

Branch protection matches a required check by CHECK-RUN name, and a job's
check-run name is its `name:` value. Registering `CI` — ci.yml's WORKFLOW name —
matched no check run at all: the context sat permanently "Expected", green merge
groups timed out and nothing merged for ~1h45m on 2026-08-17. This job makes the
name real, implementing #9283's Option A ruling in the durable form: one stable
required context that rolls up every gate job, so adding a gate extends the
queue's protection with no settings change.

- `ci-rollup` / `name: CI`, `if: always()`, `needs:` filter + the six gate jobs
  (test-gate, dogfood-gate, temporal-conformance, build-core, build-docs,
  console-pin). The sharded matrices are deliberately absent: their verdicts
  arrive through the aggregates, which count per-shard attestations instead of
  reading the one aggregate word #6082 measured going wrong both ways.
- The verdict is computed from `needs.*.result` explicitly, because default
  propagation would SKIP this job on an upstream failure and a skipped required
  context is the #3622 deadlock. success/skipped pass; failure/cancelled fail;
  an unrecognised runner lifecycle value (`abandoned`, #6082) fails closed; an
  empty `needs` context fails rather than ticking (#4690).
- Pins the literal in `check-required-contexts.mjs` (#6865), so a rename of the
  roll-up fails in the ESLint job instead of silently detaching the gate — the
  one name in the repo where that protection matters most.

Fixes #9318

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NYgmGheCzM6NrHZN436Cxf
@github-actions github-actions Bot added the ci/cd label Aug 17, 2026
@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 17, 2026 — with Claude
@os-zhuang
os-zhuang marked this pull request as ready for review August 17, 2026 15:20
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 17, 2026

Copy link
Copy Markdown
Contributor Author

ACCEPT — PM review at 9b207e0 (dispatching seat, session session_01NYgmGheCzM6NrHZN436Cxf).

Verified against the diff, not the report: one appended job (ci-rollup / name: CI, if: always(), seven needs, jq-based explicit verdict with fail-closed on cancelled, unrecognised values, empty context, and even missing jq), plus the one out-of-surface registry row in check-required-contexts.mjs — kept per the implementer's option A, since the registry header explicitly licenses inert rows and the rename hazard is live the moment this job exists.

The PR run itself supplied a live acceptance test beyond the card's three criteria: on attempt 1, test-gate's runner died before any step (429s downloading download-artifact), left abandoned in the needs context, and the new CI check correctly failed closed — the #6082 value, caught in the wild on the roll-up's first outing. Attempt 2 (re-run of failed jobs only, legitimate infra-death class) went green across the board and CI reported success at 15:07:28Z.

Flipped ready and armed merge-when-ready. After merge, the maintainer step follows: re-add CI to the required checks, then prune the interim per-job entries.


Generated by Claude Code

@os-zhuang

Copy link
Copy Markdown
Contributor Author

应该不需要,已经改过设置了

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

Labels

ci/cd 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.

ci: add an always-run roll-up job named CI so the queue's required check matches a real check run (#9283 Option A implementation)

2 participants