Skip to content

Commit e5131cf

Browse files
ci(scripts): read the shard sizes from ci.yml's matrix blocks instead of copying them (#26488)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9MKqYfea9FwGdabNwe4CdFw)_ Part of #26443 Part of #26431 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:新增一个读取器 `scripts/ci-shard-matrix.mjs`,从 ci.yml 的 `matrix:` 读分片数;分片器、必需上下文自测、分片证明自测三处不再自抄 /8、/3。 - 为什么改:以前改一次矩阵大小要找遍几处副本,副本都不是权威;现在数字只写在工作流里。 - 风险与回滚:只动仓内 CI 脚本和根 package.json 一个别名;读取器读不到矩阵就报错,不回落默认值。回滚就是 revert 本 PR。 - 先看:`scripts/ci-shard-matrix.mjs`(读取与拒收规则)、`scripts/partition-test-shards.mjs` 的 `SHARD_COUNT` 一行、`scripts/check-shard-attestation.mjs` 自测里改成派生的锚点。 - 两分钟验证:`node scripts/ci-shard-matrix.mjs` 应打印 `test 8 planned` / `dogfood 3 literal` / `test-ui 16 literal` 三行。 - 需要你做的:无,席位落地。 ## What changes - **New `scripts/ci-shard-matrix.mjs`**, the one reader of ci.yml's shard-matrix sizes. It reads a job's `strategy.matrix.shard` as text (no `yaml` dependency, because `check-shard-attestation.mjs` runs in jobs that install none and `partition-test-shards.mjs` needs the count synchronously at import). It understands the two spellings ci.yml uses: a literal list `[1, 2, 3]`, and a planned matrix `fromJSON(... || 'FALLBACK')`, whose size is the full-roster fallback. Every other shape is refused with an error naming the job: no such job, no `matrix:`, no `shard:`, two `matrix:` blocks, a planned matrix with no fallback, a block sequence, a scalar, quoted items, or a list that is not exactly 1..N. There is no default count. - **`scripts/partition-test-shards.mjs`**: `SHARD_COUNT` is now `liveShardTotal('test')` instead of the literal `8`. Self-test step 4 no longer compares the matrix with a copy of itself; it still reads back the two other ci.yml spellings (`--shard N/M` and the attestation's `--total`). - **`scripts/check-required-contexts.mjs`**: battery (9)'s shadowing-collision anchor `name: Test Core (… matrix.shard …/N)` is built from the live matrix size. Two header lines that still said `/3` now say `/N`. - **`scripts/check-shard-attestation.mjs`**: every self-test anchor taken from the live ci.yml derives its size from the reader. That covers the live display names of both matrices, the `TEST_MATRIX`/`GATE_PLAN` fallback rosters, the `--leg "test/N:` roster anchor, the literal-list control and the `TEST_NAME`/dogfood-name anchors. It also adds one assertion that the live test matrix reads as the planned shape those #26084 anchors assume (325 → 326 assertions). - **`package.json`**: `check:shard-attestation` runs `node scripts/ci-shard-matrix.mjs --self-test` first. lint.yml already runs that alias, so the reader's self-test runs in CI and `check:self-test-wired` counts it (239 → 240 scripts) without a workflow edit. ## Why The shard sizes were spelled in five self-tests and fixtures, and no copy was the authority. ci.yml's `matrix:` blocks are. Resizing a matrix meant editing every copy (#26443 Measured). This PR does the devx half. The ui half (two `/16` fixtures under `ui/scripts/__tests__/`) and `scripts/pm/check-expected-skips.mjs` (the #26431 roster child) are left for their own cards, so neither card closes here. ## Tests - `node scripts/ci-shard-matrix.mjs --self-test`: `✓ ci-shard-matrix self-test: 27 cases pass across 5 batteries (live: test=8, dogfood=3, test-ui=16).` The batteries are the live ci.yml (4), the literal list (3), the planned matrix (3), job boundaries (3) and refusals with no default (14). The refusal battery includes a checkout with no ci.yml (`ENOENT`, not defaulted). It uses the battery-name floor and the verdict handshake copied from `check-agent-model-declared.mjs`. - `node scripts/partition-test-shards.mjs --self-test`: `self-test OK (75 measured packages -> 78 shard items, 8 shards, …)`, exit 0. - `node scripts/check-required-contexts.mjs --self-test`: `174 assertions`, exit 0. - `node scripts/check-shard-attestation.mjs --self-test`: `326 assertions`, exit 0. - `node scripts/ci/plan-test-core-shards.mjs --self-test` (imports `SHARD_COUNT`): exit 0. ## Verification Gate families from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at head `35440f1d`: 44 derived, and all 44 ran locally with exit 0. They include `pnpm check:pm-dispatch-gates` (`✓ dispatch-gates self-test (fast tier): 1935 cases pass`), `check:self-test-wired`, `check:required-contexts`, `check:shard-attestation`, `check:cross-package-test-inputs`, and the four dist readers after a `./packages/**` build under the verify lock. `--ran` reconciliation: `44 derived, 44 run, 0 NOT-MEASURED (a DERIVED zero), 0 UNRUN`. The dispatch-gates slow tier was not run locally (no script is deleted or renamed). It runs on push to main and on the schedule. ## Acceptance notes - **The reader sits at `scripts/ci-shard-matrix.mjs`, not under a new `scripts/lib/`.** I wrote it under `scripts/lib/` first. Then `check:pm-dispatch-gates` went red on one case: `promoting 34 module(s) to gate files subtracts no inherited hint from any other family — LOST: scripts/check-test-completeness.mjs … <- scripts/partition-test-shards.mjs (scripts/lib/ci-shard-matrix.mjs)`. A multi-segment import specifier reads as a watch hint, and the modules that import `partition-test-shards.mjs` would lose it. A sibling specifier is the refused single-segment shape, and the case is green with it. - **Acceptance grep.** `git grep -nE "/(3|8|16)\b" scripts/*.mjs scripts/pm/*.mjs` (ui excluded, see below) has 105 hits outside the reader and `check-expected-skips.mjs`. None of them copies a live ci.yml matrix size. By class: - Prose and run citations, e.g. `Test Core (3/3)` in measured-incident comments. - Replays of measured runs at the width those runs had, e.g. the dogfood `WORKFLOW_TEXT` for run 38021470264, the six-shard replays, and the `test-N-of-3` synthetic judge fixtures. - Synthetic workflows that never read ci.yml (`scripts/pm/ci-failure.mjs` `Fixture shard (… /3)`). - CLI file-slice labels (`@objectstack/cli 2/3`), which are not matrix sizes. - Unrelated fractions (`options 1/2/3`, `15/16`). - **Left for other cards.** I edited neither of these: - `ui/scripts/__tests__/select-affected-tests-wiring.test.ts:54` and `ui/scripts/__tests__/select-affected-tests.test.ts:450-453` (`/16`) belong to `domain:ui`. - `scripts/pm/check-expected-skips.mjs:316,330,359` belongs to the #26431 roster child. - **Noted, not changed.** - `scripts/ci/select-shard-timings-run.mjs:117` still declares `SHARD_COUNT = 8`. Its header says that value is only its matchers' default (each run is judged at its own `Test Core (N/M)` width). The card's grep does not cover it, and neither does this card's file surface. - `scripts/pm/dispatch-gates.self-test.mjs:4898-4900` spells `Test UI (… /16)` in a synthetic `uiJobFixture`, not a read of ci.yml. - ci.yml's own comments (for example "splits the package list into eight") still name the counts. ci.yml is read-only for this card. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (no spec change) - [x] 2. Every name I added uses the casing for its kind. - [ ] 3. No formatter ran; `pnpm lint` passes. (no formatter ran; `pnpm lint` was not run locally, it is a repo-wide scan CI owns) - [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. (publishes nothing: in-repo tooling, `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. (the 维护者速读 section is Chinese by the dispatch convention) - [ ] The *Before Submitting* list in CONTRIBUTING.md is done. (local subset per the dispatch gate list; the full lint farm is CI's) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01ADHkmT3NzSaaWLSdqdDPMd --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmT3NzSaaWLSdqdDPMd)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2282aa8 commit e5131cf

