Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit 24c8c17

Browse files
committed
Merge origin/main (PR #12263, 1fb1ced) into claude/issue-12272-typecheck-root-dist
One conflict, content/docs/guide/ci-cd-pipeline.md, two adjacent table rows: the `type-check` row is this branch's, the `test` row is main's. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
2 parents 501d611 + 1fb1ced commit 24c8c17

18 files changed

Lines changed: 7082 additions & 79 deletions

‎.github/workflows/ci.yml‎

Lines changed: 167 additions & 57 deletions
Large diffs are not rendered by default.
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
# Regenerate scripts/test-shard-timings.json from main's full-battery runs,
2+
# weekly, and open the pull request (objectui#12208, ruling 6121816480, letter
3+
# B: "the timing file refreshed from `main`'s full-suite runs on a schedule").
4+
#
5+
# ## What the file is, and why it has a clock
6+
#
7+
# `scripts/test-shard-timings.json` is the per-file duration dataset both
8+
# `ci.yml` test matrices are split by (`scripts/test-shard-sequencer.mjs`,
9+
# wired as the root vitest config's `sequence.sequencer`). It goes stale in one
10+
# direction only: files are added and suites get slower while the committed
11+
# numbers stay put. A new file is weighed at the dataset's median, so a stale
12+
# dataset degrades to spreading files by count -- the old hash split's shape,
13+
# and visible in the aggregators' headroom readings -- but the drift is silent
14+
# unless something refreshes it. A timer is what refreshes it.
15+
#
16+
# ## Why a committed file and a pull request, not an artifact the shards fetch
17+
#
18+
# Every leg of a matrix computes the whole partition on its own and keeps its
19+
# bin, so every leg of one run must read the SAME weights. A committed file
20+
# guarantees that by construction: the legs share a checkout. Legs that each
21+
# fetched "the newest timings artifact" at their own start could straddle a
22+
# refresh, and two legs on different weights run some files twice and others
23+
# never, with every leg green. The committed file also makes a partition move a
24+
# reviewed, reproducible event -- `pnpm test --shard=3/12` on a laptop runs the
25+
# same files CI's leg 3 ran -- and the dataset is in the root config's import
26+
# closure, so the pull request that moves it runs the full suite on the new
27+
# split before it lands (`scripts/select-affected-tests.mjs`).
28+
#
29+
# ## What this workflow is not allowed to do
30+
#
31+
# ⛔ It never hand-edits a timing: every byte it commits came out of
32+
# `scripts/test-file-timings.mjs merge`.
33+
# ⛔ It never touches `ci.yml`: not the matrix widths, not a `timeout-minutes`.
34+
# The width a run's measured total implies is printed by the `Test` and
35+
# `Test (coverage)` aggregators on every run; moving a width is a reviewed
36+
# edit, not this workflow's.
37+
# ⛔ It never opens a pull request for a byte-identical file.
38+
#
39+
# ## The cost it carries, stated
40+
#
41+
# The pull request is opened with this run's `GITHUB_TOKEN`, and GitHub starts
42+
# no workflow run for an event that token raised -- the release pull request's
43+
# limitation, which objectui#5397's ruling chose to keep rather than add a
44+
# personal access token. So the refresh pull request arrives with no CI; the
45+
# seat or person who lands it pushes to its branch once (a merge of `main` is
46+
# enough), and that push runs the full suite on the new split. The branch is
47+
# this workflow's own and is overwritten by each refresh, the same posture
48+
# `changeset-release.yml` takes with its version branch.
49+
#
50+
# ## Which runs it reads
51+
#
52+
# `scripts/test-file-timings.mjs select`: the newest completed `CI` runs on
53+
# `main` from the full battery's events (`push` and the hourly `schedule`,
54+
# objectui#12214) whose twelve `Test (shard N/12)` legs all reported success
55+
# and whose twelve `test-file-timings-N` artifacts are all present; at most
56+
# six, and a file keeps the median of the runs that ran it. When no run
57+
# qualifies -- before the first full battery has uploaded, or after a week of
58+
# red -- it prints NOT MEASURED, writes nothing and exits green, because
59+
# nothing failed; a NOT MEASURED that persists is the finding.
60+
#
61+
# ## The pull-request leg
62+
#
63+
# A pull request that edits this workflow or the scripts it runs executes it
64+
# all -- the selection, the downloads, the regeneration -- and stops before
65+
# the two acts that leave the runner: the push and the pull request.
66+
67+
name: Test Shard Timings Refresh
68+
69+
on:
70+
schedule:
71+
# Monday 06:47 UTC: after a weekend of hourly full-battery runs, and on a
72+
# minute no other workflow here is scheduled on.
73+
- cron: '47 6 * * 1'
74+
workflow_dispatch:
75+
pull_request:
76+
paths:
77+
- '.github/workflows/test-shard-timings-refresh.yml'
78+
- 'scripts/test-file-timings.mjs'
79+
- 'scripts/test-shard-partition.mjs'
80+
81+
# Read-only at the top level; the one job widens to exactly what it writes.
82+
permissions:
83+
contents: read
84+
85+
# One refresh per ref at a time: the newer inputs are the better ones, so an
86+
# in-flight run yields. Scoped by ref so two pull requests' dry runs do not
87+
# cancel each other.
88+
concurrency:
89+
group: test-shard-timings-refresh-${{ github.ref }}
90+
cancel-in-progress: true
91+
92+
jobs:
93+
refresh:
94+
name: Regenerate the per-file test timings
95+
runs-on: ubuntu-latest
96+
timeout-minutes: 15
97+
permissions:
98+
contents: write # push this workflow's own branch
99+
pull-requests: write # open the refresh pull request
100+
actions: read # list runs, read their jobs, download their timing artifacts
101+
102+
steps:
103+
- name: Checkout code
104+
uses: actions/checkout@v7
105+
106+
# The selector and the generator are node builtins only: no pnpm, no
107+
# install.
108+
- name: Setup Node.js
109+
uses: actions/setup-node@v7
110+
with:
111+
node-version: '22.x'
112+
113+
# Exit 3 is NOT MEASURED, not a failure: nothing was eligible, nothing is
114+
# written, and the job says so and stays green. Any other non-zero exit
115+
# is red. `|| status=$?` because this step runs under `bash -e`.
116+
- name: Choose the runs to measure from
117+
id: select
118+
env:
119+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
120+
run: |
121+
status=0
122+
node scripts/test-file-timings.mjs select --shards 12 --limit 6 > "$RUNNER_TEMP/runs.json" || status=$?
123+
echo "status=$status" >> "$GITHUB_OUTPUT"
124+
if [ "$status" -eq 3 ]; then
125+
echo "::warning title=Test shard timings::NOT MEASURED: no completed full-battery run on main has all twelve test shards green and all twelve timing artifacts. scripts/test-shard-timings.json is untouched."
126+
exit 0
127+
fi
128+
if [ "$status" -ne 0 ]; then
129+
exit "$status"
130+
fi
131+
cat "$RUNNER_TEMP/runs.json"
132+
133+
- name: Download each run's per-file timings
134+
if: steps.select.outputs.status == '0'
135+
env:
136+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
137+
run: |
138+
mkdir -p "$RUNNER_TEMP/timings"
139+
node -e 'for (const r of JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"))) console.log(r.id)' "$RUNNER_TEMP/runs.json" > "$RUNNER_TEMP/run-ids.txt"
140+
while read -r id; do
141+
gh run download "$id" --repo "$GITHUB_REPOSITORY" --pattern 'test-file-timings-*' --dir "$RUNNER_TEMP/timings/$id"
142+
done < "$RUNNER_TEMP/run-ids.txt"
143+
144+
- name: Regenerate the dataset
145+
if: steps.select.outputs.status == '0'
146+
run: |
147+
node scripts/test-file-timings.mjs merge --runs-file "$RUNNER_TEMP/runs.json" \
148+
--out scripts/test-shard-timings.json --summary "$RUNNER_TEMP/summary.md" "$RUNNER_TEMP"/timings/*
149+
git diff --stat -- scripts/test-shard-timings.json
150+
cat "$RUNNER_TEMP/summary.md" >> "$GITHUB_STEP_SUMMARY"
151+
152+
- name: Open or update the refresh pull request
153+
if: steps.select.outputs.status == '0' && github.event_name != 'pull_request'
154+
env:
155+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
156+
BRANCH: ci/test-shard-timings-refresh
157+
run: |
158+
if git diff --quiet -- scripts/test-shard-timings.json; then
159+
echo "The regenerated dataset is byte-identical to the committed one: no pull request."
160+
exit 0
161+
fi
162+
git config user.name 'github-actions[bot]'
163+
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
164+
git switch -c "$BRANCH"
165+
git add scripts/test-shard-timings.json
166+
git commit -m 'chore(test): refresh the per-file test timings (objectui#12208)'
167+
git push --force origin "$BRANCH"
168+
{
169+
echo 'The weekly refresh of `scripts/test-shard-timings.json`, the per-file durations both `ci.yml` test matrices are split by (objectui#12208). Generated by `scripts/test-file-timings.mjs merge` from the runs below; never hand-edited.'
170+
echo
171+
cat "$RUNNER_TEMP/summary.md"
172+
echo
173+
echo '**This pull request has no CI of its own yet.** It was opened with the Actions token, and GitHub starts no workflow for an event that token raised. Push to its branch once (a merge of `main` is enough): the dataset is in the root vitest config import closure, so that push runs the full suite on the new split.'
174+
} > "$RUNNER_TEMP/body.md"
175+
open=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$BRANCH" --state open --json number --jq '.[0].number // empty')
176+
if [ -n "$open" ]; then
177+
gh pr edit "$open" --repo "$GITHUB_REPOSITORY" --body-file "$RUNNER_TEMP/body.md"
178+
echo "Updated the open refresh pull request #$open."
179+
else
180+
gh pr create --repo "$GITHUB_REPOSITORY" --base main --head "$BRANCH" \
181+
--title 'chore(test): refresh the per-file test timings (objectui#12208)' --body-file "$RUNNER_TEMP/body.md"
182+
fi

‎content/docs/guide/ci-cd-pipeline.md‎

Lines changed: 50 additions & 7 deletions
Large diffs are not rendered by default.

‎scripts/__tests__/coverage-red-cause-census.test.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,15 @@ describe('the proximate reading separates a red suite from a broken runner', ()
156156
kinds: [],
157157
});
158158
});
159+
160+
it('reads a coverage shard at any width, not only the 4-way runs it was written for (objectui#12256)', () => {
161+
const eightWay = {
162+
name: 'Test (coverage shard 7/8)',
163+
conclusion: 'failure',
164+
steps: [{ name: 'Run tests with coverage (shard 7/8)', conclusion: 'failure' }],
165+
};
166+
expect(proximateCause([eightWay])).toEqual({ failingShards: [7], kinds: ['vitest-nonzero'] });
167+
});
159168
});
160169

161170
describe('attribution requires BOTH the live window and the computed shard', () => {

‎scripts/__tests__/coverage-shard-reporter-readability.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { parse as parseYaml } from 'yaml';
99
*
1010
* ## What this guards, and why nothing else could
1111
*
12-
* `ci.yml`'s `test-coverage` job runs vitest four ways in parallel and merges
13-
* the four blob reports in the job below it, which is where the configured
12+
* `ci.yml`'s `test-coverage` job runs vitest as a sharded matrix and merges
13+
* every shard's blob report in the job below it, which is where the configured
1414
* coverage thresholds are enforced. A CLI `--reporter` **replaces** the
1515
* reporter set rather than adding to it, and vitest's resolution is:
1616
*
@@ -156,7 +156,7 @@ describe("ci.yml's coverage shards keep a red run readable", () => {
156156
offenders,
157157
'a sharded coverage leg no longer writes a blob report:\n' +
158158
offenders.map((o) => ` - ${o}`).join('\n') +
159-
'\n\nThe `coverage-report` job merges the four blobs and enforces the configured thresholds over ' +
159+
'\n\nThe `coverage-report` job merges every shard\'s blob and enforces the configured thresholds over ' +
160160
'the merged report — that is the whole reason the lane is sharded (objectui#5403). Restoring log ' +
161161
'readability by dropping the blob would trade an unreadable failure for an unevaluated coverage ' +
162162
'floor, which is the same defect wearing the other hat (objectui#9177).',
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { describe, expect, it } from 'vitest';
2+
import fs from 'node:fs';
3+
import path from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import { parse as parseYaml } from 'yaml';
6+
7+
/**
8+
* The coverage lane's width, held to ONE number (objectui#12256, folded into
9+
* objectui#12208).
10+
*
11+
* `ci.yml`'s coverage lane spells its shard count in two jobs. `test-coverage`
12+
* declares the matrix and repeats the count in its job name, its step name and
13+
* its `--shard=N/M` flag; `coverage-report` counts the blob reports the shards
14+
* wrote and refuses to merge a partial set. The blob's FILE NAME carries the
15+
* count too (`blob-N-M.json`, written by vitest from the `--shard` flag), so a
16+
* width moved in the matrix alone leaves the merge looking for blobs that no
17+
* shard writes: the count step answers zero, the merge never runs, and the
18+
* coverage gate is unevaluated on every push -- the lane's own worst failure,
19+
* one edit away. Nothing about that is visible on a pull request, because the
20+
* lane runs on `push` and `schedule` only.
21+
*
22+
* So every spelling is read here and held to the matrix:
23+
*
24+
* - the matrix is the contiguous run 1..M;
25+
* - the job name, the step name and `--shard=` all say M;
26+
* - `coverage-report` declares `COVERAGE_SHARDS: 'M'`, and its count step
27+
* reads THAT variable for both the blob glob and the expected count, with
28+
* no literal width left in the step;
29+
* - the merge step is a `--merge-reports` that overrides no threshold, so
30+
* the configured `coverage.thresholds` are what is enforced over the merge
31+
* of all M blobs -- the one place the gate runs, since the shard legs
32+
* zero the thresholds.
33+
*/
34+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
35+
const workflowSource = fs.readFileSync(path.join(repoRoot, '.github/workflows/ci.yml'), 'utf8');
36+
37+
interface Step {
38+
name?: string;
39+
run?: unknown;
40+
id?: string;
41+
}
42+
interface Job {
43+
name?: string;
44+
env?: Record<string, unknown>;
45+
strategy?: { matrix?: { shard?: unknown } };
46+
steps?: Step[];
47+
}
48+
49+
const workflow = parseYaml(workflowSource) as { jobs?: Record<string, Job> };
50+
51+
function job(key: string): Job {
52+
const found = workflow.jobs?.[key];
53+
expect(found, `ci.yml no longer defines a \`${key}\` job; this pin needs a new referent, not deletion`).toBeDefined();
54+
return found as Job;
55+
}
56+
57+
/** The matrix width, after checking the matrix is the contiguous run 1..M. */
58+
function matrixWidth(): number {
59+
const shards = job('test-coverage').strategy?.matrix?.shard;
60+
expect(Array.isArray(shards), '`test-coverage` must declare a `shard:` matrix list').toBe(true);
61+
const list = shards as unknown[];
62+
expect(list.length, 'a zero-wide matrix is a broken reader, not a lane').toBeGreaterThan(0);
63+
expect(list, 'the coverage matrix must be the contiguous run 1..M').toEqual(list.map((_, i) => i + 1));
64+
return list.length;
65+
}
66+
67+
function runOf(step: Step | undefined): string {
68+
return typeof step?.run === 'string' ? step.run : '';
69+
}
70+
71+
describe("ci.yml's coverage lane says its width once (objectui#12256)", () => {
72+
const width = matrixWidth();
73+
74+
it('names each shard job with the matrix width', () => {
75+
expect(job('test-coverage').name).toBe(`Test (coverage shard \${{ matrix.shard }}/${width})`);
76+
});
77+
78+
it('passes the matrix width to vitest, in the flag and in the step name', () => {
79+
const steps = (job('test-coverage').steps ?? []).filter((s) => runOf(s).includes('test:coverage'));
80+
expect(steps.length, 'no step in `test-coverage` runs `pnpm test:coverage` any more').toBeGreaterThan(0);
81+
for (const step of steps) {
82+
const flags = [...runOf(step).matchAll(/--shard=\$\{\{ matrix\.shard \}\}\/(\d+)/g)].map((m) => Number(m[1]));
83+
expect(flags, `${step.name}: one --shard flag, carrying the matrix width`).toEqual([width]);
84+
expect(step.name, `${step.name}: the step name states a different width`).toContain(`/${width})`);
85+
}
86+
});
87+
88+
it('declares the same width as the count the merge refuses to go below', () => {
89+
expect(job('coverage-report').env?.COVERAGE_SHARDS, '`coverage-report` must declare `COVERAGE_SHARDS`').toBe(
90+
String(width),
91+
);
92+
});
93+
94+
it('counts the blobs through that variable, with no literal width left in the step', () => {
95+
const count = runOf((job('coverage-report').steps ?? []).find((s) => s.id === 'blobs'));
96+
expect(count, '`coverage-report` must keep its `blobs` count step').not.toBe('');
97+
expect(count).toContain('-name "blob-*-${COVERAGE_SHARDS}.json"');
98+
expect(count).toContain('-ne "$COVERAGE_SHARDS"');
99+
// A digit in the blob glob or the comparison is a second spelling of the
100+
// width, and the one that goes stale first.
101+
expect(count).not.toMatch(/blob-\*-\d/);
102+
expect(count).not.toMatch(/-ne\s+\d/);
103+
});
104+
105+
it('enforces the configured thresholds over the merge of every blob, overriding none', () => {
106+
const merge = runOf((job('coverage-report').steps ?? []).find((s) => s.id === 'merge'));
107+
expect(merge, '`coverage-report` must keep its `merge` step').toContain('--merge-reports');
108+
expect(merge, 'the merge is where the thresholds are enforced; it must not override them').not.toContain(
109+
'thresholds',
110+
);
111+
const legs = (job('test-coverage').steps ?? []).filter((s) => runOf(s).includes('--shard='));
112+
for (const leg of legs) {
113+
// The other half of the same contract: the legs zero the thresholds, so
114+
// the merge above is the only evaluation, over all of them.
115+
expect(runOf(leg)).toContain('--coverage.thresholds.lines=0');
116+
}
117+
});
118+
});

0 commit comments

Comments
 (0)