Compare commits

..

2 Commits

Author SHA1 Message Date
diegosouzapw
6d7c221aa0 chore(changelog): record release-green sequencing fix 2026-08-08 23:30:12 -03:00
diegosouzapw
965f85ef61 fix(ci): sequence tarball boot after package build 2026-08-08 23:29:50 -03:00
3 changed files with 88 additions and 27 deletions

View File

@@ -0,0 +1 @@
- Sequence the release-green tarball boot smoke after its package artifact build so clean-worktree validation cannot race a missing `dist/server.js`.

View File

@@ -96,9 +96,7 @@ export function firstFailureLine(out) {
.split("\n")
.map((l) => l.trim())
.filter(Boolean);
const hit = lines.find((l) =>
/||not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l)
);
const hit = lines.find((l) => /||not ok|AssertionError|error TS|FAIL|Error:|REGRESS/i.test(l));
return (hit || lines[lines.length - 1] || "failed").slice(0, 200);
}
@@ -595,14 +593,6 @@ async function main() {
args: ["run", "check:pack-artifact"],
timeout: 20 * 60 * 1000,
});
// WS1.2 (#7065 class): boot the REAL packed tarball from a clean install —
// the runtime gate structure checks cannot provide. Reuses the same dist/ build.
slow.push({
id: "pack-boot",
label: "Tarball boot-smoke (installed CLI serves /health)",
args: ["run", "check:pack-boot"],
timeout: 15 * 60 * 1000,
});
}
slow.forEach((g) => announce(`${g.label} [parallel]`));
const slowResults = await Promise.all(
@@ -619,6 +609,41 @@ async function main() {
detail: code === 0 ? "pass" : firstFailureLine(out),
});
});
if (WITH_BUILD) {
// WS1.2 (#7065 class): boot the REAL packed tarball from a clean install.
// check:pack-artifact is the builder for dist/ when staging is absent, so the
// boot smoke MUST run after it completes. Running both in the parallel wave
// races check:pack-boot against dist/server.js creation on clean worktrees.
const packArtifactIndex = slow.findIndex((g) => g.id === "pack-artifact");
const packArtifactResult = slowResults[packArtifactIndex];
const bootLabel = "Tarball boot-smoke (installed CLI serves /health)";
if (!packArtifactResult || packArtifactResult.code !== 0) {
const out = "skipped because package-artifact did not produce a valid dist/ build";
saveGateLog("pack-boot", out);
record({
id: "pack-boot",
label: bootLabel,
kind: "hard",
ok: false,
detail: out,
});
} else {
announce(bootLabel);
const { code, out } = await runAsync(npmCmd, ["run", "check:pack-boot"], {
timeout: 15 * 60 * 1000,
});
saveGateLog("pack-boot", out);
record({
id: "pack-boot",
label: bootLabel,
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"], {

View File

@@ -22,16 +22,12 @@ const extract = extractCiGates as (
) => { id: string; job: string; args: string[]; env?: Record<string, string> }[];
test("eslintCounts sums errors + warnings across files", () => {
const parsed = [
{ errorCount: 2, warningCount: 5 },
{ errorCount: 0, warningCount: 3 },
{},
];
const parsed = [{ errorCount: 2, warningCount: 5 }, { errorCount: 0, warningCount: 3 }, {}];
assert.deepEqual(eslintCounts(parsed), { errors: 2, warnings: 8 });
});
test("parseEslintJson tolerates a leading non-JSON banner", () => {
const out = "npm warn something\n[{\"errorCount\":0,\"warningCount\":1}]";
const out = 'npm warn something\n[{"errorCount":0,"warningCount":1}]';
assert.deepEqual(parseEslintJson(out), [{ errorCount: 0, warningCount: 1 }]);
assert.equal(parseEslintJson("no json here"), null);
});
@@ -52,8 +48,14 @@ test("parseEslintJson tolerates ESLint's trailing unpruned-suppressions stderr s
});
test("parseCognitiveCount reads the gate's count (en + pt)", () => {
assert.equal(parseCognitiveCount("[cognitive-complexity] 797 function(s) exceed the threshold (15)."), 797);
assert.equal(parseCognitiveCount("[cognitive-complexity] REGRESSÃO — 801 violações > baseline 797"), 801);
assert.equal(
parseCognitiveCount("[cognitive-complexity] 797 function(s) exceed the threshold (15)."),
797
);
assert.equal(
parseCognitiveCount("[cognitive-complexity] REGRESSÃO — 801 violações > baseline 797"),
801
);
assert.equal(parseCognitiveCount("no number"), null);
});
@@ -175,8 +177,16 @@ test("pre-flight wires the test-masking PR-context gate against origin/main (v3.
);
// run() must honor a per-gate env override so GITHUB_BASE_REF actually reaches the child
// (routed through buildGateEnv since the --hermetic scrub was added).
assert.match(src, /env:\s*buildGateEnv\(opts\.env\)/, "run() must merge opts.env into the child env");
assert.match(src, /\.\.\.\(extra \|\| \{\}\)/, "buildGateEnv must spread the per-gate env override");
assert.match(
src,
/env:\s*buildGateEnv\(opts\.env\)/,
"run() must merge opts.env into the child env"
);
assert.match(
src,
/\.\.\.\(extra \|\| \{\}\)/,
"buildGateEnv must spread the per-gate env override"
);
});
test("pre-flight --hermetic scrubs the live-test trigger vars (2026-07-05 false-positive fix)", async () => {
@@ -214,6 +224,27 @@ test("pre-flight runs the slow suites CONCURRENTLY (v3.8.45 perf — was ~1h ser
assert.match(src, /slow\.forEach\([\s\S]*?saveGateLog\(g\.id/, "each slow gate persists its log");
});
test("pre-flight runs tarball boot only after the package artifact builder completes", async () => {
const fs = await import("node:fs");
const src = fs.readFileSync(
new URL("../../scripts/quality/validate-release-green.mjs", import.meta.url),
"utf8"
);
const parallelWave = src.indexOf("const slowResults = await Promise.all");
const packBoot = src.indexOf('id: "pack-boot"');
assert.ok(parallelWave >= 0, "the parallel slow-gate wave must exist");
assert.ok(
packBoot > parallelWave,
"pack-boot must be declared after the parallel artifact build"
);
assert.match(
src,
/packArtifactResult[\s\S]*?check:pack-boot/,
"pack-boot must be explicitly sequenced from the package-artifact result"
);
});
// ─── --full-ci gate extraction (P0, v3.8.46 post-mortem) ─────────────────────
const CI_FIXTURE = `
@@ -259,7 +290,11 @@ test("extractCiGates: pulls npm-run gate steps from the ci.yml gate jobs only",
assert.ok(ids.includes("check:docs-all") && ids.includes("check:docs-symbols"), "multi-line run");
// …and NON-gate steps + jobs outside the gate set are ignored.
assert.ok(!ids.includes("build") && !ids.some((i) => i.startsWith("test:")), "no build/test-run");
assert.equal(gates.find((g) => g.job === "test-unit"), undefined, "test-unit job is not scanned");
assert.equal(
gates.find((g) => g.job === "test-unit"),
undefined,
"test-unit job is not scanned"
);
});
test("extractCiGates: preserves `-- <args>` so ratchet flags reach the script", () => {
@@ -272,7 +307,10 @@ test("extractCiGates: preserves `-- <args>` so ratchet flags reach the script",
test("extractCiGates: skips the non-local gates (pr-evidence, codeql-ratchet)", () => {
const ids = extract(CI_FIXTURE).map((g) => g.id);
assert.ok(!ids.includes("check:pr-evidence"), "pr-evidence needs a PR body — skipped");
assert.ok(!ids.includes("check:codeql-ratchet"), "codeql-ratchet is a remote-main check — skipped");
assert.ok(
!ids.includes("check:codeql-ratchet"),
"codeql-ratchet is a remote-main check — skipped"
);
assert.ok(FULL_CI_SKIP.has("check:pr-evidence") && FULL_CI_SKIP.has("check:codeql-ratchet"));
});
@@ -295,10 +333,7 @@ test("extractCiGates: attaches GITHUB_BASE_REF=main env to test-masking + de-dup
test("extractCiGates: the REAL ci.yml yields the base-reds that leaked in v3.8.46", async () => {
const fs = await import("node:fs");
const yaml = fs.readFileSync(
new URL("../../.github/workflows/ci.yml", import.meta.url),
"utf8"
);
const yaml = fs.readFileSync(new URL("../../.github/workflows/ci.yml", import.meta.url), "utf8");
const ids = new Set(extract(yaml).map((g) => g.id));
// The exact gates that leaked to the v3.8.46 release PR because the pre-flight
// never ran them — --full-ci now reproduces every one.