perf(release-green): run the 4 slow suites concurrently — pre-flight wall time ~sum -> ~slowest (#6319)

The pre-flight ran unit (~25-35min) + vitest (~3-8min) + integration (~3-10min)
+ pack-artifact (~15min) SEQUENTIALLY via execFileSync — ~1h wall in the
v3.8.45 release (the dominant cost of Phase 0). They are independent processes
with per-process DATA_DIR isolation, so they now run in a single Promise.all
over an async runAsync() twin of run(): total drops to ~the slowest single
suite (~30min). Each still saves its per-gate log (_artifacts/release-green/)
and reports HARD independently. --quick still skips them; --with-build folds
pack-artifact into the same wave (or runs it alone under --quick).
Guard: tests/unit/validate-release-green.test.ts (13/13).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-06 08:25:48 -03:00
committed by GitHub
parent ab8b41bdaa
commit 5d07bdd76f
2 changed files with 104 additions and 28 deletions

View File

@@ -45,7 +45,8 @@
// Per-gate output is saved to _artifacts/release-green/<gate>.log (gitignored) —
// diagnose a red from the file instead of re-running the gate.
import { execFileSync } from "node:child_process";
import { execFile, execFileSync } from "node:child_process";
import { promisify } from "node:util";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -189,7 +190,29 @@ function run(cmd, cmdArgs, opts = {}) {
}
}
function main() {
const execFileAsync = promisify(execFile);
// Async twin of run() — same {code, out} contract, so the slow suites (unit /
// vitest / integration / pack-artifact) can run CONCURRENTLY instead of in
// series. Sequentially they dominate the pre-flight wall time (~2h in the
// v3.8.45 run); they are independent processes with per-process DATA_DIR
// isolation, so overlapping them cuts the pre-flight to ~the slowest single one.
async function runAsync(cmd, cmdArgs, opts = {}) {
try {
const { stdout, stderr } = await execFileAsync(cmd, cmdArgs, {
cwd: ROOT,
encoding: "utf8",
maxBuffer: 256 * 1024 * 1024,
env: buildGateEnv(opts.env),
...(opts.timeout ? { timeout: opts.timeout } : {}),
});
return { code: 0, out: `${stdout || ""}${stderr || ""}` };
} catch (err) {
return classifyRunError(err, opts.timeout);
}
}
async function main() {
const args = new Set(process.argv.slice(2));
const JSON_OUT = args.has("--json");
const WITH_BUILD = args.has("--with-build");
@@ -386,35 +409,69 @@ function main() {
// with 15 such reds). They run SILENTLY for many minutes; the announce line above + these
// hard ceilings keep a long-but-healthy run from being mistaken for a hang (the ceiling also
// converts a genuine DB-handle hang into a visible failure instead of an infinite block).
hardCmd(
"unit",
"Unit tests (full suite, CI concurrency — runs ~20-35min silently)",
npmCmd,
["run", "test:unit:ci"],
{ timeout: 45 * 60 * 1000 }
// The slow suites are INDEPENDENT processes (each self-isolates DATA_DIR) with
// no shared state, so they run CONCURRENTLY — the pre-flight wall time becomes
// ~the slowest single suite instead of their sum (unit ~25-35min + vitest
// ~3-8min + integration ~3-10min + pack-artifact ~15min was ~1h serial in the
// v3.8.45 run). pack-artifact (--with-build) joins the same wave. Integration
// runs ONLY on the release PR full CI, so a regression here is invisible until
// release — that is why it is a HARD pre-flight gate.
const slow = [
{
id: "unit",
label: "Unit tests (full suite, CI concurrency — runs ~20-35min silently)",
args: ["run", "test:unit:ci"],
timeout: 45 * 60 * 1000,
},
{
id: "vitest",
label: "Vitest (MCP / autoCombo / cache — ~3-8min)",
args: ["run", "test:vitest"],
timeout: 15 * 60 * 1000,
},
{
id: "integration",
label: "Integration tests (~3-10min)",
args: ["run", "test:integration"],
timeout: 20 * 60 * 1000,
},
];
if (WITH_BUILD) {
slow.push({
id: "pack-artifact",
label: "Package artifact (npm pack policy)",
args: ["run", "check:pack-artifact"],
timeout: 20 * 60 * 1000,
});
}
slow.forEach((g) => announce(`${g.label} [parallel]`));
const slowResults = await Promise.all(
slow.map((g) => runAsync(npmCmd, g.args, { timeout: g.timeout }))
);
hardCmd(
"vitest",
"Vitest (MCP / autoCombo / cache — ~3-8min)",
npmCmd,
["run", "test:vitest"],
{ timeout: 15 * 60 * 1000 }
);
// Integration tests run ONLY on the release PR full CI (PR→main), so an assertion
// regression here (e.g. a contributor flipping a Codex fingerprint key order) is
// invisible until release — run them in the pre-flight as a HARD gate.
hardCmd("integration", "Integration tests (~3-10min)", npmCmd, ["run", "test:integration"], {
slow.forEach((g, i) => {
const { code, out } = slowResults[i];
saveGateLog(g.id, out);
record({
id: g.id,
label: g.label,
kind: "hard",
ok: code === 0,
detail: code === 0 ? "pass" : firstFailureLine(out),
});
});
} else if (WITH_BUILD) {
// --with-build without the suites (--quick): still verify the package artifact.
const { code, out } = await runAsync(npmCmd, ["run", "check:pack-artifact"], {
timeout: 20 * 60 * 1000,
});
}
if (WITH_BUILD) {
hardCmd(
"pack-artifact",
"Package artifact (npm pack policy)",
npmCmd,
["run", "check:pack-artifact"],
{ timeout: 20 * 60 * 1000 }
);
saveGateLog("pack-artifact", out);
record({
id: "pack-artifact",
label: "Package artifact (npm pack policy)",
kind: "hard",
ok: code === 0,
detail: code === 0 ? "pass" : firstFailureLine(out),
});
}
const { releaseGreen, hardFailures, drift } = computeVerdict(results);

View File

@@ -158,3 +158,22 @@ test("pre-flight --hermetic scrubs the live-test trigger vars (2026-07-05 false-
assert.match(src, /saveGateLog/, "per-gate output must be persisted");
assert.match(src, /_artifacts[/", ]+release-green/, "logs must land in _artifacts/release-green");
});
test("pre-flight runs the slow suites CONCURRENTLY (v3.8.45 perf — was ~1h serial)", async () => {
const fs = await import("node:fs");
const src = fs.readFileSync(
new URL("../../scripts/quality/validate-release-green.mjs", import.meta.url),
"utf8"
);
// main() must be async and the slow suites (unit/vitest/integration/pack-artifact)
// must run via a single Promise.all over runAsync — not four sequential hardCmd calls.
assert.match(src, /async function main\(\)/, "main must be async to await the parallel wave");
assert.match(src, /const execFileAsync = promisify\(execFile\)/, "async runner must exist");
assert.match(src, /await Promise\.all\(\s*slow\.map\(/, "slow suites must run concurrently");
// The four slow-gate ids must all be present in the parallel wave.
for (const id of ["unit", "vitest", "integration", "pack-artifact"]) {
assert.ok(src.includes(`id: "${id}"`), `slow gate ${id} must be in the parallel wave`);
}
// Each still saves its per-gate log for red diagnosis without a re-run.
assert.match(src, /slow\.forEach\([\s\S]*?saveGateLog\(g\.id/, "each slow gate persists its log");
});