Skip to content

Commit b97fe92

Browse files
ci(ui-coverage): run objectui's coverage lane nightly from the root, thresholds unchanged (#26003)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D15ZXd9H65VV7icrwS78oFd)_ Part of #25924 (item 2 of the card's Recommendation: the coverage lane on a schedule, thresholds unchanged). Part of #25677 (roster row 7 of #25697, "Test (coverage shard n/8) · Test (coverage)": kept, follow-up port). Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 新增根 workflow `ui-coverage-nightly.yml`(名为 `UI Coverage`),把 objectui 的覆盖率车道(8 个分片 + 1 个合并任务)搬回根目录,只在每晚 06:13 UTC 定时跑,也可以手动触发。阈值不变(lines 40、functions 33、branches 30、statements 40),仍由合并任务在完整报告上检查。 - **为什么:** 卡上说的「每次 push 都跑」在合并后的仓库里已经不成立:ui/ 合并时这条车道没有移植,`ui/.github/` 里那份 GitHub 不会执行,所以从合并那天起这四个阈值没有任何地方在检查。现在按卡的建议以定时方式补回,不回到每次 push。 - **谁会发现红:** `scheduled-full-run-card.yml` 现在也监听 `UI Coverage`,定时运行红了就开或刷新一张卡 `nightly full run: red on main (UI Coverage)`。 - **不变的:** 不进必需检查,不进发版门(`RELEASE_GATE_WORKFLOWS` 仍只有 `CI` 和 `Lint & Type Check`),PR 和合并队列都不跑它。 - **代价:** 覆盖率掉到阈值以下要到第二天早上才报出来(卡上已接受);每晚约 8×22 分钟 runner。 - **卡上的另一项(install 重试)没有在本 PR 里做**,原因见下文,交席位定。 - **受管面:** `.github/workflows/**`,Tier H,需要 GOVERNED_APPROVERS 批准。 ## What changed | File | Change | |---|---| | `.github/workflows/ui-coverage-nightly.yml` | New. `ui-coverage` (`Test UI (coverage shard n/8)`, 8 shards, 40 min): install, the seven-package build ci.yml's ui jobs use, then `pnpm --dir ui run test:coverage` with `--reporter=blob --reporter=default --reporter=github-actions --shard=n/8 --testTimeout=60000` and the thresholds zeroed on the legs; uploads `ui/.vitest-reports/*`. `ui-coverage-report` (`Test UI (coverage)`): refuses a partial blob set, merges with `--merge-reports` (where the configured thresholds are enforced), publishes `ui/coverage/`, prints the shard headroom (report-only) and states whether the gate ran. Triggers: `schedule` `13 6 * * *` and `workflow_dispatch`. Caches are restore-only (Build UI Deps' turbo seed, the pnpm store). | | `.github/workflows/scheduled-full-run-card.yml` | Watches `UI Coverage` too. | | `scripts/ci/scheduled-full-run.mjs` | `CARD_CADENCES['UI Coverage'] = 'nightly'` (the `nightly` wording already existed). Self-test +4 cases: the nightly identity, the nightly body, the workflow's single daily cron, and the watcher list and the cadence map naming the same workflows. Battery floor 42 → 46. | | `ui/scripts/__tests__/coverage-shard-width.test.ts`, `coverage-shard-reporter-readability.test.ts` | Restored from before #25879 (they retired with the lane), now reading the new file through `helpers/root-ci.ts`. | | `ui/scripts/__tests__/shard-headroom.test.ts` | The coverage half of the retired wiring pin comes back against `ui-coverage-report`; the `test-aggregate` half stays retired (no root job). | | `ui/scripts/__tests__/helpers/root-ci.ts`, `ui/scripts/shard-headroom.mjs` | `UI_COVERAGE` (file + job keys); header prose. | ## Measurements - **The coverage lane's trigger on the merged repo:** no root workflow runs `test:coverage` for `ui/` (`grep -rn 'test:coverage' .github/workflows/` → only `coverage-nightly.yml`, which is spec's). objectui's `test-coverage` / `coverage-report` jobs exist only in `ui/.github/workflows/ci.yml`, which GitHub does not run. The ui coverage thresholds have had no reader since the cutover. - **Readers checked before adding the lane:** required contexts (branch ruleset and classic protection: none of the new names, and none is added); `RELEASE_GATE_WORKFLOWS` (unchanged); `check-ci-filter-parity` (reads ci.yml only, which this PR does not touch); the aggregator roster (no ci.yml aggregator involved); `merge-queue-triage.yml` (`workflows: [CI]`, unchanged). The one reader added is the red-run card filer. - **Install retry (card item 4), not done here:** the card's landing spot `scripts/ci-setup-pnpm.sh` is now `ui/scripts/ci-setup-pnpm.sh`, and no workflow GitHub runs calls it (only `ui/.github/workflows/*`); it also never ran the install, only Corepack. The root's pnpm acquisition (`.github/actions/setup-pnpm`) already retries. `pnpm install --frozen-lockfile` is spelled at ~40 sites in 25 root workflows. Actions API, failed runs created since 2026-10-19 (632 runs): exactly 1 job failed at an install step, run 39316437234 `Type Check · debt ledger`, `node-gyp` headers download ECONNRESET from nodejs.org. The choice of landing (a shared install action at every site, an inline retry, or no change) goes to the devx seat on the card. ## Verification - `node scripts/ci/scheduled-full-run.mjs --self-test`: OK (battery 46 cases). - Both changed workflows parse; the new one declares exactly the two jobs and the two triggers above. - The derived dispatch gates and the three ui pins: see the report on #25924 (local install is slow in this container; CI's `Lint & Repo Gates` and `Test UI` run them on this PR too). - The new workflow cannot run on a pull request (schedule and dispatch only). The first real reading is a `workflow_dispatch` on `main` after merge, or the first night. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01WKpunbiE8c9w2i9FsEvQas Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5e113b3 commit b97fe92

