fix(ci): stamp BUILD_SHA before the release-green pack gate validates

check:pack-artifact assembles dist/ through build:cli, which never writes
dist/BUILD_SHA (only build:release does). #12959 pointed the provenance ref at
HEAD, but the #10427 guard still stops at 'dist/BUILD_SHA is missing' before it
ever reaches the ancestry check — reproduced on tip + #13635 + #13436, the first
tree whose Turbopack build compiles.

ci.yml sequences build -> stamp -> validate; the validator now does the same in
both entry points, keeping PACK_GATE_ENV for the validate step. The guard is
unchanged: an unstamped dist/ or one built from another commit still fails.
On that tree the stamped gate passes: 'BUILD_SHA 5cb3ae5d9 is on the release line'.

Refs #12732
This commit is contained in:
diegosouzapw
2026-09-14 19:07:00 -03:00
parent 6b08790274
commit 2ef073f513
3 changed files with 64 additions and 7 deletions

View File

@@ -0,0 +1 @@
- **fix(ci):** the release-green validator now runs its package-artifact gate the way `ci.yml` does — build, stamp `dist/BUILD_SHA`, then validate against the tree under test. `build:cli` never writes the stamp, so even with the provenance ref pointed at `HEAD` the gate could only ever report "dist/BUILD_SHA is missing" once the build itself compiled ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732))

View File

@@ -461,6 +461,37 @@ async function runAsync(cmd, cmdArgs, opts = {}) {
}
}
/**
* Package-artifact gate, run the way ci.yml's pack job runs it (#10427).
*
* `check:pack-artifact` assembles dist/ through `build:cli` when staging is missing, and
* `build:cli` never writes dist/BUILD_SHA — only `build:release` does. Pointing the ref at
* HEAD (PACK_GATE_ENV) is not enough on its own: the guard still stops at "dist/BUILD_SHA is
* missing". ci.yml builds, stamps, then validates; mirror that order here. The guard is not
* relaxed: an unstamped dist/ or one built from another commit still fails.
*/
async function runPackArtifactGate(timeoutMs) {
const deadline = Date.now() + timeoutMs;
const steps = [
{ cmd: npmCmd, args: ["run", "build:cli"] },
{ cmd: process.execPath, args: ["scripts/build/write-build-sha.mjs"] },
{
cmd: npmCmd,
args: ["run", "check:pack-artifact"],
env: PACK_GATE_ENV,
},
];
let out = "";
for (const step of steps) {
const remaining = deadline - Date.now();
if (remaining <= 0) return classifyRunError({ killed: true, signal: "SIGTERM" }, timeoutMs);
const result = await runAsync(step.cmd, step.args, { env: step.env, timeout: remaining });
out += result.out;
if (result.code !== 0) return { code: result.code, out };
}
return { code: 0, out };
}
async function main() {
const args = new Set(process.argv.slice(2));
const JSON_OUT = args.has("--json");
@@ -713,14 +744,15 @@ async function main() {
slow.push({
id: "pack-artifact",
label: "Package artifact (npm pack policy)",
args: ["run", "check:pack-artifact"],
env: PACK_GATE_ENV,
run: runPackArtifactGate,
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, env: g.env }))
slow.map((g) =>
g.run ? g.run(g.timeout) : runAsync(npmCmd, g.args, { timeout: g.timeout, env: g.env })
)
);
slow.forEach((g, i) => {
const { code, out } = slowResults[i];
@@ -770,10 +802,7 @@ async function main() {
}
} 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"], {
env: PACK_GATE_ENV,
timeout: 20 * 60 * 1000,
});
const { code, out } = await runPackArtifactGate(20 * 60 * 1000);
saveGateLog("pack-artifact", out);
record({
id: "pack-artifact",

View File

@@ -277,6 +277,33 @@ test("pre-flight runs tarball boot only after the package artifact builder compl
);
});
test("pack gate builds, stamps dist/BUILD_SHA, then validates against the tree under test (#10427)", async () => {
const fs = await import("node:fs");
const src = fs.readFileSync(
new URL("../../scripts/quality/validate-release-green.mjs", import.meta.url),
"utf8"
);
const gate = src.slice(src.indexOf("async function runPackArtifactGate"));
assert.ok(gate.length > 0, "the pack gate runner must exist");
const buildAt = gate.indexOf('"build:cli"');
const stampAt = gate.indexOf("scripts/build/write-build-sha.mjs");
const checkAt = gate.indexOf('"check:pack-artifact"');
// `build:cli` never writes dist/BUILD_SHA, so a bare `check:pack-artifact` always failed
// the provenance guard with "dist/BUILD_SHA is missing" — the same trap ci.yml avoids.
assert.ok(buildAt >= 0 && stampAt > buildAt, "BUILD_SHA must be stamped after build:cli");
assert.ok(checkAt > stampAt, "the artifact must be validated only after it is stamped");
assert.match(
gate.slice(checkAt, checkAt + 200),
/env: PACK_GATE_ENV/,
"a release-branch tip is never an ancestor of origin/main mid-cycle"
);
assert.match(src, /const PACK_GATE_ENV = \{ OMNIROUTE_RELEASE_REF: "HEAD" \}/);
// Both entry points (the parallel wave and --with-build --quick) must use it.
assert.equal(src.match(/runPackArtifactGate\b/g)?.length, 3);
assert.doesNotMatch(src, /runAsync\(npmCmd, \["run", "check:pack-artifact"\]/);
assert.doesNotMatch(src, /id: "pack-artifact",[^}]*args:/);
});
// ─── --full-ci gate extraction (P0, v3.8.46 post-mortem) ─────────────────────
const CI_FIXTURE = `