ci: run the db project as its own workspaces-c lane

workspaces-b became the slowest PR check (499-593s) once the server shards
were rebalanced. The @paperclipai/db project alone was 168-250s of its
436-513s of vitest time (PR runs 38084171761, 38083084276 and 38083020834,
2026-10-10). Vitest's native --shard would not level it: the project is
already file-parallel and client.test.ts is 117-189s of its wall time, so
a hash-ordered slice can put both slow suites on one runner.

The db project now runs as general-workspaces-c in both pr-trusted.yml and
release-verify.yml; workspaces-b keeps the other sixteen projects at
~265-275s of vitest time. Local `all` mode still covers every project, and
the guard tests assert that the three workspace lanes cover the root Vitest
project list exactly once and that both workflows carry the new lane.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Bender (Fable) committed 2026-10-10 20:58:45 +00:00
1 parent 9ba5569030
commit 88433c1be7
5 files changed
+65 -9

No files matched your search

+8
View File
@@ -585,8 +585,16 @@ jobs:
group_label: workspaces-a (2/2)
shard_index: 1
shard_count: 2
# workspaces-b became the slowest check (499-593s) once the server
# shards were rebalanced: the db project alone was 168-250s of its
# 436-513s of vitest time (PR runs 38084171761, 38083084276 and
# 38083020834, 2026-10-10). The db project now runs as its own
# workspaces-c lane; the rest of workspaces-b is ~265-275s of
# vitest time, so both jobs land near 300-350s with setup.
- group: general-workspaces-b
group_label: workspaces-b
- group: general-workspaces-c
group_label: workspaces-c
steps:
- name: Checkout repository
+3
View File
@@ -174,6 +174,9 @@ jobs:
shard_count: 2
- group: general-workspaces-b
group_label: workspaces-b
# Keep parity with pr.yml: the db project runs as its own lane.
- group: general-workspaces-c
group_label: workspaces-c
steps:
- name: Checkout repository
@@ -503,7 +503,7 @@ test("release verify workflow covers the same split test surface as stable PR ve
assert.match(buildJob, /persist-credentials: false/);
assert.doesNotMatch(buildJob, /cache: pnpm/);
for (const group of ["general-server-without-chat-or-native-runner", "general-chat", "general-workspaces-a", "general-workspaces-b"]) {
for (const group of ["general-server-without-chat-or-native-runner", "general-chat", "general-workspaces-a", "general-workspaces-b", "general-workspaces-c"]) {
assert.match(verifyWorkflow, new RegExp(`group: ${group}`));
}
for (const [group, count] of [["general-server-without-chat-or-native-runner", 10], ["general-chat", 3]]) {
@@ -107,10 +107,12 @@ test("general shards retain scripts and route-named suites outside the serialize
]) assert.ok(general.selectedGeneralServerSuites.includes(file), `missing configured server suite: ${file}`);
});
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");
});
for (const group of ["general-workspaces-b", "general-workspaces-c"]) {
test(`shard flags are rejected for the ${group} group`, () => {
const result = dryRun(["--mode", "general", "--group", group, "--shard-index", "0", "--shard-count", "3"]);
assert.notEqual(result.status, 0, `${group} must not accept shard flags`);
});
}
test("workspace lanes cover every non-server project in the root Vitest configuration", () => {
const config = readFileSync(path.join(repoRoot, "vitest.config.ts"), "utf8");
@@ -118,11 +120,29 @@ test("workspace lanes cover every non-server project in the root Vitest configur
assert.ok(roots.includes("server"), "expected the explicit root Vitest project list");
const expected = roots.filter(root => root !== "server")
.map(root => JSON.parse(readFileSync(path.join(repoRoot, root, "package.json"), "utf8")).name).sort();
const actual = ["general-workspaces-a", "general-workspaces-b"]
const actual = ["general-workspaces-a", "general-workspaces-b", "general-workspaces-c"]
.flatMap(group => dryRunJson(["--mode", "general", "--group", group]).workspaceProjects).sort();
assert.deepEqual(actual, expected, "no configured project may be silently omitted or run twice");
});
test("workspaces-c runs the db project alone and workspaces-b no longer carries it", () => {
// The db project was 168-250s of the 436-513s workspaces-b vitest time
// (PR runs 38084171761, 38083084276 and 38083020834, 2026-10-10), so it
// runs in its own lane. The PR and release matrices both carry that lane.
const workspacesC = dryRunJson(["--mode", "general", "--group", "general-workspaces-c"]);
assert.deepEqual(workspacesC.workspaceProjects, ["@paperclipai/db"]);
assert.equal(workspacesC.workspacesVitestShard, null);
const workspacesB = dryRunJson(["--mode", "general", "--group", "general-workspaces-b"]);
assert.ok(!workspacesB.workspaceProjects.includes("@paperclipai/db"), "db must not run twice");
assert.ok(workspacesB.workspaceProjects.length > 0, "workspaces-b must still run its other projects");
for (const workflow of ["pr-trusted.yml", "release-verify.yml"]) {
const text = readFileSync(path.join(repoRoot, ".github", "workflows", workflow), "utf8");
for (const group of ["general-workspaces-b", "general-workspaces-c"]) {
assert.match(text, new RegExp(`- group: ${group}\\n\\s+group_label: [^\\n]+\\n(?!\\s+shard_)`), `${workflow} must run ${group} unsharded`);
}
}
});
test("workspaces-a shards map to Vitest native --shard slices over a stable project list", () => {
const shards = [0, 1].map((index) =>
dryRunJson([
+28 -3
View File
@@ -117,9 +117,27 @@ function excludedServerSuites(groupName) {
}
const generalWorkspacesAGroupName = "general-workspaces-a";
const generalWorkspacesBGroupName = "general-workspaces-b";
const generalWorkspacesCGroupName = "general-workspaces-c";
const generalWorkspacesAProjects = ["@paperclipai/ui", "paperclipai"];
const generalWorkspacesBProjects = nonServerProjects.filter((project) => !generalWorkspacesAProjects.includes(project));
const generalGroupNames = [generalServerGroupName, generalWorkspacesAGroupName, generalWorkspacesBGroupName];
// The db project runs in its own lane. Its vitest invocation took 168-250s
// of a 436-513s workspaces-b job (PR runs 38084171761, 38083084276 and
// 38083020834, 2026-10-10), which made workspaces-b the slowest check once
// the server shards were rebalanced. Vitest's native --shard would not help
// here: the project is already file-parallel and client.test.ts alone is
// 117-189s of its wall time, so a hash-ordered slice can land both of its
// slow suites on one runner. Splitting by project is deterministic and
// leaves the remaining projects at ~265-275s of vitest time, roughly level
// with this lane.
const generalWorkspacesCProjects = ["@paperclipai/db"];
const generalWorkspacesBProjects = nonServerProjects.filter(
(project) => !generalWorkspacesAProjects.includes(project) && !generalWorkspacesCProjects.includes(project),
);
const generalGroupNames = [
generalServerGroupName,
generalWorkspacesAGroupName,
generalWorkspacesBGroupName,
generalWorkspacesCGroupName,
];
const allowedGeneralGroupNames = [
...generalGroupNames,
generalServerWithoutChatGroupName,
@@ -520,6 +538,11 @@ function runGeneralGroup(routeTests, groupName, shardIndex = null, shardCount =
return;
}
if (groupName === generalWorkspacesCGroupName) {
runProjectGroup(generalWorkspacesCProjects, groupName);
return;
}
fail(`Unknown group "${groupName}".`);
}
@@ -604,7 +627,9 @@ if (options.dryRun) {
? generalWorkspacesAProjects
: options.group === generalWorkspacesBGroupName
? generalWorkspacesBProjects
: null,
: options.group === generalWorkspacesCGroupName
? generalWorkspacesCProjects
: null,
workspacesVitestShard:
options.group === generalWorkspacesAGroupName &&
options.shardCount !== null &&