From 34b8a3fda02cd2c1adea7553ebe0a6041ec1046e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 15 Jul 2026 07:07:54 +0100 Subject: [PATCH] fix(release): isolate trusted candidate dependencies --- scripts/release-candidate-checklist.mjs | 14 ++++++-------- test/scripts/release-candidate-checklist.test.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/scripts/release-candidate-checklist.mjs b/scripts/release-candidate-checklist.mjs index a034c7b59a41..0ec527303b10 100644 --- a/scripts/release-candidate-checklist.mjs +++ b/scripts/release-candidate-checklist.mjs @@ -9,7 +9,6 @@ import { readFileSync, renameSync, rmSync, - symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; @@ -273,6 +272,7 @@ export function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd, encoding: "utf8", + env: options.env ? { ...process.env, ...options.env } : process.env, maxBuffer: COMMAND_CAPTURE_MAX_BUFFER_BYTES, stdio: options.capture ? ["ignore", "pipe", "pipe"] : "inherit", }); @@ -516,13 +516,11 @@ function runFromTrustedTooling(argv, { targetRoot, workflowRef }) { cwd: targetRoot, }); worktreeAdded = true; - const targetNodeModules = join(targetRoot, "node_modules"); - if (!existsSync(targetNodeModules)) { - throw new Error("release candidate validation requires target checkout node_modules"); - } - // Trusted release tooling executes from an isolated worktree, while dependency - // bytes stay pinned to the prepared target checkout used by local preflight. - symlinkSync(targetNodeModules, join(toolingRoot, "node_modules"), "dir"); + // Build an isolated trusted dependency tree. Lifecycle scripts stay disabled; + // the frozen lockfile and prefer-offline store keep setup deterministic. + run("pnpm", ["install", "--frozen-lockfile", "--ignore-scripts", "--prefer-offline"], { + cwd: toolingRoot, + }); const result = spawnSync( process.execPath, [join(toolingRoot, "scripts/release-candidate-checklist.mjs"), ...argv], diff --git a/test/scripts/release-candidate-checklist.test.ts b/test/scripts/release-candidate-checklist.test.ts index 1a7412ff2764..148aaf69a490 100644 --- a/test/scripts/release-candidate-checklist.test.ts +++ b/test/scripts/release-candidate-checklist.test.ts @@ -98,6 +98,16 @@ describe("release candidate checklist", () => { expect(output).toHaveLength(2 * 1024 * 1024); }); + it("passes scoped environment overrides to release child commands", () => { + const output = run( + process.execPath, + ["-e", "process.stdout.write(process.env.OPENCLAW_RELEASE_TEST_VALUE ?? '')"], + { capture: true, env: { OPENCLAW_RELEASE_TEST_VALUE: "passed" } }, + ); + + expect(output).toBe("passed"); + }); + it("keeps the frozen release target separate from clean trusted workflow tooling", () => { expect( validateCandidateCheckout({ @@ -161,6 +171,10 @@ describe("release candidate checklist", () => { ).toThrow("clean tracked tooling checkout"); const source = readFileSync("scripts/release-candidate-checklist.mjs", "utf8"); expect(source).toContain('const TOOLING_ROOT = fileURLToPath(new URL("../", import.meta.url))'); + expect(source).toContain('mkdtempSync(join(tmpdir(), "openclaw-release-tooling-"))'); + expect(source).toContain( + '["install", "--frozen-lockfile", "--ignore-scripts", "--prefer-offline"]', + ); expect(source).toContain("`+refs/heads/${workflowRef}:${remoteRef}`"); expect(source).toContain('"worktree", "add", "--detach", toolingRoot, trustedToolingSha'); expect(source).toContain(