perf(ci): balance node test shards and gate heavy CI lanes by changed scope (#108091)

This commit is contained in:
Peter Steinberger
2026-07-15 01:16:47 -07:00
committed by GitHub
parent 9b6361a50c
commit 57761ebe8c
10 changed files with 849 additions and 244 deletions

View File

@@ -540,8 +540,22 @@ jobs:
console.warn(`Changed Node test planning failed; using compact full suite: ${error}`);
}
}
// Targeted (narrow-diff) PRs skip the heavy packaging lanes unless the
// diff touches surfaces those lanes exist to prove: test-only diffs
// cannot change dist bytes, and QA smoke re-proves packaging/scenario
// surfaces that unit shards plus build smokes already cover elsewhere.
const changedScopeHasBuildImpact =
changedNodeTestShards === null ||
typeof changedNodeTestPlan.hasBuildArtifactAffectingChange !== "function" ||
changedNodeTestPlan.hasBuildArtifactAffectingChange(changedPaths);
const changedScopeHasQaImpact =
changedNodeTestShards === null ||
typeof changedNodeTestPlan.hasQaSmokeAffectingChange !== "function" ||
changedNodeTestPlan.hasQaSmokeAffectingChange(changedPaths);
const runBuildArtifacts = runNodeFull && changedScopeHasBuildImpact;
const runQaSmokeCi =
runNodeFull &&
changedScopeHasQaImpact &&
(!frozenTarget || existsSync("extensions/qa-lab/src/ci-smoke-plan.ts"));
const nodeTestShards = runNodeFull
? (changedNodeTestShards
@@ -571,8 +585,12 @@ jobs:
const nodeTestNonDistShards = nodeTestShards.filter((shard) => !shard.requires_dist);
const nodeTestDistShards = nodeTestShards.filter((shard) => shard.requires_dist);
// Targeted jobs cannot discover repository-scanning boundary tests
// through imports, so preserve the existing full boundary gate.
const runNodeCoreDist = changedNodeTestShards !== null || nodeTestDistShards.length > 0;
// through imports. Keep the full boundary gate inside build-artifacts
// when that lane runs; test-only targeted plans carry their own
// nondist changed-boundary shard instead.
const runNodeCoreDist =
(changedNodeTestShards !== null && runBuildArtifacts) ||
nodeTestDistShards.length > 0;
const channelContractShards = runNodeFull ? createChannelContractTestShards() : [];
const protocolCoverageRequested = runNode || runIosBuild || runAndroid;
const runProtocolEventCoverage =
@@ -587,7 +605,7 @@ jobs:
run_android: runAndroid,
run_skills_python: runSkillsPython,
run_windows: runWindows,
run_build_artifacts: runNodeFull,
run_build_artifacts: runBuildArtifacts,
run_checks_fast_core: checksFastCoreTasks.length > 0,
run_checks_fast: runNodeFull,
historical_target: historicalTarget,
@@ -1297,10 +1315,10 @@ jobs:
;;
contracts-plugins-ci-routing)
pnpm test:contracts:plugins
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-changed-node-test-plan.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-changed-node-test-plan.test.ts test/scripts/ci-run-node-test-shard.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
;;
ci-routing)
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-changed-node-test-plan.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
pnpm test src/commands/status.scan-result.test.ts src/scripts/ci-changed-scope.test.ts test/scripts/changed-lanes.test.ts test/scripts/ci-changed-node-test-plan.test.ts test/scripts/ci-run-node-test-shard.test.ts test/scripts/ci-workflow-guards.test.ts test/scripts/run-vitest.test.ts test/scripts/test-projects.test.ts
;;
max-lines-ratchet)
if ! has_package_script "check:max-lines-ratchet"; then
@@ -1682,83 +1700,8 @@ jobs:
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000"
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1"
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
shell: bash
run: |
set -euo pipefail
node --input-type=module <<'EOF'
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
import { join } from "node:path";
const targets = JSON.parse(process.env.OPENCLAW_NODE_TEST_TARGETS_JSON ?? "null");
if (Array.isArray(targets) && targets.length > 0) {
// One file per process preserves the isolation boundaries encoded by
// full-suite include-pattern shards while keeping one runner job.
for (const target of targets) {
const result = spawnSync(
"pnpm",
["exec", "node", "scripts/test-projects.mjs", target],
{ env: process.env, stdio: "inherit" },
);
if (result.status !== 0) {
process.exit(result.status ?? 1);
}
}
process.exit(0);
}
const groups = JSON.parse(process.env.OPENCLAW_NODE_TEST_GROUPS_JSON ?? "null");
const plans = Array.isArray(groups) && groups.length > 0
? groups
: [{
configs: JSON.parse(process.env.OPENCLAW_NODE_TEST_CONFIGS_JSON ?? "[]"),
env: JSON.parse(process.env.OPENCLAW_NODE_TEST_ENV_JSON ?? "null"),
includePatterns: JSON.parse(
process.env.OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON ?? "null",
),
shard_name: process.env.OPENCLAW_VITEST_SHARD_NAME,
}];
for (const plan of plans) {
const configs = plan.configs;
if (!Array.isArray(configs) || configs.length === 0) {
console.error("Missing node test shard configs");
process.exit(1);
}
const childEnv = {
...process.env,
...(plan.shard_name ? { OPENCLAW_VITEST_SHARD_NAME: plan.shard_name } : {}),
};
if (plan.env && typeof plan.env === "object" && !Array.isArray(plan.env)) {
for (const [key, value] of Object.entries(plan.env)) {
if (typeof value === "string") {
childEnv[key] = value;
}
}
}
if (Array.isArray(plan.includePatterns) && plan.includePatterns.length > 0) {
const includeFile = join(
process.env.RUNNER_TEMP ?? ".",
`node-test-include-${process.env.GITHUB_JOB ?? "local"}-${Date.now()}.json`,
);
writeFileSync(includeFile, JSON.stringify(plan.includePatterns), "utf8");
childEnv.OPENCLAW_VITEST_INCLUDE_FILE = includeFile;
} else {
delete childEnv.OPENCLAW_VITEST_INCLUDE_FILE;
}
const result = spawnSync(
"pnpm",
["exec", "node", "scripts/test-projects.mjs", ...configs],
{
env: childEnv,
stdio: "inherit",
},
);
if ((result.status ?? 1) !== 0) {
process.exit(result.status ?? 1);
}
}
EOF
run: node scripts/ci-run-node-test-shard.mjs
# Types, lint, and format check shards.
check-shard:
@@ -1794,7 +1737,9 @@ jobs:
runner: blacksmith-4vcpu-ubuntu-2404
- check_name: check-test-types
task: test-types
runner: blacksmith-4vcpu-ubuntu-2404
# Slowest static lane (~3.5min on 4 vCPU); extra cores shorten the
# PR critical path for roughly the same billed core-minutes.
runner: blacksmith-16vcpu-ubuntu-2404
steps:
- *linux_node_checkout_step
- name: Setup Node environment
@@ -1904,7 +1849,9 @@ jobs:
- check_name: check-additional-boundaries-a
group: boundaries
boundary_shard: 1/4
runner: blacksmith-8vcpu-ubuntu-2404
# This shard runs ~4min on 8 vCPU and regularly owns the PR wall
# clock; extra cores shorten it for similar billed core-minutes.
runner: blacksmith-16vcpu-ubuntu-2404
- check_name: check-additional-boundaries-bcd
group: boundaries
boundary_shard: 2/4,3/4,4/4