Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/ci-static-checks/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down
32 changes: 16 additions & 16 deletions test/install-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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'
Expand All @@ -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),
Expand All @@ -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,
});
Expand All @@ -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,
Expand All @@ -602,12 +603,11 @@ 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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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/);
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions test/pr-workflow-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Loading