From c4a1d355366c89f15d3e86edd846023cbcb63b94 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Oct 2026 01:48:58 -0500 Subject: [PATCH] fix(ci): freeze lockfiles before pnpm bootstrap (#162862) ## What Problem This Solves Older selected-source installers can invoke pnpm before their install-only frozen-lockfile flag takes effect, allowing package-manager bootstrap to rewrite the selected source lockfile. ## User Impact The source receiver supplies frozen policy before starting the selected installer. Source verification, dependency pins, and false-input behavior stay unchanged. No schema or runtime storage change. ## Why This Change Was Made Current main already fixes its shared installer with early native policy and an explicit install flag. This PR preserves that implementation and adds only the receiver process-boundary setting, plus distinct regression coverage. The explicit flag remains necessary for older pnpm workspace-configuration precedence. The final production/tooling delta is one added line. ## Evidence Actual pnpm 12.5.1 originally reproduced the lockfile mutation. The repaired Linux receiver preserved the selected source through the unchanged post-install verifier; its later SQLite diagnostic failure remains separate and is not claimed fixed. On the corrected composition, all 14 hydration cases passed in 38.273 seconds. Actual pnpm 10.23.0 conflicting-workspace and false/invalid-input cells preserved the expected lockfile behavior. Prior receiver boundary proof remains applicable to unchanged source. Independent full-diff P2 review is clean. The earlier broad gate attempt and historical diagnostic failures remain recorded. The maintainer explicitly waived further test execution and approved landing this reviewed correction; no current-head full-suite or hosted-CI success is claimed. Independent code review, security review, and exact-head merge admission remain required. --- scripts/crabbox-source-receiver.mts | 1 + .../crabbox-hydrate-dependencies.test.ts | 45 +++++++++++++++++++ test/scripts/crabbox-wrapper.test.ts | 6 ++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/scripts/crabbox-source-receiver.mts b/scripts/crabbox-source-receiver.mts index 7731b9edeb90..34abd6e0c2a3 100644 --- a/scripts/crabbox-source-receiver.mts +++ b/scripts/crabbox-source-receiver.mts @@ -289,6 +289,7 @@ try { // source's install owner before any caller payload can run. const installEnv = { ...env, CI: "true", GITHUB_WORKSPACE: cwd, NODE_BIN: path.dirname(process.execPath), FROZEN_LOCKFILE: "true", + PNPM_CONFIG_FROZEN_LOCKFILE: "true", DEPENDENCY_CACHE: "false", DEPENDENCY_CACHE_HIT: "false" }; for (const key of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE"]) delete installEnv[key]; process.stderr.write("[crabbox] reconciling selected-source dependencies\n"); diff --git a/test/scripts/crabbox-hydrate-dependencies.test.ts b/test/scripts/crabbox-hydrate-dependencies.test.ts index 20b00d9dac1f..c7ec5cd676de 100644 --- a/test/scripts/crabbox-hydrate-dependencies.test.ts +++ b/test/scripts/crabbox-hydrate-dependencies.test.ts @@ -47,6 +47,51 @@ function write(root: string, relative: string, contents: string, mode?: number) } describe.skipIf(process.platform === "win32")("Crabbox dependency hydration", () => { + it.each(["true", "false", "invalid"])( + "preserves frozen policy through pnpm bootstrap and workspace config (%s)", + (frozen) => { + const root = tempDirs.make("openclaw-frozen-bootstrap-"); + const bin = path.join(root, "bin"); + mkdirSync(bin); + symlinkSync(resolveTestNodeExecPath(), path.join(bin, "node")); + const lock = path.join(root, "pnpm-lock.yaml"); + const calls = path.join(root, "pnpm-calls"); + writeFileSync(lock, "original\n"); + write( + bin, + "pnpm", + `#!/bin/bash +printf '%s\\n' "$*" >> "$PNPM_CALLS" +# pnpm 10 applies workspace frozenLockfile:false after its environment settings. +if [ "$1" = install ]; then + case " $* " in + *" --frozen-lockfile "*) ;; + *) printf 'rewritten\\n' > "$PNPM_LOCK" ;; + esac +elif [ "\${PNPM_CONFIG_FROZEN_LOCKFILE:-}" != true ]; then + printf 'rewritten\\n' > "$PNPM_LOCK" +fi +`, + 0o755, + ); + const result = spawnSync("bash", [".github/actions/setup-node-env/install-dependencies.sh"], { + encoding: "utf8", + env: { + PATH: process.env.PATH, + NODE_BIN: bin, + FROZEN_LOCKFILE: frozen, + DEPENDENCY_CACHE: "false", + DEPENDENCY_CACHE_HIT: "false", + PNPM_CALLS: calls, + PNPM_LOCK: lock, + }, + }); + expect(result.status, result.stderr).toBe(frozen === "invalid" ? 2 : 0); + expect(readFileSync(lock, "utf8")).toBe(frozen === "false" ? "rewritten\n" : "original\n"); + expect(existsSync(calls)).toBe(frozen !== "invalid"); + }, + ); + it.each([ ["default hydration", "fresh"], ["shared setup action", "fresh"], diff --git a/test/scripts/crabbox-wrapper.test.ts b/test/scripts/crabbox-wrapper.test.ts index b34b18f14ebd..6c750f8260c5 100644 --- a/test/scripts/crabbox-wrapper.test.ts +++ b/test/scripts/crabbox-wrapper.test.ts @@ -4742,7 +4742,11 @@ process.on("exit", () => { const copyOptions = { recursive: true, verbatimSymlinks: true }; cpSync(selectedSource, producer, copyOptions); mkdirSync(path.dirname(path.join(producer, installOwner)), { recursive: true }); - writeFileSync(path.join(producer, installOwner), installer); + writeFileSync( + path.join(producer, installOwner), + // Older selected installers can invoke pnpm before applying their install-only flag. + 'test "${PNPM_CONFIG_FROZEN_LOCKFILE:-}" = true || exit 73\n' + installer, + ); writeFileSync( path.join(producer, sourceCommand), [