5 files changed

Lines changed: 386 additions & 47 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
"check:workflow-step-name-quoting": "node scripts/check-workflow-step-name-quoting.mjs --self-test && node scripts/check-workflow-step-name-quoting.mjs",
165165
"check:select-shard-packages": "bash scripts/ci/select-shard-packages.selftest.sh",
166166
"check:select-gate-families": "bash scripts/ci/select-gate-families.selftest.sh",
167-
"check:shard-attestation": "node scripts/check-shard-attestation.mjs --self-test && node scripts/check-shard-attestation.mjs",
167+
"check:shard-attestation": "node scripts/ci-shard-matrix.mjs --self-test && node scripts/check-shard-attestation.mjs --self-test && node scripts/check-shard-attestation.mjs",
168168
"check:required-contexts": "node scripts/check-required-contexts.mjs --self-test && node scripts/check-required-contexts.mjs",
169169
"check:aggregator-roster": "node scripts/check-aggregator-roster.mjs --self-test && node scripts/check-aggregator-roster.mjs",
170170
"check:ci-filter-parity": "node scripts/check-ci-filter-parity.mjs --self-test && node scripts/check-ci-filter-parity.mjs",

‎scripts/check-required-contexts.mjs‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
* 2. the job id still exists in it;
4444
* 3. its `name:` is EXACTLY the registered literal — the pin proper;
4545
* 4. the job declares no `strategy.matrix`. A matrix job's check-run name is
46-
* the name with the matrix values appended (`Test Core (1/3)`), so a bare
46+
* the name with the matrix values appended (`Test Core (1/N)`), so a bare
4747
* registered name would never report. Worse, per #5617's audit a SKIPPED
4848
* matrix job publishes the name UNEXPANDED, so the two spellings are not
4949
* even stable across runs;
@@ -78,7 +78,7 @@
7878
* Two jobs with one name publish one context whose conclusion is whichever
7979
* finished last, so an unrelated job could satisfy a required gate. This
8080
* is live ammunition here rather than a hypothetical: ci.yml's sharded
81-
* `test` job is named `Test Core (${{ matrix.shard }}/3)` and its gate is
81+
* `test` job is named `Test Core (${{ matrix.shard }}/N)` and its gate is
8282
* named `Test Core`; deleting the suffix collides them.
8383
*
8484
* ## The required SET: readable, diffable, and deliberately not merge-blocking
@@ -194,6 +194,7 @@ import { fileURLToPath } from 'node:url';
194194
// the same narrow reading for the same defect one flag over (#10889).
195195
import { invokes, shellCommands } from './check-shard-attestation.mjs';
196196
import { isEntrypoint } from './invoked-as.mjs';
197+
import { shardTotal } from './ci-shard-matrix.mjs';
197198