8 files changed

Lines changed: 755 additions & 20 deletions

File tree

‎.github/workflows/scheduled-full-run-card.yml‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,13 @@ on:
8787
#
8888
# Every name listed here must have an entry in `CARD_CADENCES`
8989
# (scripts/ci/scheduled-full-run.mjs), which refuses a name it does not
90-
# hold rather than default one, so a card never prints the wrong cadence.
91-
workflows: ['CI', 'Lint & Type Check']
90+
# hold rather than default one, so a card never prints the wrong cadence;
91+
# its self-test holds this list and that map to the same names.
92+
#
93+
# `UI Coverage` (ui-coverage-nightly.yml, #25924) is the nightly one: the
94+
# ui coverage thresholds are enforced there and nowhere else, so a red
95+
# night files `nightly full run: red on main (UI Coverage)`.
96+
workflows: ['CI', 'Lint & Type Check', 'UI Coverage']
9297
types: [completed]
9398

9499
permissions: {}

‎.github/workflows/ui-coverage-nightly.yml‎

Lines changed: 370 additions & 0 deletions
Large diffs are not rendered by default.

‎scripts/ci/scheduled-full-run.mjs‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ const titlePrefix = (cadence) => `${cadence} full run: red on main`;
289289
export const CARD_CADENCES = Object.freeze({
290290
CI: '4-hourly',
291291
'Lint & Type Check': '4-hourly',
292+
// ui-coverage-nightly.yml (#25924): objectui's coverage lane, nightly.
293+
'UI Coverage': 'nightly',
292294
});
293295

