Skip to content

Commit df91a35

Browse files
ci: let a covered push run borrow the merge queue's verdict (#31611)
Fixes #31519 ## 维护者速读 - 改了什么:`main` 的 push 运行先读合并队列的结论。本次 push 的每个提交,如果都在自己的第一父提交上有一次全绿的队列运行,push 运行就只跑 main 专属的作业,其余的测试从队列借用结论;只要缺一个、红一个或基不对,就照旧跑全套。 - 为什么改:897 次 push 运行里有 896 次在重测队列刚刚测过的同一棵树,每次大约 260 分钟的 runner 时间。 - 风险与回滚:默认关闭——读不到、读错或结论不全都算"未覆盖",push 照跑全套;每 4 小时的定时运行仍是完整测试;发布门禁通过队列运行重新读取借用的结论。回滚就是 revert 这一个 PR。 - 先看:`.github/workflows/ci.yml` 顶部新增的两个作业(`queue-verdict`、`Borrowed queue verdict`),以及 `scripts/ci/scheduled-full-run.mjs` 里的 HALF 4。 - 两分钟验证:`node scripts/ci/scheduled-full-run.mjs --self-test` 与 `node scripts/check-shard-attestation.mjs --self-test` 都应 exit 0;"queue verdict: the push probe" 一组里包含下文三个故意制造的红例。 - 需要你做的:审阅后手动合并(Tier H)。 ## What changes - **`queue-verdict` job** (push only, sparse checkout of `scripts/`): `scheduled-full-run.mjs --queue-verdict --before --after` lists the pushed commits (cap 20) and reads, for each one, the newest `merge_group` run of `ci.yml` on that commit. The push is **covered** only when every commit's deciding queue run concluded `success` and was built on the commit's first parent. It writes `covered`, `runs` and `ui_full_green` to `GITHUB_OUTPUT` and a verdict paragraph to the step summary. A read failure or an empty push is `covered=false`. - **`Borrowed queue verdict` job** (`if: ${{ success() && needs.queue-verdict.outputs.covered == 'true' }}`): the per-merge verdict the release gate reads by name. It runs the affected-test selector against `HEAD^1` and answers `test_ui=skip` only when the head's queue run selected the whole Test UI suite and all 16 shards passed (design note Q2 = B). - **Covered pushes skip** the jobs whose verdict is borrowed. Every skip reads `needs.queue-verdict.outputs.covered != 'true'`, so a missing or garbled output runs the job (fail closed). The three aggregates (`test-gate`, `dogfood-gate`, `test-ui-gate`) carry `if: always() && needs.queue-verdict.outputs.covered != 'true'`; `build-docs`, `build-e2e-ui` and `live-e2e-ui` carry the covered conjunct; `test-ui` also needs `test_ui != 'skip'`. - **Seed-only legs** (design note Q1 = A): on a covered push the Test Core and dogfood shards still run to save their Turbo seeds, but exit early on `OS_SEED_ONLY` before their slice build and test step. Their attestation pairs are guarded off. - **Release gate** (`release.yml` comment, `scheduled-full-run.mjs` HALF 3): a borrowed push run is re-read through the head's queue run. An unread borrow record is `pending` and refused, never a pass. - **`ui/scripts/test-file-timings.mjs`**: `selectRuns` reads schedule runs only, since a covered push no longer runs every Test UI shard. - **Docs**: the AGENTS.md "Two speeds" sentence and a new paragraph in `docs/agents/multi-agent-discipline.md`. No required context is removed: every required check still reports on every push, as a pass or a skip that its aggregate reads. ## Why The push run of `main` re-tests the tree the merge queue just passed (896 of 897 measured runs), at about 260 runner-minutes each. The queue already ran that exact tree green on that exact base, so the push run can borrow the verdict. It keeps a per-merge verdict for the release gate, and it keeps the cache seeds. The 4-hourly schedule stays the full battery, and no test is skipped on a SHA the queue did not run green. ## Where the build differs from the design note (6722126387) - **(a) Lint UI runs on a covered push.** Its Turbo seed saves on `push` only, not on `schedule`, so skipping it would starve the seed. - **(b) A seed-only Test Core leg seeds the closure and holds no test results.** `prune-turbo-cache.mjs` is unchanged; a seed-only leg passes the closure's summary directory as `--run-summaries`. - **(c) Dogfood Regression Gate skips on a covered push**, while Dogfood Verify CLI's own conclusion stands. - **(d) `check-shard-attestation.mjs` admits exactly two spellings** (the borrowed gate `if:` and the seed-only credential guard), with an 8-case battery. - **The seed-only exits read `OS_SEED_ONLY`** instead of a step `if:`, because dispatch-gates' LIVE case "names the invocation itself" reads the dogfood test step and a step `if:` broke it. - **AGENTS.md re-wrap:** the paragraph from "**Green means**" through Main-red is re-wrapped, with every word outside the "Two speeds" sentence byte-identical, to stay inside the file's 1050-line and 120-byte line budget. The lines are in the claim surface. - **Files admitted by claim amendment:** `scripts/check-aggregator-roster.mjs` (6 self-test anchors only), `.github/workflows/ui-test-timings-refresh.yml` (2 prose lines only), and `ui/scripts/__tests__/turbo-cache-save-bound.test.ts` (one assertion and its comment). ## Tests - `scheduled-full-run.mjs --self-test`: new battery "queue verdict: the push probe" (17 cases) and "release gate: a borrowed push verdict" (15 cases). The battery floor goes from 9 to 11. - `check-shard-attestation.mjs --self-test`: new battery "#31519 a borrowed push: the gate skip and the seed-only credential guard" (8 cases). The floor goes from 24 to 25. - `ui/scripts/__tests__/test-file-timings.test.ts`: a `selectRuns` case (17 pass). `turbo-cache-save-bound.test.ts`: 46 pass. - **Three deliberate reds** through `runQueueVerdict` on a 3-commit push, each turning a covered push into a full run: - **missing queue run**: "no merge_group run of the workflow is on cccccccccc", so `covered=false`. - **red queue run**: "queue run 601 on bbbbbbbbbb concluded failure", so `covered=false`. - **wrong base**: "queue run 500 on aaaaaaaaaa was built on dddddddddd, not on the commit's first parent bbbbbbbbbb", so `covered=false`. - The control (all three green on their parents) reads `covered=true`. - **Ablations**: removing each guard (missing, red, wrong base, and the release gate reading an unread record as a full verdict) makes `--self-test` exit 1; restored, it exits 0. ## Verification All at head 80eca7d (origin/main 8c75f83 merged), after a full `pnpm build` (exit 0): - `node scripts/pm/dispatch-gates.mjs --commands`: 83 derived gate commands, each run with its exit code recorded before any pipe. **83 of 83 exit 0.** - `node scripts/pm/dispatch-gates.mjs --ran ran.list`: "83 derived famil(ies) accounted for, 83 run, 0 NOT-MEASURED (a DERIVED zero)", exit 0. - That sweep caught one red the earlier sweep could not: the AGENTS.md re-wrap had split `Dogfood Regression Gate` across two lines, and `check:required-contexts` matches the literal by substring. Fixed in 80eca7d; the gate is green in the 83. - ESLint (`--no-inline-config`) on the changed root scripts and the changed `ui/scripts` files: exit 0. - `git merge-tree` against PR #31429's head 65cf47a and against current `origin/main`: clean. Serial: PR #31429 (devx#3) also edits ci.yml; merge-tree clean at 65cf47a; whichever lands second merges main Size: 11 files, +1206 / -87 (under 3000 lines). Tier H (`ci.yml`, `release.yml`, `AGENTS.md`): the maintainer merges it by hand. Clause-②: no Metadata-protocol: none ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; ESLint passes on every changed script (the full `pnpm lint` tree was not run). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (nothing publishes: `skip-changeset`) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jQztfi9hnsaBeBVcCU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5d1382a commit df91a35

