Skip to content

Commit e253f3a

Browse files
ci: fail fast on the test, test-ui and dogfood matrices in the merge queue only (#31753)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbSRBDKqdoCoEAF1SZ4CCZZs)_ Fixes #31517 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:合并队列(`merge_group`)里,Test Core、Test UI、Dogfood 三个测试矩阵改成「一个分片红了,其余分片立即停止」。PR、push、定时运行不变,仍然每个分片都跑完。 - **为什么改**:以前队列里一个分片已经红了,还要等最慢的分片(Test Core 最慢片中位约 19 分钟)跑完聚合检查才报红,排在后面的条目也跟着干等。按裁决 6730013883(A)执行。 - **风险与回滚**:只用矩阵自带的 fail-fast,没有取消整次运行,真红仍然是红(聚合检查读到 `failure`)。代价是队列运行只显示第一个红分片,完整红单仍在作者的 PR 运行里。回滚:把三处表达式改回 `false`。 - **先看**:`.github/workflows/ci.yml` 里 `test` 任务的 `strategy` 段(表达式和说明);`scripts/check-shard-attestation.mjs` 新增的 #31517 自测电池;`ci.yml` 里 `dogfood` 与 `test-ui` 的 `strategy` 段。 - **两分钟验证**:`node scripts/check-shard-attestation.mjs --self-test`,预期最后一行是 `✓ check-shard-attestation --self-test: 350 assertions …`,并且 `grep -n 'fail-fast:' .github/workflows/ci.yml` 恰好三行,都是 `${{ github.event_name == 'merge_group' }}`。 - **需要你做的**:审阅并亲手合并(`.github/workflows/**` 属 Tier H)。 ## What changes - **`.github/workflows/ci.yml`**: `strategy.fail-fast: ${{ github.event_name == 'merge_group' }}` on `test` (Test Core), `test-ui` (Test UI) and `dogfood` (Dogfood Regression Gate), as ruled (ruling record 6730013883, letter A). The expression is false on `pull_request`, `push`, `schedule` and `workflow_dispatch`, so those runs keep every shard running to its end. It is the matrix's own fail-fast; there is no run-level cancel and no `concurrency` change. - **The A1 wording** (#23177 ruling B, item A1, comment 6139629628): the `test` strategy comment says why the queue is the exception (its job is a fast mergeable-or-not answer, and the PR run of the same diff already showed every red); the Test Core step's A1 note now says A1 holds inside a shard on every event and the queue is the exception across shards; the `dogfood`, `test-ui`, Dogfood Regression Gate and Test UI aggregate comments say how each reads the fail-fast shape. - **`scripts/check-shard-attestation.mjs`**: a new self-test battery, `#31517 merge-queue fail-fast: one shard red, the rest cancelled by the strategy` (8 cases, roster floor 25 → 26), and a header paragraph. No verdict logic changes. The cases: a red shard beside attested and fail-fast-cancelled siblings is red by the declared negative and by the count; a red shard with zero attestations is red (`failure` is never the zero-attestation #3668 pass); a `failure` matrix asks no cancel cause; the measured fail-fast cancel annotation classifies as `other`, never a supersession; if a matrix ever read `cancelled` after a sibling attested, that cause still reds it; a planned (#26084) roster cut short is red; the Dogfood Regression Gate reads a red dogfood shard with a cancelled sibling as red. - **`scripts/ci-shard-matrix.mjs`**: its self-test fixture job spelled `fail-fast: false`; it now spells the new expression, so the reader is exercised over the line ci.yml carries. The other three scripts the card named (`check-shard-attestation.mjs`, `check-aggregator-roster.mjs`, `partition-test-shards.mjs`) pin no `fail-fast` anchor: `git grep -n fail-fast -- scripts` at f2abd47 hits `ci-shard-matrix.mjs:220` only, plus unrelated step-level fail-fast prose in `scripts/ci/lane-verdict.mjs` and `scripts/pm/ci-failure.mjs`. ## Tests <details><summary>Self-tests and live checks of the touched and card-named scripts (6903a73, and again at the final head 0a383d6, all exit 0)</summary> - `node scripts/check-shard-attestation.mjs --self-test`: exit 0, `✓ check-shard-attestation --self-test: 350 assertions …` - `node scripts/check-shard-attestation.mjs` (static guard over ci.yml): exit 0, `✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).` - `node scripts/ci-shard-matrix.mjs --self-test`: exit 0, `27 cases pass across 5 batteries (live: test=8, dogfood=3, test-ui=16)` - `node scripts/check-aggregator-roster.mjs --self-test`: exit 0 (63 assertions); live: exit 0 - `node scripts/partition-test-shards.mjs --self-test`: exit 0 - `node scripts/ci/scheduled-full-run.mjs --self-test`: exit 0 </details> <details><summary>The new cases can fail: two mutations, red, then restored (committed head, in-place edit, `git checkout HEAD --` restore, blob hash checked)</summary> - `classifyCancelAnnotations` also reading `The strategy configuration was canceled` as a supersession: self-test exit 1, failing exactly `#31517: a shard cancelled by its matrix's fail-fast is never read as a supersession` and `#31517: were the matrix ever to read cancelled after a sibling attested, the fail-fast cause still reds it…`. - `judge()` routing a `failure` leg into the `cancelled` branch: self-test exit 1, failing exactly `#31517: the first shard red before any sibling attested, all cancelled ⇒ red…`. - Both restores: `git hash-object` equal to the HEAD blob and `git diff HEAD` empty. </details> ## Acceptance notes - **The aggregates read the fail-fast shape correctly.** Measured on run 30271824408 (the #3668 experiment, a fail-fast matrix): shard 1 concluded `failure`, shard 2 concluded `cancelled`, and the observer read the matrix result as `failure`. The cancelled shard's check run 89995981782 carries the annotation `The strategy configuration was canceled because "matrix._1" failed` (read from the check-run annotations endpoint). So `Test Core` and the Dogfood Regression Gate read `failure` (a declared negative, plus the missing credentials), and `Test UI` reads anything but `success` (or a filter-explained skip) as red. - **A fail-fast queue run is never borrowed by a push (#31611, df91a35).** `queueRunVerdict` in `scripts/ci/scheduled-full-run.mjs` covers a commit only when its queue run concluded `success`; a run whose aggregate is red concludes `failure`. `testUiFullGreen` counts only `Test UI (k/16)` jobs that concluded `success`, so a cancelled shard makes `ui_full_green` false and the push runs Test UI. Both are pinned by that script's existing self-test (a `failure` queue run, and 15 of 16 green shards). Nothing in that file changes. - **What a red queue run shows now.** Only the shards that finished before the first red one, plus that one. The author's pull_request run of the same diff still shows every red shard (ruling 6139629628 A1, unchanged there). - **Reading after landing (owed by the devx seat, on #31517):** for 7 days of `merge_group` runs of ci.yml, the queue time from the first red `Test Core (k/8)`, `Test UI (k/16)` or `Dogfood Regression Gate (k/3)` shard to the matching aggregate concluding red, compared with the same reading over the 7 days before landing. - **Noted, not filed (pre-existing, unchanged here):** a matrix where every shard is killed by its own timeout before any shard attests reads `cancelled` with zero attestations, and that is #3668's pass with no cause read (ruled not extended on #22653). Fail-fast does not widen it: a timeout fires at 60 or 30 minutes, long after the fast shards attest, and a shard that goes red first makes the matrix read `failure`. ## Verification <details><summary>Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (63 commands)</summary> Run at the final head 0a383d6 (origin/main 90cea47 merged): 63 derived, 63 run, every one exit 0. `--ran` verdict: `✓ dispatch-gates --ran: 63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED`. The first run, at 6903a73 before the merge, read the same 63-command list; there `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:sourcemap-no-sources-content` exited 3 (no built dist yet). `check:type-check-debt` built the workspace in that run, so on the final head they read a dist built at 6903a73; `packages/objectql` is the only package whose source the merge moved, and this diff touches no package. ```text node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs :: exit 0 node scripts/check-declaration-mirrors.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-self-test-wired.mjs :: exit 0 node scripts/check-self-test-wired.mjs --self-test :: exit 0 node scripts/check-self-test-workflow-commands.mjs :: exit 0 node scripts/check-self-test-workflow-commands.mjs --self-test :: exit 0 node scripts/check-step-collectors.mjs :: exit 0 node scripts/check-step-collectors.mjs --self-test :: exit 0 node scripts/check-whole-set-label-write.mjs :: exit 0 node scripts/check-whole-set-label-write.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/ci/scheduled-full-run.mjs --self-test :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/bare-root-worklist.mjs --self-test :: exit 0 node scripts/pm/ci-failure.mjs --self-test :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:aggregator-roster :: exit 0 pnpm check:bash32-floor :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:cli-command-ids :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:declared-population-live :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:dual-build-cjs-loads :: exit 0 pnpm check:entry-guard :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:lean-entry-closure :: exit 0 pnpm check:node-version :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:parse-guard :: exit 0 pnpm check:pm-close-cards :: exit 0 pnpm check:pm-expected-skips :: exit 0 pnpm check:pm-fleet-write-dispatch :: exit 0 pnpm check:pm-fleet-write-execute :: exit 0 pnpm check:pm-fleet-write-validate :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-issue-transfer :: exit 0 pnpm check:pm-post-stamped :: exit 0 pnpm check:pm-record-recognisers :: exit 0 pnpm check:pnpm-acquisition :: exit 0 pnpm check:pnpm-filter-targets :: exit 0 pnpm check:pnpm-flag-spellings :: exit 0 pnpm check:ratchet-remedy-authority :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:required-contexts :: exit 0 pnpm check:scripts-symbol-anchors :: exit 0 pnpm check:shard-attestation :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:stall-guard-budget :: exit 0 pnpm check:stall-guard-headroom :: exit 0 pnpm check:type-check-coverage :: exit 0 pnpm check:type-check-debt :: exit 0 pnpm check:watch-hint-literal :: exit 0 pnpm check:workflow-status-functions :: exit 0 pnpm check:workflow-step-name-quoting :: exit 0 pnpm check:pm-dispatch-gates :: exit 0 ``` Also at 0a383d6: `cd ui && pnpm exec vitest run` on the three `ui/scripts/__tests__` files that import `scripts/ci-shard-matrix.mjs` (`select-affected-tests-wiring`, `select-affected-tests`, `test-file-timings`): 3 files, 95 tests passed. </details> Tier H: `.github/workflows/ci.yml` is a governed surface, so the maintainer merges this by hand. `skip-changeset`: the diff touches only a workflow and root `scripts/`, which no package publishes. --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jSkaG5iii6WC7w4qSX)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f647430 commit e253f3a

3 files changed

Lines changed: 90 additions & 7 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,8 +1041,24 @@ jobs:
10411041
# simply never created. The literal after `||` is the full roster, read
10421042
# when the plan job published nothing (it died or timed out), and the gate
10431043
# reads this same expression, so the two cannot count different rosters.
1044+
#
1045+
# ⭐ FAIL-FAST IN THE MERGE QUEUE ONLY (#31517, ruling record 6730013883).
1046+
# On a `merge_group` run the first shard to go red cancels its siblings,
1047+
# so `Test Core` reads red when that shard ends instead of when the
1048+
# slowest shard ends, and the queue ejects the entry and rebuilds the
1049+
# entries behind it that much sooner. On every other event the
1050+
# expression is false and every shard runs to its end: the pull_request
1051+
# run is where an author reads every red shard in one round (#23177
1052+
# ruling B, item A1, comment 6139629628), and push and schedule runs stay
1053+
# complete. The queue is the exception because its job is a fast
1054+
# mergeable-or-not answer, and the PR run of the same diff already
1055+
# showed every red. It is the matrix's own `fail-fast`, ⛔ never a
1056+
# run-level cancel: a real red dominates the matrix result (`failure`,
1057+
# never `cancelled`, measured on run 30271824408, a fail-fast matrix),
1058+
# and `Test Core` reads `failure` as a veto, whereas a matrix cancelled
1059+
# with zero attestations passes its aggregate (#3668).
10441060
strategy:
1045-
fail-fast: false
1061+
fail-fast: ${{ github.event_name == 'merge_group' }}
10461062
matrix:
10471063
shard: ${{ fromJSON(needs.test-plan.outputs.shards || '[1, 2, 3, 4, 5, 6, 7, 8]') }}
10481064

@@ -1580,6 +1596,11 @@ jobs:
15801596
# non-zero exit, and the step exits with it, so the shard is red
15811597
# exactly when it was before — and now names every red package of the
15821598
# shard in one run instead of the first one turbo reached.
1599+
# A1 holds inside a shard on every event. Across shards the merge
1600+
# queue is the exception (#31517, ruling record 6730013883): there
1601+
# the matrix is fail-fast, so the first red shard cancels the
1602+
# others, and the queue run names that shard's reds only. The
1603+
# pull_request run of the same diff names every shard's.
15831604
#
15841605
# Measured before this was written: a three-package probe (turbo
15851606
# 2.11.5, `a#test` exits 1, `c` depends on `a`) ran `b` and `c` to
@@ -2716,8 +2737,13 @@ jobs:
27162737
timeout-minutes: 30
27172738
permissions:
27182739
contents: read
2740+
# Fail-fast on `merge_group` only, as on Test Core (#31517, ruling record
2741+
# 6730013883; the reasons are on the `test` job's strategy). Push and
2742+
# schedule, the other events this matrix runs on, run all three shards to
2743+
# their end. A red shard dominates the matrix result over the siblings it
2744+
# cancels, and `Dogfood Regression Gate` reads `failure` as a veto.
27192745
strategy:
2720-
fail-fast: false
2746+
fail-fast: ${{ github.event_name == 'merge_group' }}
27212747
matrix:
27222748
shard: [1, 2, 3]
27232749

@@ -3083,7 +3109,11 @@ jobs:
30833109
# 30271824408, a fail-fast matrix with one real failure + one cancelled
30843110
# sibling): a real shard failure DOMINATES the aggregate — it reads
30853111
# "failure", never "cancelled" — so passing `cancelled` masks no
3086-
# regression. That dominance is what does NOT hold for `abandoned`, which
3112+
# regression. That run is the merge queue's own shape since #31517: the
3113+
# dogfood matrix is fail-fast on `merge_group`, and the sibling a red
3114+
# shard cancels records "The strategy configuration was canceled
3115+
# because …" on its check run, which the cause read classifies as no
3116+
# supersession: a partially attested matrix it cut short reads red. That dominance is what does NOT hold for `abandoned`, which
30873117
# is why `abandoned` gets counting rather than a place in a word list.
30883118
name: Dogfood Regression Gate
30893119
needs: [dogfood, dogfood-verify, filter, queue-verdict]
@@ -4147,8 +4177,15 @@ jobs:
41474177
timeout-minutes: 30
41484178
permissions:
41494179
contents: read
4180+
# Fail-fast on `merge_group` only, as on Test Core (#31517, ruling record
4181+
# 6730013883; the reasons are on the `test` job's strategy). Every other
4182+
# event runs all sixteen shards to their end. The `Test UI` aggregate
4183+
# passes only `success` or a skip `filter` explains, so the matrix of a
4184+
# red shard and the siblings it cancelled reads red there. A fail-fast queue run is red, so a push never
4185+
# borrows it: `queue-verdict` takes only a `success` queue run, and
4186+
# `ui_full_green` counts only shards that concluded `success`.
41504187
strategy:
4151-
fail-fast: false
4188+
fail-fast: ${{ github.event_name == 'merge_group' }}
41524189
matrix:
41534190
shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
41544191

@@ -4373,7 +4410,9 @@ jobs:
43734410
# That is #4928's skipped-only-when-filter-succeeded guard, and it makes a
43744411
# queue build or PR that touches no ui path read green here the way Lint
43754412
# UI and Build & E2E UI read `skipped` there. Everything else is red:
4376-
# `failure`; `cancelled`, which is also what a shard killed by its own
4413+
# `failure`, which is also what the matrix reads when a red shard on a
4414+
# `merge_group` run cancelled its siblings by fail-fast (#31517);
4415+
# `cancelled`, which is also what a shard killed by its own
43774416
# `timeout-minutes` reads (on a superseded push this paints the OLD sha
43784417
# red, which no required check reads); a `skipped` that `filter` does not
43794418
# explain; and any lifecycle value such as the `abandoned` test-gate met

‎scripts/check-shard-attestation.mjs‎

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@
8080
* `cancelled`. So a superseded matrix masks no regression, and demanding
8181
* credentials there would paint the false red on the superseded SHA that
8282
* #3668 removed.
83+
* Since #31517 (ruling record 6730013883) the merge queue's `test` and
84+
* `dogfood` matrices are fail-fast, which is run 30271824408's own shape:
85+
* the red shard's `failure` dominates the leg, the leg's declared negative
86+
* vetoes the gate, and the shards the strategy cancelled are missing from
87+
* the count. Such a shard records `The strategy configuration was
88+
* canceled because "…" failed` on its check run, which
89+
* `classifyCancelAnnotations` reads as `other`, never a supersession.
8390
* ⛔ That argument covers a shard that RAN AND FAILED. It does not cover a
8491
* shard killed while still executing, which produces no verdict at all —
8592
* there is no `failure` to dominate (#16157, run 34007386254: `Test Core
@@ -1740,11 +1747,12 @@ const SELF_TEST_BATTERIES = Object.freeze({
17401747
'#26084 a planned roster: only the shards the plan started are counted': 16,
17411748
'#26084 the static guard holds the planned matrix and the gate to one expression': 10,
17421749
'#31519 a borrowed push: the gate skip and the seed-only credential guard': 8,
1750+
'#31517 merge-queue fail-fast: one shard red, the rest cancelled by the strategy': 8,
17431751
});
17441752

17451753
// DELETING an entry silences that battery's floor exactly as effectively as
17461754
// zeroing it, so the roster's own size is pinned too.
1747-
const SELF_TEST_BATTERY_FLOOR = 25;
1755+
const SELF_TEST_BATTERY_FLOOR = 26;
17481756

17491757
// The key an assertion is filed under when no battery is open. It is not a
17501758
// declared battery, so it reds by the same set difference rather than silently
@@ -2293,6 +2301,40 @@ async function selfTest() {
22932301
"#22653 control: the single-leg path keeps #22606's verdict and words — a killed CLI leg is red beside a superseded matrix shard",
22942302
);
22952303

2304+
// ── #31517: the merge queue's matrices are fail-fast ─────────────────────
2305+
// On `merge_group` the `test` and `dogfood` matrices stop their remaining
2306+
// shards once one is red (ruling record 6730013883). The shape is run
2307+
// 30271824408's: the red shard concludes `failure`, the shards the strategy
2308+
// cancelled conclude `cancelled` and attest nothing, and the matrix result
2309+
// reads `failure`. The annotation below is that run's cancelled shard (check
2310+
// run 89995981782), read from the check-run annotations endpoint.
2311+
battery('#31517 merge-queue fail-fast: one shard red, the rest cancelled by the strategy');
2312+
const FAIL_FAST_ANNOTATIONS = [annotation('The strategy configuration was canceled because "matrix._1" failed'), annotation('The operation was canceled.')];
2313+
const failFast = (ids) => testCore(ids, undefined, { result: 'failure' });
2314+
const fastShardsFirst = failFast([1, 3].map((n) => attest('test', n, 6)));
2315+
assert(!fastShardsFirst.ok, '#31517: shard 2 red, 1 and 3 attested before it, 4-6 cancelled by fail-fast ⇒ red');
2316+
assert(
2317+
fastShardsFirst.errors.some((e) => e.includes("reported result 'failure'")) &&
2318+
fastShardsFirst.errors.some((e) => [2, 4, 5, 6].every((n) => e.includes(`test-${n}-of-6`))),
2319+
'#31517: …by the declared negative AND by the count, which names the red shard and every shard it cancelled',
2320+
);
2321+
assert(!failFast([]).ok, '#31517: the first shard red before any sibling attested, all cancelled ⇒ red — `failure` is never the zero-attestation #3668 pass');
2322+
assert(
2323+
shardsNeedingCancelCause(testLegs('failure'), new Map([1, 3].map((n) => attest('test', n, 6)))).length === 0,
2324+
'#31517: a fail-fast matrix reads `failure`, so the gate asks no cancel cause — no API read can turn it green',
2325+
);
2326+
assert(classifyCancelAnnotations(FAIL_FAST_ANNOTATIONS).kind === 'other', "#31517: a shard cancelled by its matrix's fail-fast is never read as a supersession");
2327+
const ifItEverReadCancelled = testCore(only5, new Map([1, 2, 3, 4, 6].map((n) => [attestationId('test', n, 6), classifyCancelAnnotations(FAIL_FAST_ANNOTATIONS)])));
2328+
assert(
2329+
!ifItEverReadCancelled.ok && ifItEverReadCancelled.errors.some((e) => e.includes('The strategy configuration was canceled')),
2330+
'#31517: were the matrix ever to read `cancelled` after a sibling attested, the fail-fast cause still reds it, quoting the record',
2331+
);
2332+
assert(
2333+
!judge({ gate: 'Test Core', legs: [{ job: 'test', total: 8, result: 'failure', shards: [2, 5, 7], planResult: 'success' }], filterResult: 'success', present: new Map([attest('test', 5, 8)]), runId: '99' }).ok,
2334+
'#31517 × #26084: a planned roster cut short by fail-fast ⇒ red',
2335+
);
2336+
assert(!dogfood('failure', 'success', [fullDogfood[0], fullDogfood[3]]).ok, '#31517: dogfood 2/3 red, 3/3 cancelled by fail-fast, the CLI leg green ⇒ the Dogfood Regression Gate reads red');
2337+
22962338
// ── #22653: the per-shard cause read itself, offline ─────────────────────
22972339
battery('#22653 the cause read, offline: one jobs list, then one annotations read per unattested shard');
22982340
const CI_TEXT = readFileSync(join(scriptRepoRoot(), '.github', 'workflows', 'ci.yml'), 'utf8');

‎scripts/ci-shard-matrix.mjs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ function selfTest() {
217217
check(`${label} ⇒ refused naming '${needle}' (got ${JSON.stringify(message)})`, message !== null && message.includes(needle) && message.includes(`job '${job}'`));
218218
};
219219
const workflow = (body) => `on: push\njobs:\n${body}`;
220-
const matrixJob = (id, shardLine, extra = '') => ` ${id}:\n runs-on: ubuntu-latest\n${extra} strategy:\n fail-fast: false\n matrix:\n ${shardLine}\n steps:\n - run: echo\n`;
220+
// The strategy spells ci.yml's sharded jobs' `fail-fast` (merge-queue only, #31517), so the
221+
// reader is exercised over an expression line between `strategy:` and `matrix:`.
222+
const matrixJob = (id, shardLine, extra = '') => ` ${id}:\n runs-on: ubuntu-latest\n${extra} strategy:\n fail-fast: \${{ github.event_name == 'merge_group' }}\n matrix:\n ${shardLine}\n steps:\n - run: echo\n`;
221223

222224
battery('the live ci.yml');
223225
const live = readFileSync(join(REPO_ROOT, CI_WORKFLOW), 'utf8');

0 commit comments

Comments
 (0)