198199
// ── The self-test's own battery roster and floor (#13489) ──────────────────
199200
//
@@ -2065,13 +2066,16 @@ async function selfTest() {
20652066
);
20662067

20672068
// ── (9) the shadowing collision, on the live specimen ────────────────────
2068-
// ci.yml's sharded `test` job is named `Test Core (${{ matrix.shard }}/8)`
2069+
// ci.yml's sharded `test` job is named `Test Core (${{ matrix.shard }}/N)`
20692070
// and its aggregate gate is named `Test Core`. Dropping the suffix makes two
20702071
// jobs publish one context, and the surviving conclusion is whichever
2071-
// finished last — a shard could satisfy the aggregate's required gate.
2072+
// finished last — a shard could satisfy the aggregate's required gate. N is
2073+
// the matrix's own size, read from the workflow (scripts/ci-shard-matrix.mjs),
2074+
// so a resized matrix moves this anchor with it.
20722075
battery('(9) the shadowing collision, on the live specimen');
2076+
const TEST_CORE_SHARD_NAME = `name: Test Core (\${{ matrix.shard }}/${shardTotal(sources['ci.yml'], 'test')})`;
20732077
const collided = fixture('collide the shard name with the gate name', 'ci.yml', (s) =>
2074-
s.replace('name: Test Core (${{ matrix.shard }}/8)', 'name: Test Core'),
2078+
s.replace(TEST_CORE_SHARD_NAME, 'name: Test Core'),
20752079
);
20762080
assert(
20772081
collided.problems.some((p) => p.includes("job 'test'") && p.includes("published by ci.yml:test-gate")),
@@ -2080,7 +2084,7 @@ async function selfTest() {
20802084
// The suffixed spelling is NOT a collision — the guard must not read a
20812085
// prefix as a clash, or ci.yml is red on main today.
20822086
assert(
2083-
baseline.problems.length === 0 && sources['ci.yml'].includes('name: Test Core (${{ matrix.shard }}/8)'),
2087+
baseline.problems.length === 0 && sources['ci.yml'].includes(TEST_CORE_SHARD_NAME),
20842088
'the real suffixed shard name coexists with the bare gate name (assertion 9 compares whole names, not prefixes)',
20852089
);
20862090

‎scripts/check-shard-attestation.mjs‎

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ import { tmpdir } from 'node:os';
227227
import { dirname, join, resolve } from 'node:path';
228228
import { fileURLToPath } from 'node:url';
229229
import { isEntrypoint } from './invoked-as.mjs';
230+
import { readShardMatrix, shardTotal } from './ci-shard-matrix.mjs';
230231

231232
/** Artifact-name prefix shared by every shard credential. */
232233
const ARTIFACT_PREFIX = 'shard-attest-';
@@ -2253,8 +2254,11 @@ async function selfTest() {
22532254
// The runs replayed below (38036705318, 34007386254) ran the SIX-shard matrix,
22542255
// which #25376 widened to eight, so they are read against the live workflow
22552256
// with the shard name as it stood then. The live names are pinned on their own
2256-
// at the end of this battery.
2257-
const LIVE_TEST_NAME = 'name: Test Core (${{ matrix.shard }}/8)';
2257+
// at the end of this battery, at the sizes the live matrices declare (read
2258+
// through scripts/ci-shard-matrix.mjs, so no size is copied here).
2259+
const LIVE_TEST_TOTAL = shardTotal(CI_TEXT, 'test');
2260+
const LIVE_DOGFOOD_TOTAL = shardTotal(CI_TEXT, 'dogfood');
2261+
const LIVE_TEST_NAME = `name: Test Core (\${{ matrix.shard }}/${LIVE_TEST_TOTAL})`;
22582262
const CI_TEXT_SIX_SHARDS = CI_TEXT.replace(LIVE_TEST_NAME, 'name: Test Core (${{ matrix.shard }}/6)');
22592263
assert(CI_TEXT_SIX_SHARDS !== CI_TEXT, `#22653: the live ci.yml no longer carries \`${LIVE_TEST_NAME}\` — the six-shard replays below would read the live roster`);
22602264
const PARTIAL_JOBS = /\/repos\/o\/r\/actions\/runs\/38036705318\/attempts\/1\/jobs\?per_page=100&page=1$/;
@@ -2349,8 +2353,9 @@ async function selfTest() {
23492353
'#22653: a matrix name with no shard expression cannot locate a shard ⇒ unreadable, naming the shard',
23502354
);
23512355

2352-
assert([1, 2, 3, 4, 5, 6, 7, 8].every((n) => workflowJobDisplayName(CI_TEXT, 'test', n) === `Test Core (${n}/8)`), '#22653: the real ci.yml names each Test Core shard as the jobs API lists it');
2353-
assert([1, 2, 3].every((n) => workflowJobDisplayName(CI_TEXT, 'dogfood', n) === `Dogfood Regression Gate (${n}/3)`), '#22653: …and each dogfood shard');
2356+
const oneTo = (total) => Array.from({ length: total }, (_, i) => i + 1);
2357+
assert(oneTo(LIVE_TEST_TOTAL).every((n) => workflowJobDisplayName(CI_TEXT, 'test', n) === `Test Core (${n}/${LIVE_TEST_TOTAL})`), '#22653: the real ci.yml names each Test Core shard as the jobs API lists it');
2358+
assert(oneTo(LIVE_DOGFOOD_TOTAL).every((n) => workflowJobDisplayName(CI_TEXT, 'dogfood', n) === `Dogfood Regression Gate (${n}/${LIVE_DOGFOOD_TOTAL})`), '#22653: …and each dogfood shard');
23542359
assert(workflowJobDisplayName(CI_TEXT, 'dogfood-verify') === 'Dogfood Verify CLI', "#22653 control: the single leg's literal name reads as before");
23552360
assert(workflowJobDisplayName(CI_TEXT, 'test') === undefined, '#22653: a matrix template asked WITHOUT a shard is still unresolvable ⇒ undefined');
23562361
assert(workflowJobDisplayName('jobs:\n m:\n runs-on: x\n', 'm', 1) === undefined, '#22653: a nameless matrix job is not guessed at — GitHub appends the matrix values itself');
@@ -2683,10 +2688,15 @@ async function selfTest() {
26832688
// start mutating dogfood's matrix instead — still red, still for a
26842689
// plausible-looking reason, and no longer the assertion written here.
26852690
// Since #26084 the test matrix is planned, and its full roster is the
2686-
// fallback literal after `||`; growing THAT is growing the matrix.
2687-
const TEST_MATRIX = "shard: ${{ fromJSON(needs.test-plan.outputs.shards || '[1, 2, 3, 4, 5, 6, 7, 8]') }}";
2688-
assert((await fixture('grow the matrix', (s) => s.replace(TEST_MATRIX, TEST_MATRIX.replace('8]', '8, 9]')))).problems.some((p) => p.includes('declares 9 shard')), 'growing the matrix without the gate ⇒ red');
2689-
assert((await fixture('shrink the roster', (s) => s.replace('--leg "test/8', '--leg "test/2'))).problems.some((p) => p.includes('expects 2 attestation')), 'shrinking the gate roster without the matrix ⇒ red');
2691+
// fallback literal after `||`; growing THAT is growing the matrix. The
2692+
// roster is the one the live matrix declares, spelled as ci.yml spells it.
2693+
const liveTest = readShardMatrix(good, 'test');
2694+
const N = liveTest.total;
2695+
const ROSTER = JSON.stringify(liveTest.values).replaceAll(',', ', ');
2696+
assert(liveTest.form === 'planned', `ci.yml's test matrix reads as ${liveTest.form}, not planned — the #26084 anchors below assume the planned shape`);
2697+
const TEST_MATRIX = `shard: \${{ fromJSON(needs.test-plan.outputs.shards || '${ROSTER}') }}`;
2698+
assert((await fixture('grow the matrix', (s) => s.replace(TEST_MATRIX, TEST_MATRIX.replace(`${N}]`, `${N}, ${N + 1}]`)))).problems.some((p) => p.includes(`declares ${N + 1} shard`)), 'growing the matrix without the gate ⇒ red');
2699+
assert((await fixture('shrink the roster', (s) => s.replace(`--leg "test/${N}:`, '--leg "test/2:'))).problems.some((p) => p.includes('expects 2 attestation')), 'shrinking the gate roster without the matrix ⇒ red');
26902700
assert((await fixture('uncounted intruder', (s) => s.replace(/^jobs:$/m, 'jobs:\n intruder:\n runs-on: ubuntu-latest\n steps:\n - run: node scripts/check-shard-attestation.mjs --emit'))).problems.some((p) => p.includes('no gate counts')), 'an attestation no gate counts ⇒ red');
26912701
assert((await fixture('rename test-gate', (s) => s.replace(' test-gate:', ' test-gate-renamed:'))).problems.some((p) => p.includes("'test-gate'")), 'losing a required-context gate ⇒ red');
26922702
assert((await fixture('typo the artifact name', (s) => s.replace('name: shard-attest-test-', 'name: shard-attest-typo-'))).problems.some((p) => p.includes('cannot count')), 'an artifact name the gate cannot match ⇒ red');
@@ -2707,16 +2717,16 @@ async function selfTest() {
27072717

27082718
// ── #26084: the planned matrix and the gate read one expression ─────────
27092719
battery('#26084 the static guard holds the planned matrix and the gate to one expression');
2710-
const GATE_PLAN = " OS_TEST_PLAN: ${{ needs.test-plan.outputs.shards || '[1, 2, 3, 4, 5, 6, 7, 8]' }}\n";
2720+
const GATE_PLAN = ` OS_TEST_PLAN: \${{ needs.test-plan.outputs.shards || '${ROSTER}' }}\n`;
27112721
const planProblem = async (label, mutate, ...needles) => hasPlanProblem(await fixture(label, mutate), ...needles);
27122722
const hasPlanProblem = (result, ...needles) => result.problems.some((p) => needles.every((needle) => p.includes(needle)));
27132723
assert(
2714-
await planProblem('the gate reads another fallback', (s) => s.replace(GATE_PLAN, GATE_PLAN.replace(", 7, 8]", ']')), 'not the matrix', '#26084'),
2724+
await planProblem('the gate reads another fallback', (s) => s.replace(GATE_PLAN, GATE_PLAN.replace(`, ${N}]`, ']')), 'not the matrix', '#26084'),
27152725
'#26084: a gate roster expression that differs from the matrix by one byte ⇒ red',
27162726
);
27172727
assert(
27182728
await planProblem('the gate loses --planned', (s) => s.replace(' \\\n --planned "test=$OS_TEST_PLAN"', ''), 'without --planned', '#26084'),
2719-
'#26084: a gate that counts a planned leg against 1..8 ⇒ red',
2729+
'#26084: a gate that counts a planned leg against its full 1..N roster ⇒ red',
27202730
);
27212731
assert(
27222732
await planProblem('the gate loses --plan-result', (s) => s.replace(' \\\n --plan-result "$OS_TEST_PLAN_RESULT"', ''), '--plan-result', '#26084'),
@@ -2739,7 +2749,7 @@ async function selfTest() {
27392749
'#26084: a planned matrix with no full-roster fallback ⇒ red',
27402750
);
27412751
assert(
2742-
await planProblem('--planned on a literal matrix', (s) => s.replace(TEST_MATRIX, 'shard: [1, 2, 3, 4, 5, 6, 7, 8]'), 'literal list', '#26084'),
2752+
await planProblem('--planned on a literal matrix', (s) => s.replace(TEST_MATRIX, `shard: ${ROSTER}`), 'literal list', '#26084'),
27432753
'#26084: --planned beside a literal matrix ⇒ red (nothing to narrow)',
27442754
);
27452755
assert(plannedMatrix("${{ fromJSON(needs.p.outputs.s || '[1, 2]') }}")?.total === 2 && plannedMatrix('[1, 2]') === null, '#26084 plannedMatrix: reads the planned shape, ignores a literal list');
@@ -2818,29 +2828,30 @@ async function selfTest() {
28182828
hasProblem(await fixture('drop the token (test-gate)', inJob('test-gate', TOKEN_LINE, '')), "gate 'test-gate'", 'GITHUB_TOKEN'),
28192829
"#22653: test-gate's verify step without GITHUB_TOKEN ⇒ red",
28202830
);
2821-
const TEST_NAME = ' name: Test Core (${{ matrix.shard }}/8)\n';
2831+
const TEST_NAME = ` name: Test Core (\${{ matrix.shard }}/${N})\n`;
2832+
const DOGFOOD_NAME = ` name: Dogfood Regression Gate (\${{ matrix.shard }}/${shardTotal(good, 'dogfood')})\n`;
28222833
assert(
28232834
hasProblem(await fixture('a matrix name without the shard expression', (s) => s.replace(TEST_NAME, ' name: Test Core shards\n')), '#22653', "job 'test' is named"),
28242835
'#22653: a matrix name with no `${{ matrix.shard }}` ⇒ red — the platform would append the values itself, a name the reader cannot rebuild',
28252836
);
28262837
assert(
2827-
hasProblem(await fixture('a matrix name with a second expression', (s) => s.replace(TEST_NAME, ' name: Test Core (${{ matrix.shard }}/8) ${{ github.event_name }}\n')), '#22653', "job 'test' is named"),
2838+
hasProblem(await fixture('a matrix name with a second expression', (s) => s.replace(TEST_NAME, TEST_NAME.replace('\n', ' ${{ github.event_name }}\n'))), '#22653', "job 'test' is named"),
28282839
'#22653: any expression besides the shard ⇒ red — the text reading cannot evaluate it',
28292840
);
28302841
assert(
28312842
hasProblem(
2832-
await fixture('the dogfood matrix name on another matrix key', (s) => s.replace(' name: Dogfood Regression Gate (${{ matrix.shard }}/3)\n', ' name: Dogfood Regression Gate (${{ matrix.os }}/3)\n')),
2843+
await fixture('the dogfood matrix name on another matrix key', (s) => s.replace(DOGFOOD_NAME, DOGFOOD_NAME.replace('matrix.shard', 'matrix.os'))),
28332844
'#22653',
28342845
"job 'dogfood' is named",
28352846
),
28362847
"#22653: dogfood's matrix leg is held to the same name rule — it is asked on a partial cancel too",
28372848
);
28382849
assert(
2839-
(await fixture('the matrix name double-quoted', (s) => s.replace(TEST_NAME, ' name: "Test Core (${{ matrix.shard }}/8)"\n'))).problems.length === 0,
2850+
(await fixture('the matrix name double-quoted', (s) => s.replace(TEST_NAME, ` name: "Test Core (\${{ matrix.shard }}/${N})"\n`))).problems.length === 0,
28402851
'#22653 positive limb: a quoted template reads the same from text and from YAML ⇒ green',
28412852
);
28422853
assert(
2843-
(await fixture('the shard expression without inner spaces', (s) => s.replace(TEST_NAME, ' name: Test Core (${{matrix.shard}}/8)\n'))).problems.length === 0,
2854+
(await fixture('the shard expression without inner spaces', (s) => s.replace(TEST_NAME, TEST_NAME.replace('${{ matrix.shard }}', '${{matrix.shard}}')))).problems.length === 0,
28442855
'#22653 positive limb: `${{matrix.shard}}` is the same expression ⇒ green',
28452856
);
28462857
assert(baseline.problems.length === 0 && baseline.gateIds.includes('test-gate'), '#22653: the checked-in test-gate carries every prerequisite — the baseline is green with it counted as a gate');

0 commit comments

Comments
 (0)