diff --git a/changelog.d/maintenance/12732-release-green-pack-gate-provenance.md b/changelog.d/maintenance/12732-release-green-pack-gate-provenance.md new file mode 100644 index 0000000000..b582399352 --- /dev/null +++ b/changelog.d/maintenance/12732-release-green-pack-gate-provenance.md @@ -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)) diff --git a/scripts/quality/validate-release-green.mjs b/scripts/quality/validate-release-green.mjs index e5939214e2..d7d5143fa9 100644 --- a/scripts/quality/validate-release-green.mjs +++ b/scripts/quality/validate-release-green.mjs @@ -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", diff --git a/tests/unit/validate-release-green.test.ts b/tests/unit/validate-release-green.test.ts index aae0570284..d116bf0722 100644 --- a/tests/unit/validate-release-green.test.ts +++ b/tests/unit/validate-release-green.test.ts @@ -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 = `