294296
// THE CADENCES A CADENCE REPLACED, newest first. `CI` and `Lint & Type Check`
@@ -1072,7 +1074,7 @@ async function releaseGate(argv) {
10721074
// remedy is to find what stopped registering, never to lower the number.
10731075
const SELF_TEST_BATTERIES = Object.freeze({
10741076
'scheduled-full-run concurrency isolation': 32,
1075-
'scheduled-full-run card identity and de-dup': 42,
1077+
'scheduled-full-run card identity and de-dup': 46,
10761078
'card refresh: a stale pm:blocked (the measured #23885 sequence)': 22,
10771079
'release gate: the selection rule on the recorded 2026-10-12 listing': 8,
10781080
'release gate: the selection rule on synthetic runs': 17,
@@ -1525,6 +1527,40 @@ async function selfTest() {
15251527
}
15261528
});
15271529
}
1530+
// UI Coverage is the one NIGHTLY workflow the card watches (#25924): its card
1531+
// says nightly, never 4-hourly, and the workflow runs exactly one daily cron.
1532+
const uiCoverage = cardIdentity('UI Coverage');
1533+
await check(() => {
1534+
if (uiCoverage.cadence !== 'nightly' || uiCoverage.title !== 'nightly full run: red on main (UI Coverage)' || uiCoverage.marker !== 'os-nightly-full-run-ui-coverage') {
1535+
throw new Error(`cadence: the UI Coverage identity is (${uiCoverage.cadence}, '${uiCoverage.title}', '${uiCoverage.marker}'), not the nightly one`);
1536+
}
1537+
});
1538+
await check(() => {
1539+
for (const b of bodiesOf(uiCoverage)) {
1540+
if (!/every night/.test(b)) throw new Error(`cadence: a nightly card body does not say nightly:\n${b}`);
1541+
if (/every 4 hours|4 hours ago|every hour\b|an hour ago/.test(b)) throw new Error(`cadence: a nightly card body names another cadence:\n${b}`);
1542+
}
1543+
});
1544+
await check(() => {
1545+
const file = '.github/workflows/ui-coverage-nightly.yml';
1546+
const text = readFileSync(file, 'utf8');
1547+
const name = /^name:\s*(.+?)\s*$/m.exec(text)?.[1];
1548+
if (name !== 'UI Coverage') throw new Error(`cadence: ${file} is named '${name}', not the 'UI Coverage' CARD_CADENCES holds`);
1549+
const crons = [...text.matchAll(/^\s*-\s*cron:\s*'([^']+)'/gm)].map((m) => m[1]);
1550+
if (crons.length !== 1 || !/^\d{1,2} \d{1,2} \* \* \*$/.test(crons[0])) {
1551+
throw new Error(`cadence: ${file} declares ${JSON.stringify(crons)}, but CARD_CADENCES says nightly (one daily cron)`);
1552+
}
1553+
});
1554+
await check(() => {
1555+
// Every name the watcher lists has a cadence, and every cadence is watched.
1556+
const text = readFileSync('.github/workflows/scheduled-full-run-card.yml', 'utf8');
1557+
const line = /^\s*workflows:\s*\[(.*)\]\s*$/m.exec(text)?.[1] ?? '';
1558+
const watched = [...line.matchAll(/'([^']+)'/g)].map((m) => m[1]).sort();
1559+
const held = Object.keys(CARD_CADENCES).sort();
1560+
if (JSON.stringify(watched) !== JSON.stringify(held)) {
1561+
throw new Error(`cadence: scheduled-full-run-card.yml watches ${JSON.stringify(watched)}, CARD_CADENCES holds ${JSON.stringify(held)}`);
1562+
}
1563+
});
15281564
await check(() => {
15291565
let message = '';
15301566
try {
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { parse as parseYaml } from 'yaml';
3+
4+
import { UI_COVERAGE, rootWorkflow } from './helpers/root-ci';
5+
6+
/**
7+
* The pin on the coverage shard leg's reporter set (objectui#9177).
8+
*
9+
* ## What this guards, and why nothing else could
10+
*
11+
* The root `ui-coverage-nightly.yml`'s `ui-coverage` job (objectui's `ci.yml`
12+
* `test-coverage`, ported nightly by objectstack#25924) runs vitest as a
13+
* sharded matrix and merges
14+
* every shard's blob report in the job below it, which is where the configured
15+
* coverage thresholds are enforced. A CLI `--reporter` **replaces** the
16+
* reporter set rather than adding to it, and vitest's resolution is:
17+
*
18+
* if (!resolved.reporters.length) {
19+
* resolved.reporters.push([isAgent ? "agent" : "default", {}]);
20+
* if (process.env.GITHUB_ACTIONS === "true")
21+
* resolved.reporters.push(["github-actions", {}]);
22+
* }
23+
*
24+
* so `--reporter=blob` on its own displaced BOTH halves of that pair. A red
25+
* shard's log then ended at `blob report written to …` — no failing test name,
26+
* no assertion text, no timeout message — and the shard job's only annotation
27+
* was the generic `Process completed with exit code 1.` The failures survived
28+
* only inside the `coverage-blob-N` artifact, which is download-only.
29+
*
30+
* objectui#8545 measured what that costs: two episodes in which one test file
31+
* held `main`'s coverage gate unevaluated for **84** and **87** consecutive
32+
* pushes, both found the same way — a person reading a job log by hand, days
33+
* later.
34+
*
35+
* Nothing else in this repository reads what that step runs.
36+
* `ci-cd-pipeline-doc.test.ts` pins the pairing between `ci.yml` and the
37+
* pipeline page, but it compares COMMANDS, not flags — its extractor is
38+
*
39+
* for (const m of text.matchAll(/\bpnpm\s+([\w:.-]+)/g)) {
40+
* if (rootScripts.has(m[1])) found.add(`pnpm ${m[1]}`);
41+
* }
42+
*
43+
* whose capture stops at the first space, so `pnpm test:coverage` is all it
44+
* ever sees. Every flag on that line could be deleted and that file stays
45+
* green.
46+
*
47+
* ## Both directions, because the failure has two shapes
48+
*
49+
* Dropping `--reporter=default` / `--reporter=github-actions` restores
50+
* objectui#9177. Dropping `--reporter=blob` to "simplify" the line is the same
51+
* defect wearing the other hat: the merge job loses the only input it has, so
52+
* a readable failure is bought with an unevaluated coverage floor
53+
* (objectui#5403). Neither is caught by CI going green, because the shard leg
54+
* passing is exactly the state in which nobody looks at its log.
55+
*
56+
* ⛔ If you are deleting this file, you are deleting the only mechanical guard
57+
* on that reporter set. The measurement behind it is in the ⚠️ comment above
58+
* the step in `ui-coverage-nightly.yml`; re-run it before you change the line.
59+
*/
60+
const workflowSource = rootWorkflow(UI_COVERAGE.file);
61+
62+
/** The reporter the merge job needs, and the two that keep the log readable. */
63+
const BLOB_REPORTER = '--reporter=blob';
64+
const READABLE_REPORTERS = ['--reporter=default', '--reporter=github-actions'];
65+
66+
interface Step {
67+
name?: string;
68+
run?: unknown;
69+
}
70+
71+
/**
72+
* Every `run:` body in the `ui-coverage` job that invokes the coverage suite.
73+
*
74+
* Read through the YAML parser rather than off the raw text: the step is a
75+
* folded block scalar (`run: >-`), so the flags are spread over four source
76+
* lines and a regex over the file would have to re-implement the folding to see
77+
* the command the runner actually executes. Matching on `test:coverage` rather
78+
* than on the full string is what makes a rewrite FAIL here instead of
79+
* vanishing — a pin that searched for today's exact line would simply stop
80+
* finding anything and report a healthy green over a step that no longer
81+
* contains it.
82+
*/
83+
function coverageRunSteps(source: string): Array<{ step: string; run: string }> {
84+
const workflow = parseYaml(source) as {
85+
jobs?: Record<string, { steps?: Step[] }>;
86+
};
87+
const job = workflow.jobs?.[UI_COVERAGE.shards];
88+
expect(
89+
job,
90+
`${UI_COVERAGE.file} no longer defines a \`${UI_COVERAGE.shards}\` job. If the coverage lane was restructured, this pin ` +
91+
'needs a new referent — do not delete it without giving the reporter set one.',
92+
).toBeDefined();
93+
const found: Array<{ step: string; run: string }> = [];
94+
for (const step of job?.steps ?? []) {
95+
if (typeof step.run !== 'string') continue;
96+
if (!step.run.includes('test:coverage')) continue;
97+
found.push({ step: step.name ?? '(unnamed)', run: step.run });
98+
}
99+
return found;
100+
}
101+
102+
describe("the ui coverage shards keep a red run readable", () => {
103+
it('still runs the coverage suite at all — the pin is not vacuous', () => {
104+
// Without this, every assertion below passes over a job that stopped running
105+
// vitest, which is the one way a `toContain` pin lies.
106+
const steps = coverageRunSteps(workflowSource);
107+
expect(
108+
steps.map((s) => s.step),
109+
`no step in ${UI_COVERAGE.file}'s \`${UI_COVERAGE.shards}\` job runs \`test:coverage\` any more. If that is ` +
110+
'deliberate, this pin is obsolete and should be deleted WITH the reasoning that made it ' +
111+
'obsolete written down. If it is not, the nightly lane just lost its coverage measurement.',
112+
).not.toEqual([]);
113+
});
114+
115+
it('is reading the shard leg, not some other coverage invocation', () => {
116+
// Names the population the two assertions below judge: the sharded legs.
117+
// `--merge-reports` in the job below is a different step in a different job
118+
// and is deliberately out of reach here.
119+
const sharded = coverageRunSteps(workflowSource).filter((s) => s.run.includes('--shard='));
120+
expect(
121+
sharded.map((s) => s.step),
122+
`the \`${UI_COVERAGE.shards}\` job no longer passes \`--shard=\` to the coverage suite. The reporter ` +
123+
'rule below is about the SHARDED legs specifically — an unsharded lane has no blob to ' +
124+
'keep and a different set of reporters is right for it.',
125+
).not.toEqual([]);
126+
});
127+
128+
it('passes a reporter that prints failing test names alongside the blob', () => {
129+
const offenders = coverageRunSteps(workflowSource)
130+
.filter((s) => s.run.includes(BLOB_REPORTER))
131+
.flatMap((s) =>
132+
READABLE_REPORTERS.filter((r) => !s.run.includes(r)).map((r) => `${s.step}: missing ${r}`),
133+
);
134+
135+
expect(
136+
offenders,
137+
'a coverage shard passes `--reporter=blob` without the reporters that keep its log readable.\n' +
138+
offenders.map((o) => ` - ${o}`).join('\n') +
139+
'\n\nA CLI `--reporter` REPLACES the default set rather than adding to it, so blob alone ends a ' +
140+
'failing shard at `blob report written to …` with no test name and no assertion text, and leaves ' +
141+
'the download-only artifact as the only copy of the failure. `--reporter=default` restores the ' +
142+
'job log; `--reporter=github-actions` restores the per-test `::error` annotation, which is the ' +
143+
'only form an API reader can get without downloading anything. objectui#9177; the cost is ' +
144+
'objectui#8545, where one test file held the coverage gate unevaluated for 84 and 87 pushes.',
145+
).toEqual([]);
146+
});
147+
148+
it('keeps the blob reporter the merge job depends on', () => {
149+
const offenders = coverageRunSteps(workflowSource)
150+
.filter((s) => s.run.includes('--shard='))
151+
.filter((s) => !s.run.includes(BLOB_REPORTER))
152+
.map((s) => s.step);
153+
154+
expect(
155+
offenders,
156+
'a sharded coverage leg no longer writes a blob report:\n' +
157+
offenders.map((o) => ` - ${o}`).join('\n') +
158+
`\n\nThe \`${UI_COVERAGE.report}\` job merges every shard's blob and enforces the configured thresholds over ` +
159+
'the merged report — that is the whole reason the lane is sharded (objectui#5403). Restoring log ' +
160+
'readability by dropping the blob would trade an unreadable failure for an unevaluated coverage ' +
161+
'floor, which is the same defect wearing the other hat (objectui#9177).',
162+
).toEqual([]);
163+
});
164+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { parse as parseYaml } from 'yaml';
3+
4+
import { UI_COVERAGE, rootWorkflow } from './helpers/root-ci';
5+
6+
/**
7+
* The coverage lane's width, held to ONE number (objectui#12256, folded into
8+
* objectui#12208).
9+
*
10+
* The lane runs in the root `.github/workflows/ui-coverage-nightly.yml`
11+
* (objectstack#25924), where objectui's `ci.yml` jobs `test-coverage` and
12+
* `coverage-report` became `ui-coverage` and `ui-coverage-report`.
13+
*
14+
* The lane spells its shard count in two jobs. `ui-coverage`
15+
* declares the matrix and repeats the count in its job name, its step name and
16+
* its `--shard=N/M` flag; `ui-coverage-report` counts the blob reports the shards
17+
* wrote and refuses to merge a partial set. The blob's FILE NAME carries the
18+
* count too (`blob-N-M.json`, written by vitest from the `--shard` flag), so a
19+
* width moved in the matrix alone leaves the merge looking for blobs that no
20+
* shard writes: the count step answers zero, the merge never runs, and the
21+
* coverage gate is unevaluated on every night -- the lane's own worst failure,
22+
* one edit away. Nothing about that is visible on a pull request, because the
23+
* lane runs on `schedule` and `workflow_dispatch` only.
24+
*
25+
* So every spelling is read here and held to the matrix:
26+
*
27+
* - the matrix is the contiguous run 1..M;
28+
* - the job name, the step name and `--shard=` all say M;
29+
* - `ui-coverage-report` declares `COVERAGE_SHARDS: 'M'`, and its count step
30+
* reads THAT variable for both the blob glob and the expected count, with
31+
* no literal width left in the step;
32+
* - the merge step is a `--merge-reports` that overrides no threshold, so
33+
* the configured `coverage.thresholds` are what is enforced over the merge
34+
* of all M blobs -- the one place the gate runs, since the shard legs
35+
* zero the thresholds.
36+
*/
37+
const workflowSource = rootWorkflow(UI_COVERAGE.file);
38+
39+
interface Step {
40+
name?: string;
41+
run?: unknown;
42+
id?: string;
43+
}
44+
interface Job {
45+
name?: string;
46+
env?: Record<string, unknown>;
47+
strategy?: { matrix?: { shard?: unknown } };
48+
steps?: Step[];
49+
}
50+
51+
const workflow = parseYaml(workflowSource) as { jobs?: Record<string, Job> };
52+
53+
function job(key: string): Job {
54+
const found = workflow.jobs?.[key];
55+
expect(found, `${UI_COVERAGE.file} no longer defines a \`${key}\` job; this pin needs a new referent, not deletion`).toBeDefined();
56+
return found as Job;
57+
}
58+
59+
/** The matrix width, after checking the matrix is the contiguous run 1..M. */
60+
function matrixWidth(): number {
61+
const shards = job(UI_COVERAGE.shards).strategy?.matrix?.shard;
62+
expect(Array.isArray(shards), `\`${UI_COVERAGE.shards}\` must declare a \`shard:\` matrix list`).toBe(true);
63+
const list = shards as unknown[];
64+
expect(list.length, 'a zero-wide matrix is a broken reader, not a lane').toBeGreaterThan(0);
65+
expect(list, 'the coverage matrix must be the contiguous run 1..M').toEqual(list.map((_, i) => i + 1));
66+
return list.length;
67+
}
68+
69+
function runOf(step: Step | undefined): string {
70+
return typeof step?.run === 'string' ? step.run : '';
71+
}
72+
73+
describe("the ui coverage lane says its width once (objectui#12256)", () => {
74+
const width = matrixWidth();
75+
76+
it('names each shard job with the matrix width', () => {
77+
expect(job(UI_COVERAGE.shards).name).toBe(`Test UI (coverage shard \${{ matrix.shard }}/${width})`);
78+
});
79+
80+
it('passes the matrix width to vitest, in the flag and in the step name', () => {
81+
const steps = (job(UI_COVERAGE.shards).steps ?? []).filter((s) => runOf(s).includes('test:coverage'));
82+
expect(steps.length, `no step in \`${UI_COVERAGE.shards}\` runs \`test:coverage\` any more`).toBeGreaterThan(0);
83+
for (const step of steps) {
84+
const flags = [...runOf(step).matchAll(/--shard=\$\{\{ matrix\.shard \}\}\/(\d+)/g)].map((m) => Number(m[1]));
85+
expect(flags, `${step.name}: one --shard flag, carrying the matrix width`).toEqual([width]);
86+
expect(step.name, `${step.name}: the step name states a different width`).toContain(`/${width})`);
87+
}
88+
});
89+
90+
it('declares the same width as the count the merge refuses to go below', () => {
91+
expect(job(UI_COVERAGE.report).env?.COVERAGE_SHARDS, `\`${UI_COVERAGE.report}\` must declare \`COVERAGE_SHARDS\``).toBe(
92+
String(width),
93+
);
94+
});
95+
96+
it('counts the blobs through that variable, with no literal width left in the step', () => {
97+
const count = runOf((job(UI_COVERAGE.report).steps ?? []).find((s) => s.id === 'blobs'));
98+
expect(count, `\`${UI_COVERAGE.report}\` must keep its \`blobs\` count step`).not.toBe('');
99+
expect(count).toContain('-name "blob-*-${COVERAGE_SHARDS}.json"');
100+
expect(count).toContain('-ne "$COVERAGE_SHARDS"');
101+
// A digit in the blob glob or the comparison is a second spelling of the
102+
// width, and the one that goes stale first.
103+
expect(count).not.toMatch(/blob-\*-\d/);
104+
expect(count).not.toMatch(/-ne\s+\d/);
105+
});
106+
107+
it('enforces the configured thresholds over the merge of every blob, overriding none', () => {
108+
const merge = runOf((job(UI_COVERAGE.report).steps ?? []).find((s) => s.id === 'merge'));
109+
expect(merge, `\`${UI_COVERAGE.report}\` must keep its \`merge\` step`).toContain('--merge-reports');
110+
expect(merge, 'the merge is where the thresholds are enforced; it must not override them').not.toContain(
111+
'thresholds',
112+
);
113+
const legs = (job(UI_COVERAGE.shards).steps ?? []).filter((s) => runOf(s).includes('--shard='));
114+
expect(legs.length, `no \`${UI_COVERAGE.shards}\` step passes \`--shard=\``).toBeGreaterThan(0);
115+
for (const leg of legs) {
116+
// The other half of the same contract: the legs zero the thresholds, so
117+
// the merge above is the only evaluation, over all of them.
118+
expect(runOf(leg)).toContain('--coverage.thresholds.lines=0');
119+
}
120+
});
121+
});

‎ui/scripts/__tests__/helpers/root-ci.ts‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,13 @@
1212
* the root `.github/workflows/` directory through `rootWorkflowFiles()`. It
1313
* never reads a copy under `ui/.github/`: nothing runs one.
1414
*
15-
* What the port did not carry (the coverage lane, the docs job, the shard
16-
* aggregate, the type-check-scope selector, the timings upload) has no job
17-
* here, and the pins on those retired with a disposition beside them rather
18-
* than being pointed at a job that does something else. The affected-test
19-
* selector was wired into `test-ui` later (objectstack#25868), and its pins
20-
* read that job.
15+
* What the port did not carry (the docs job, the shard aggregate, the
16+
* type-check-scope selector, the timings upload) has no job here, and the pins
17+
* on those retired with a disposition beside them rather than being pointed at
18+
* a job that does something else. The affected-test selector was wired into
19+
* `test-ui` later (objectstack#25868), and its pins read that job. The
20+
* coverage lane came back as a nightly workflow of its own
21+
* (objectstack#25924), named by `UI_COVERAGE` below.
2122
*/
2223
import fs from 'node:fs';
2324
import path from 'node:path';
@@ -32,6 +33,17 @@ export const UI_JOBS = {
3233
buildE2e: 'build-e2e-ui',
3334
} as const;
3435

36+
/**
37+
* objectui's coverage lane, ported to the root as a nightly workflow
38+
* (objectstack#25924): the file, the sharded job and the merge job that
39+
* enforces the thresholds.
40+
*/
41+
export const UI_COVERAGE = {
42+
file: 'ui-coverage-nightly.yml',
43+
shards: 'ui-coverage',
44+
report: 'ui-coverage-report',
45+
} as const;
46+
3547
/** The directory GitHub runs: `.github/workflows/` at the workspace root. */
3648
export const rootWorkflowsDir = path.join(workspaceRoot(), '.github/workflows');
3749

0 commit comments

Comments
 (0)