From 6059b6aca72a7cac974dcee997586522691d979d Mon Sep 17 00:00:00 2001 From: Chengjie Wang Date: Fri, 10 Jul 2026 15:21:46 +0800 Subject: [PATCH 1/2] fix(installer): preserve sandbox payload lockfile Signed-off-by: Chengjie Wang --- .github/actions/ci-static-checks/action.yaml | 4 ++++ scripts/install.sh | 4 ++-- test/install-preflight.test.ts | 22 ++++++++++---------- test/pr-workflow-contract.test.ts | 4 ++++ 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.github/actions/ci-static-checks/action.yaml b/.github/actions/ci-static-checks/action.yaml index 043466e958f..7934612ea23 100644 --- a/.github/actions/ci-static-checks/action.yaml +++ b/.github/actions/ci-static-checks/action.yaml @@ -25,6 +25,10 @@ runs: [ "$HADOLINT_SHA256" = "$ACTUAL" ] || { echo "::error::hadolint checksum mismatch"; exit 1; } chmod +x /usr/local/bin/hadolint + - name: Validate sandbox payload lockfile + shell: bash + run: npm --prefix nemoclaw ci --ignore-scripts --dry-run + - name: Install dependencies shell: bash run: npm install --ignore-scripts diff --git a/scripts/install.sh b/scripts/install.sh index 0bc02fef3aa..894ad1b6ff8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1537,7 +1537,7 @@ install_nemoclaw() { fi spin "Installing ${_CLI_DISPLAY} dependencies" bash -c "cd \"$NEMOCLAW_SOURCE_ROOT\" && npm install --ignore-scripts" spin "Building ${_CLI_DISPLAY} CLI modules" bash -c "cd \"$NEMOCLAW_SOURCE_ROOT\" && npm run --if-present build:cli" - spin "Building ${_CLI_DISPLAY} plugin" bash -c "cd \"$NEMOCLAW_SOURCE_ROOT\"/nemoclaw && npm install --ignore-scripts && npm run build" + spin "Building ${_CLI_DISPLAY} plugin" bash -c "cd \"$NEMOCLAW_SOURCE_ROOT\"/nemoclaw && npm ci --ignore-scripts && npm run build" spin "Linking ${_CLI_DISPLAY} CLI" bash -c "cd \"$NEMOCLAW_SOURCE_ROOT\" && npm link" # Bootstrap OpenShell when the source checkout is being used as a fresh @@ -1580,7 +1580,7 @@ install_nemoclaw() { fi spin "Installing ${_CLI_DISPLAY} dependencies" bash -c "cd \"$nemoclaw_src\" && npm install --ignore-scripts" spin "Building ${_CLI_DISPLAY} CLI modules" bash -c "cd \"$nemoclaw_src\" && npm run --if-present build:cli" - spin "Building ${_CLI_DISPLAY} plugin" bash -c "cd \"$nemoclaw_src\"/nemoclaw && npm install --ignore-scripts && npm run build" + spin "Building ${_CLI_DISPLAY} plugin" bash -c "cd \"$nemoclaw_src\"/nemoclaw && npm ci --ignore-scripts && npm run build" spin "Linking ${_CLI_DISPLAY} CLI" bash -c "cd \"$nemoclaw_src\" && npm link" # Install/upgrade the OpenShell CLI on the GitHub-clone path (curl|bash). diff --git a/test/install-preflight.test.ts b/test/install-preflight.test.ts index c030396866a..2b3eb7e84a8 100644 --- a/test/install-preflight.test.ts +++ b/test/install-preflight.test.ts @@ -41,8 +41,9 @@ function writeNpmStub(fakeBin: string, installSnippet: string = "exit 0") { set -euo pipefail if [ "$1" = "--version" ]; then echo "10.9.2"; exit 0; fi if [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "prefix" ]; then echo "$NPM_PREFIX"; exit 0; fi -if [ "$1" = "install" ] || [ "$1" = "link" ] || [ "$1" = "uninstall" ] || [ "$1" = "pack" ] || [ "$1" = "run" ]; then +if [ "$1" = "ci" ] || [ "$1" = "install" ] || [ "$1" = "link" ] || [ "$1" = "uninstall" ] || [ "$1" = "pack" ] || [ "$1" = "run" ]; then ${installSnippet} + if [ "$1" = "ci" ]; then exit 0; fi fi echo "unexpected npm invocation: $*" >&2; exit 98`, ); @@ -242,7 +243,7 @@ fi if [ "$1" = "pack" ]; then exit 1 fi -if [ "$1" = "install" ] && [[ "$*" == *"--ignore-scripts"* ]]; then +if { [ "$1" = "ci" ] || [ "$1" = "install" ]; } && [[ "$*" == *"--ignore-scripts"* ]]; then exit 0 fi if [ "$1" = "run" ]; then @@ -346,7 +347,7 @@ fi if [ "$1" = "pack" ]; then exit 1 fi -if [ "$1" = "install" ] && [[ "$*" == *"--ignore-scripts"* ]]; then +if { [ "$1" = "ci" ] || [ "$1" = "install" ]; } && [[ "$*" == *"--ignore-scripts"* ]]; then exit 0 fi if [ "$1" = "run" ]; then @@ -505,8 +506,7 @@ exit 98 expect(output.trim()).toBe("nemoclaw-installer"); expect(output).not.toMatch(/0\.1\.0/); }); - - it("uses npm install + npm link for a source checkout (no -g)", { timeout: 20000 }, () => { + it("preserves the sandbox payload lockfile with npm ci (#3798)", { timeout: 20000 }, () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-source-")); const fakeBin = path.join(tmp, "bin"); const prefix = path.join(tmp, "prefix"); @@ -602,8 +602,8 @@ fi`, expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); const log = fs.readFileSync(npmLog, "utf-8"); - // install (no -g) and link must both have been called - expect(log).toMatch(/^install(?!\s+-g)/m); + expect(log.match(/^install --ignore-scripts$/gm)).toHaveLength(1); + expect(log.match(/^ci --ignore-scripts$/gm)).toHaveLength(1); expect(log).toMatch(/^link/m); // the GitHub URL must NOT appear — this is a local install expect(log).not.toMatch(new RegExp(GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); @@ -1711,7 +1711,7 @@ exit 0 echo "ENOTFOUND simulated network error" >&2 exit 1 fi -if [ "$1" = "install" ] || [ "$1" = "run" ] || [ "$1" = "link" ]; then +if [ "$1" = "ci" ] || [ "$1" = "install" ] || [ "$1" = "run" ] || [ "$1" = "link" ]; then echo "ENOTFOUND simulated network error" >&2 exit 1 fi`, @@ -1802,7 +1802,7 @@ fi if [ "$1" = "pack" ]; then exit 1 fi -if [ "$1" = "install" ] && [[ "$*" == *"--ignore-scripts"* ]]; then +if { [ "$1" = "ci" ] || [ "$1" = "install" ]; } && [[ "$*" == *"--ignore-scripts"* ]]; then exit 0 fi if [ "$1" = "run" ]; then @@ -1934,7 +1934,7 @@ fi if [ "$1" = "pack" ]; then exit 1 fi -if [ "$1" = "install" ] && [[ "$*" == *"--ignore-scripts"* ]]; then +if { [ "$1" = "ci" ] || [ "$1" = "install" ]; } && [[ "$*" == *"--ignore-scripts"* ]]; then exit 0 fi if [ "$1" = "run" ]; then @@ -3218,7 +3218,7 @@ set -euo pipefail if [ "$1" = "--version" ]; then echo "10.9.2"; exit 0; fi if [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "prefix" ]; then echo "$NPM_PREFIX"; exit 0; fi if [ "$1" = "pack" ]; then exit 1; fi -if [ "$1" = "install" ] && [[ "$*" == *"--ignore-scripts"* ]]; then exit 0; fi +if { [ "$1" = "ci" ] || [ "$1" = "install" ]; } && [[ "$*" == *"--ignore-scripts"* ]]; then exit 0; fi if [ "$1" = "run" ]; then exit 0; fi if [ "$1" = "uninstall" ]; then exit 0; fi if [ "$1" = "link" ]; then diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index c4e52e803f2..33ece913274 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -906,6 +906,10 @@ describe("pull request and main workflow contracts", () => { const installerRuns = stepRuns(sharedActions.installerIntegration).join("\n"); expect(staticRuns).toContain("npm install --ignore-scripts"); + expect(staticRuns).toContain("npm --prefix nemoclaw ci --ignore-scripts --dry-run"); + expect( + requiredStepIndex(sharedActions.staticChecks, "Validate sandbox payload lockfile"), + ).toBeLessThan(requiredStepIndex(sharedActions.staticChecks, "Install dependencies")); expect(staticRuns).toContain("npm run validate:configs"); expect(staticRuns).toContain("npm run typecheck:scorecard"); expect(staticPrekRun).toContain("npx prek run --all-files --stage pre-commit"); From 27624139132562fe71ca9f6a30b812aa8ab92ece Mon Sep 17 00:00:00 2001 From: Carlos Villela Date: Fri, 10 Jul 2026 21:15:59 -0700 Subject: [PATCH 2/2] test(installer): verify payload lock preservation Signed-off-by: Carlos Villela --- test/install-preflight.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/install-preflight.test.ts b/test/install-preflight.test.ts index 2b3eb7e84a8..b22b38614ca 100644 --- a/test/install-preflight.test.ts +++ b/test/install-preflight.test.ts @@ -550,7 +550,7 @@ if [ "$1" = "pack" ]; then tar -czf "$tmpdir/openclaw-2026.3.11.tgz" -C "$tmpdir" package exit 0 fi -if [ "$1" = "install" ]; then exit 0; fi +if [ "$1" = "install" ]; then printf '{"rewritten":true}\n' > package-lock.json; exit 0; fi if [ "$1" = "run" ] && { [ "$2" = "build" ] || [ "$2" = "build:cli" ] || [ "$2" = "--if-present" ]; }; then exit 0; fi if [ "$1" = "link" ]; then cat > "$NPM_PREFIX/bin/nemoclaw" <<'EOS' @@ -564,8 +564,6 @@ EOS fi`, ); - // Write a package.json that triggers the source-checkout path. - // Must use spaces after colons to match the grep in install.sh. fs.writeFileSync( path.join(tmp, "package.json"), JSON.stringify({ name: "nemoclaw", version: "0.1.0" }, null, 2), @@ -575,6 +573,8 @@ fi`, path.join(tmp, "nemoclaw", "package.json"), JSON.stringify({ name: "nemoclaw-plugin", version: "0.1.0" }, null, 2), ); + const payloadLockPath = path.join(tmp, "nemoclaw", "package-lock.json"); + fs.writeFileSync(payloadLockPath, "payload lock sentinel\n"); fs.mkdirSync(path.join(tmp, "nemoclaw-blueprint", "router", "llm-router"), { recursive: true, }); @@ -593,6 +593,7 @@ fi`, NEMOCLAW_NON_INTERACTIVE: "1", NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1", NEMOCLAW_DEFER_OPENSHELL_INSTALL: "1", + NEMOCLAW_REPO_ROOT: tmp, NPM_PREFIX: prefix, NPM_LOG_PATH: npmLog, PYTHON_LOG_PATH: pythonLog, @@ -604,10 +605,9 @@ fi`, const log = fs.readFileSync(npmLog, "utf-8"); expect(log.match(/^install --ignore-scripts$/gm)).toHaveLength(1); expect(log.match(/^ci --ignore-scripts$/gm)).toHaveLength(1); + expect(fs.readFileSync(payloadLockPath)).toEqual(Buffer.from("payload lock sentinel\n")); expect(log).toMatch(/^link/m); - // the GitHub URL must NOT appear — this is a local install expect(log).not.toMatch(new RegExp(GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); - // Model Router must not run provider-specific dependency setup from the generic installer. expect(fs.existsSync(pythonLog)).toBe(false); const gitCalls = fs.existsSync(gitLog) ? fs.readFileSync(gitLog, "utf-8") : ""; expect(gitCalls).not.toMatch(/submodule/);