refactor(ci): split workspaces-a into two Vitest native shards
Split the slow workspaces-a CI lane into two Vitest native shards and keep release verification in parity. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
6601014898
commit
d648becb90
|
|
@ -174,8 +174,20 @@ jobs:
|
|||
group_label: server (5/5)
|
||||
shard_index: 4
|
||||
shard_count: 5
|
||||
# workspaces-a was the slowest check in the fully-green PR run
|
||||
# 31371439296 (2026-08-10) at 319s, with the ui project's single
|
||||
# vitest invocation accounting for ~224s and the paperclipai CLI
|
||||
# ~37s. Two shards use Vitest's native --shard on each project's
|
||||
# file list (ui: 439 files, cli: 54), bringing each job to roughly
|
||||
# half the suite time (~130s + setup) without a duration manifest.
|
||||
- group: general-workspaces-a
|
||||
group_label: workspaces-a
|
||||
group_label: workspaces-a (1/2)
|
||||
shard_index: 0
|
||||
shard_count: 2
|
||||
- group: general-workspaces-a
|
||||
group_label: workspaces-a (2/2)
|
||||
shard_index: 1
|
||||
shard_count: 2
|
||||
- group: general-workspaces-b
|
||||
group_label: workspaces-b
|
||||
|
||||
|
|
|
|||
|
|
@ -64,8 +64,16 @@ jobs:
|
|||
group_label: server (3/3)
|
||||
shard_index: 2
|
||||
shard_count: 3
|
||||
# Keep parity with pr.yml: workspaces-a is split with Vitest's
|
||||
# native --shard because the ui project dominates the lane.
|
||||
- group: general-workspaces-a
|
||||
group_label: workspaces-a
|
||||
group_label: workspaces-a (1/2)
|
||||
shard_index: 0
|
||||
shard_count: 2
|
||||
- group: general-workspaces-a
|
||||
group_label: workspaces-a (2/2)
|
||||
shard_index: 1
|
||||
shard_count: 2
|
||||
- group: general-workspaces-b
|
||||
group_label: workspaces-b
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,15 @@ test("release verify workflow covers the same split test surface as stable PR ve
|
|||
assert.match(verifyWorkflow, new RegExp(`shard_index: ${shardIndex}[\\s\\S]*?shard_count: 5`));
|
||||
}
|
||||
|
||||
// workspaces-a splits with Vitest native --shard in pr.yml; release
|
||||
// verification must keep the same two-shard coverage.
|
||||
for (const shardIndex of [0, 1]) {
|
||||
assert.match(
|
||||
verifyWorkflow,
|
||||
new RegExp(`group: general-workspaces-a[\\s\\S]*?shard_index: ${shardIndex}\\n\\s+shard_count: 2`),
|
||||
);
|
||||
}
|
||||
|
||||
assert.match(verifyWorkflow, /pnpm test:run:general -- --group/);
|
||||
assert.match(verifyWorkflow, /pnpm test:run:serialized -- --shard-index/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -77,9 +77,37 @@ test("a route/authz suite never leaks into the general-server shards", () => {
|
|||
}
|
||||
});
|
||||
|
||||
test("shard flags are rejected for the parallel workspace groups", () => {
|
||||
const result = dryRun(["--mode", "general", "--group", "general-workspaces-a", "--shard-index", "0", "--shard-count", "3"]);
|
||||
assert.notEqual(result.status, 0, "workspace groups must not accept shard flags");
|
||||
test("shard flags are rejected for the workspaces-b group", () => {
|
||||
const result = dryRun(["--mode", "general", "--group", "general-workspaces-b", "--shard-index", "0", "--shard-count", "3"]);
|
||||
assert.notEqual(result.status, 0, "workspaces-b must not accept shard flags");
|
||||
});
|
||||
|
||||
test("workspaces-a shards map to Vitest native --shard slices over a stable project list", () => {
|
||||
const shards = [0, 1].map((index) =>
|
||||
dryRunJson([
|
||||
"--mode", "general", "--group", "general-workspaces-a",
|
||||
"--shard-index", String(index), "--shard-count", "2",
|
||||
]),
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
shards.map((shard) => shard.workspacesVitestShard),
|
||||
["1/2", "2/2"],
|
||||
"each matrix job must pass its own --shard slice to vitest",
|
||||
);
|
||||
// Vitest's --shard partitions each project's file list deterministically, so
|
||||
// an identical project list across jobs is what guarantees complete,
|
||||
// non-overlapping coverage of the lane.
|
||||
assert.deepEqual(shards[0].workspaceProjects, shards[1].workspaceProjects);
|
||||
assert.ok(shards[0].workspaceProjects.length > 0, "workspaces-a must run at least one project");
|
||||
|
||||
const unsharded = dryRunJson(["--mode", "general", "--group", "general-workspaces-a"]);
|
||||
assert.deepEqual(
|
||||
unsharded.workspaceProjects,
|
||||
shards[0].workspaceProjects,
|
||||
"sharding must not change which projects the lane covers",
|
||||
);
|
||||
assert.equal(unsharded.workspacesVitestShard, null);
|
||||
});
|
||||
|
||||
test("duration-aware partition balances skewed weights better than round-robin", () => {
|
||||
|
|
|
|||
|
|
@ -208,10 +208,11 @@ function parseCliOptions(argv) {
|
|||
|
||||
const shardAllowed =
|
||||
mode === serializedModeName ||
|
||||
(mode === generalModeName && group === generalServerGroupName);
|
||||
(mode === generalModeName &&
|
||||
(group === generalServerGroupName || group === generalWorkspacesAGroupName));
|
||||
if (!shardAllowed && shardIndex !== null) {
|
||||
fail(
|
||||
"--shard-index/--shard-count are only valid with --mode serialized or --mode general --group general-server.",
|
||||
"--shard-index/--shard-count are only valid with --mode serialized, --mode general --group general-server, or --mode general --group general-workspaces-a.",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -287,9 +288,16 @@ function runGeneralSuites(routeTests) {
|
|||
}
|
||||
}
|
||||
|
||||
function runProjectGroup(projects, groupName) {
|
||||
function runProjectGroup(projects, groupName, shardIndex = null, shardCount = null) {
|
||||
// With shard args, lean on Vitest's native --shard: each matrix job runs the
|
||||
// same per-project invocations but only its slice of each project's test
|
||||
// files. Vitest's sharding is deterministic for an identical file list, so
|
||||
// the matrix jobs form a complete, non-overlapping cover of every project.
|
||||
const shardArgs =
|
||||
shardCount !== null && shardCount > 1 ? [`--shard=${shardIndex + 1}/${shardCount}`] : [];
|
||||
const shardSuffix = shardArgs.length > 0 ? ` shard ${shardIndex + 1}/${shardCount}` : "";
|
||||
for (const project of projects) {
|
||||
runVitest(["--project", project], `${groupName} project ${project}`);
|
||||
runVitest(["--project", project, ...shardArgs], `${groupName} project ${project}${shardSuffix}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -335,7 +343,11 @@ function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount =
|
|||
}
|
||||
|
||||
if (groupName === generalWorkspacesAGroupName) {
|
||||
runProjectGroup(generalWorkspacesAProjects, groupName);
|
||||
// The ui project dominates this lane (~224s of a 319s job in actions run
|
||||
// 31371439296, 2026-08-10, where workspaces-a was the slowest PR check).
|
||||
// Its 439 test files shard cleanly with Vitest's native --shard, so the
|
||||
// lane splits across runners without a duration manifest.
|
||||
runProjectGroup(generalWorkspacesAProjects, groupName, shardIndex, shardCount);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -416,6 +428,18 @@ if (options.dryRun) {
|
|||
generalServerShardDurations,
|
||||
)
|
||||
: null,
|
||||
workspaceProjects:
|
||||
options.group === generalWorkspacesAGroupName
|
||||
? generalWorkspacesAProjects
|
||||
: options.group === generalWorkspacesBGroupName
|
||||
? generalWorkspacesBProjects
|
||||
: null,
|
||||
workspacesVitestShard:
|
||||
options.group === generalWorkspacesAGroupName &&
|
||||
options.shardCount !== null &&
|
||||
options.shardCount > 1
|
||||
? `${options.shardIndex + 1}/${options.shardCount}`
|
||||
: null,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
|
|
|||
Loading…
Reference in New Issue