11 files changed

Lines changed: 1213 additions & 87 deletions

‎.github/workflows/ci.yml‎

Lines changed: 250 additions & 27 deletions
Large diffs are not rendered by default.

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1609,7 +1609,9 @@ jobs:
16091609
# newest completed `schedule` or `push` runs of `CI` and `Lint & Type
16101610
# Check` whose head is the version commit or a first-parent ancestor of it
16111611
# must both have concluded `success` (a `cancelled` run carries no
1612-
# verdict). The selection rule and its pins live in
1612+
# verdict). A CI push run that borrowed the merge queue's verdict (#31519)
1613+
# is read with the queue run it names, which must be green on the same
1614+
# commit's first parent. The selection rule and its pins live in
16131615
# scripts/ci/scheduled-full-run.mjs (HALF 3), whose --self-test lint.yml
16141616
# runs; this job only calls it.
16151617
#

‎.github/workflows/ui-test-timings-refresh.yml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# ALL THREE PARTS LIVE IN ONE SCRIPT: ui/scripts/test-file-timings.mjs
2121
# ---------------------------------------------------------------------
2222
# the writer (the reporter ci.yml's `test-ui` job passes),
23-
# `select` the newest completed `push` / `schedule` runs of ci.yml on
23+
# `select` the newest completed `schedule` runs of ci.yml on
2424
# `main` whose sixteen `Test UI (N/16)` shards ALL concluded
2525
# `success` and whose sixteen `test-file-timings-N` artifacts
2626
# are all still there (a red or cancelled shard may have
@@ -139,7 +139,7 @@ jobs:
139139
echo
140140
echo "\`test-file-timings.mjs select\` found no eligible run, so"
141141
echo "\`ui/scripts/test-shard-timings.json\` is untouched. Expected until a full-battery run"
142-
echo "(a \`push\` that starts Test UI, or the 4-hourly \`schedule\`) has uploaded all"
142+
echo "(the 4-hourly \`schedule\`, objectstack#31519) has uploaded all"
143143
echo "$UI_TEST_SHARDS \`test-file-timings-N\` reports after the reporter landed."
144144
echo
145145
echo "⛔ A NOT MEASURED that persists past a day of scheduled runs is a defect, not a"

‎AGENTS.md‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -501,19 +501,19 @@ Even inside your own worktree, operate defensively:
501501
the owning seat, or the maintainer's merge or enqueue. Read the PR's file list (`get_files`), **its author and
502502
its size** before you arm anything.
503503

504-
**Green means the gate-carrying jobs' `conclusion` is `success`** — not "no failure
505-
yet"; `in_progress` is not a pass, and arming a red PR hides it
506-
([why](docs/agents/multi-agent-discipline.md#arming-a-red-pr-hides-it)). Always read *two* things:
507-
the queue branch **and** `origin/main`. And **the queue enforces only the required
508-
set** — eleven contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`,
509-
`Test Core`, `Build Core`, `Governed Surface Queue Guard`, `Lint UI`, `Type Check UI`, `Test UI`,
510-
`Build & E2E UI`, `Live E2E UI` and `Dogfood Regression Gate`; a check outside those eleven is advisory
511-
([why that matters](docs/agents/multi-agent-discipline.md#advisory-checks-ride-through)). A required context is
512-
matched by check-run name, so a rename detaches its gate silently — treat those eleven names as contract.
513-
**Two speeds** — PR runs carry the fast set, queue runs add the dogfood shards; `main`'s scheduled and push runs
514-
carry the full battery ([detail](docs/agents/multi-agent-discipline.md#two-speeds-which-runs-carry-which-battery)).
515-
**Main-red:** a full-battery red is its lane's first work — triage files it under
516-
the main-red convention, the lane's next PR fixes it, and ⛔ no lane PR arms the queue meanwhile.
504+
**Green means the gate-carrying jobs' `conclusion` is `success`** — not "no failure yet"; `in_progress` is not a
505+
pass, and arming a red PR hides it ([why](docs/agents/multi-agent-discipline.md#arming-a-red-pr-hides-it)). Always
506+
read *two* things: the queue branch **and** `origin/main`. And **the queue enforces only the required set** —
507+
eleven contexts block: `Lint & Repo Gates` (all `check:*` gates), `TypeScript Type Check`, `Test Core`, `Build Core`,
508+
`Governed Surface Queue Guard`, `Lint UI`, `Type Check UI`, `Test UI`, `Build & E2E UI`, `Live E2E UI` and
509+
`Dogfood Regression Gate`; a check outside those eleven is advisory ([why that
510+
matters](docs/agents/multi-agent-discipline.md#advisory-checks-ride-through)). A required context is matched by
511+
check-run name, so a rename detaches its gate silently — treat those eleven names as contract. **Two speeds** —
512+
PR runs carry the fast set, queue runs add the dogfood shards; `main`'s scheduled runs carry the full battery, and a
513+
push run carries it by running the main-only jobs and borrowing the rest from the green queue runs of its commits
514+
([detail](docs/agents/multi-agent-discipline.md#two-speeds-which-runs-carry-which-battery)). **Main-red:** a
515+
full-battery red is its lane's first work — triage files it under the main-red convention, the lane's next PR fixes
516+
it, and ⛔ no lane PR arms the queue meanwhile.
517517

518518
**Re-arm awareness** ([measured](docs/agents/multi-agent-discipline.md#re-arm-awareness)): a red queue build
519519
ejects your entry and drops auto-merge, and collateral eviction is silent — neither on `main` nor in the

‎docs/agents/multi-agent-discipline.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ A check outside those eleven is advisory and rides through, and an advisory red
138138
Temporal Conformance and `UI Published Artifacts` stay full-battery only. The `Dogfood Regression
139139
Gate` context is in the required set again (ruling record 6669982756), so a dogfood red ejects the entry.
140140

141+
**A push run borrows what the queue ran** (#31519). Every commit the queue lands was tested there on the same
142+
sha. A push run's `Queue verdict` job walks the pushed commits and reads each one's newest `merge_group` run. When
143+
every commit has one that is green and built on its first parent, the push runs only the main-only jobs, `Type Check
144+
UI`, `Lint UI` and the builds, keeps its Turbo seeds through seed-only Test Core and dogfood legs, and records the
145+
queue runs in its `Borrowed queue verdict` job. `Test UI` is borrowed only when the head's queue run ran the whole
146+
suite. A missing, red, running or differently based queue run, or any read failure, runs the whole battery. The
147+
release gate re-reads the head's queue run when a borrowed push decides, and the 4-hourly schedule is unchanged.
148+
Rules and pins: `scripts/ci/scheduled-full-run.mjs` HALF 4.
149+
141150
## Re-arm awareness
142151

143152
← [`AGENTS.md` › Multi-agent working discipline](../../AGENTS.md#multi-agent-working-discipline)

‎scripts/check-aggregator-roster.mjs‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -502,15 +502,15 @@ async function selfTest() {
502502
const dropped = [
503503
{ label: 'typecheck loses a lane from needs:', file: 'lint.yml', member: 'typecheck-consumers', from: ' - typecheck-consumers\n', to: '' },
504504
{ label: 'lint loses a lane from needs:', file: 'lint.yml', member: 'lint-pm-dispatch-gates', from: ' - lint-pm-dispatch-gates\n', to: '' },
505-
{ label: 'test-gate loses its matrix from needs:', file: 'ci.yml', member: 'test', from: ' needs: [test, filter, test-plan]\n', to: ' needs: [filter, test-plan]\n' },
505+
{ label: 'test-gate loses its matrix from needs:', file: 'ci.yml', member: 'test', from: ' needs: [test, filter, test-plan, queue-verdict]\n', to: ' needs: [filter, test-plan, queue-verdict]\n' },
506506
{
507507
label: 'dogfood-gate loses the CLI pass from needs:',
508508
file: 'ci.yml',
509509
member: 'dogfood-verify',
510-
from: ' needs: [dogfood, dogfood-verify, filter]\n',
511-
to: ' needs: [dogfood, filter]\n',
510+
from: ' needs: [dogfood, dogfood-verify, filter, queue-verdict]\n',
511+
to: ' needs: [dogfood, filter, queue-verdict]\n',
512512
},
513-
{ label: 'test-ui-gate loses its matrix from needs:', file: 'ci.yml', member: 'test-ui', from: ' needs: [test-ui, filter]\n', to: ' needs: [filter]\n' },
513+
{ label: 'test-ui-gate loses its matrix from needs:', file: 'ci.yml', member: 'test-ui', from: ' needs: [test-ui, filter, queue-verdict]\n', to: ' needs: [filter, queue-verdict]\n' },
514514
];
515515
for (const scenario of dropped) {
516516
const verdict = fixture(scenario.label, scenario.file, (s) => s.replace(scenario.from, scenario.to));
@@ -532,15 +532,15 @@ async function selfTest() {
532532
const phantom = [
533533
{ label: 'typecheck needs a job that was deleted', file: 'lint.yml', from: ' - typecheck-consumers\n', to: ' - typecheck-consumers\n - typecheck-ghost\n', ghost: 'typecheck-ghost' },
534534
{ label: 'lint needs a job that was deleted', file: 'lint.yml', from: ' - lint-pm-dispatch-gates\n', to: ' - lint-pm-dispatch-gates\n - lint-ghost\n', ghost: 'lint-ghost' },
535-
{ label: 'test-gate needs a job that was deleted', file: 'ci.yml', from: ' needs: [test, filter, test-plan]\n', to: ' needs: [test, filter, test-plan, test-ghost]\n', ghost: 'test-ghost' },
535+
{ label: 'test-gate needs a job that was deleted', file: 'ci.yml', from: ' needs: [test, filter, test-plan, queue-verdict]\n', to: ' needs: [test, filter, test-plan, queue-verdict, test-ghost]\n', ghost: 'test-ghost' },
536536
{
537537
label: 'dogfood-gate needs a job that was deleted',
538538
file: 'ci.yml',
539-
from: ' needs: [dogfood, dogfood-verify, filter]\n',
540-
to: ' needs: [dogfood, dogfood-verify, filter, dogfood-ghost]\n',
539+
from: ' needs: [dogfood, dogfood-verify, filter, queue-verdict]\n',
540+
to: ' needs: [dogfood, dogfood-verify, filter, queue-verdict, dogfood-ghost]\n',
541541
ghost: 'dogfood-ghost',
542542
},
543-
{ label: 'test-ui-gate needs a job that was deleted', file: 'ci.yml', from: ' needs: [test-ui, filter]\n', to: ' needs: [test-ui, filter, test-ui-ghost]\n', ghost: 'test-ui-ghost' },
543+
{ label: 'test-ui-gate needs a job that was deleted', file: 'ci.yml', from: ' needs: [test-ui, filter, queue-verdict]\n', to: ' needs: [test-ui, filter, queue-verdict, test-ui-ghost]\n', ghost: 'test-ui-ghost' },
544544
];
545545
for (const scenario of phantom) {
546546
const verdict = fixture(scenario.label, scenario.file, (s) => s.replace(scenario.from, scenario.to));

0 commit comments

Comments
 (0)