fix: handle npm min-release-age in installers

Replays #84749 because the contributor fork branch became conflicted and was no longer maintainer-writable.

Co-authored-by: TeodoroRodrigo <rodrigoteodoro.90@gmail.com>
This commit is contained in:
Peter Steinberger
2026-05-25 08:13:47 +01:00
committed by GitHub
parent 6704d0ab27
commit 316d97c938
8 changed files with 526 additions and 156 deletions

View File

@@ -1,5 +1,15 @@
import { spawnSync } from "node:child_process";
import { chmodSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import {
chmodSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
readlinkSync,
rmSync,
symlinkSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
@@ -23,6 +33,74 @@ function linkRequiredShellTools(bin: string) {
}
}
function writeNpmFreshnessConflictFixture(path: string, argsLog: string) {
writeFileSync(
path,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
" printf 'null\\n'",
" exit 0",
"fi",
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "before" ]]; then',
" printf 'Wed May 13 2026 21:25:20 GMT-0300 (Brasilia Standard Time)\\n'",
" exit 0",
"fi",
'for arg in "$@"; do',
' if [[ "$arg" == --before=* ]]; then',
" printf '%s\\n' 'Exit prior to config file resolving' >&2",
" printf '%s\\n' 'cause' >&2",
" printf '%s\\n' '--min-release-age cannot be provided when using --before' >&2",
" exit 64",
" fi",
"done",
'for arg in "$@"; do',
' if [[ "$arg" == "--min-release-age=0" ]]; then',
" exit 0",
" fi",
"done",
"exit 65",
"",
].join("\n"),
);
chmodSync(path, 0o755);
}
function writeNpmBeforePolicyFixture(path: string, argsLog: string) {
writeFileSync(
path,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
`printf '%s\\n' "$*" >> ${JSON.stringify(argsLog)}`,
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "min-release-age" ]]; then',
" printf 'null\\n'",
" exit 0",
"fi",
'if [[ "$1" == "config" && "$2" == "get" && "$3" == "before" ]]; then',
" printf 'Wed May 13 2026 21:25:20 GMT-0300 (Brasilia Standard Time)\\n'",
" exit 0",
"fi",
'for arg in "$@"; do',
' if [[ "$arg" == "--min-release-age=0" ]]; then',
" printf '%s\\n' 'min-release-age should not be selected for project-only npmrc' >&2",
" exit 64",
" fi",
"done",
'for arg in "$@"; do',
' if [[ "$arg" == --before=* ]]; then',
" exit 0",
" fi",
"done",
"exit 65",
"",
].join("\n"),
);
chmodSync(path, 0o755);
}
describe("install-cli.sh", () => {
const script = readFileSync(SCRIPT_PATH, "utf8");
@@ -144,12 +222,7 @@ describe("install-cli.sh", () => {
linkRequiredShellTools(bin);
writeFileSync(
fakeApk,
[
"#!/bin/bash",
'printf "%s\\n" "$*" >> "$APK_LOG"',
"exit 99",
"",
].join("\n"),
["#!/bin/bash", 'printf "%s\\n" "$*" >> "$APK_LOG"', "exit 99", ""].join("\n"),
);
writeFileSync(
fakeNode,
@@ -166,14 +239,7 @@ describe("install-cli.sh", () => {
"",
].join("\n"),
);
writeFileSync(
fakeNpm,
[
"#!/bin/bash",
"exit 0",
"",
].join("\n"),
);
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
chmodSync(fakeApk, 0o755);
chmodSync(fakeNode, 0o755);
chmodSync(fakeNpm, 0o755);
@@ -233,12 +299,7 @@ describe("install-cli.sh", () => {
mkdirSync(nodePrefixBin, { recursive: true });
writeFileSync(
fakeApk,
[
"#!/bin/bash",
'printf "%s\\n" "$*" >> "$APK_LOG"',
"exit 99",
"",
].join("\n"),
["#!/bin/bash", 'printf "%s\\n" "$*" >> "$APK_LOG"', "exit 99", ""].join("\n"),
);
writeFileSync(
staleNode,
@@ -285,22 +346,8 @@ describe("install-cli.sh", () => {
"",
].join("\n"),
);
writeFileSync(
oldNpm,
[
"#!/bin/bash",
"exit 0",
"",
].join("\n"),
);
writeFileSync(
fakeNpm,
[
"#!/bin/bash",
"exit 0",
"",
].join("\n"),
);
writeFileSync(oldNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
chmodSync(fakeApk, 0o755);
chmodSync(staleNode, 0o755);
chmodSync(oldNode, 0o755);
@@ -384,14 +431,7 @@ describe("install-cli.sh", () => {
"",
].join("\n"),
);
writeFileSync(
fakeNpm,
[
"#!/bin/bash",
"exit 0",
"",
].join("\n"),
);
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
chmodSync(fakeApk, 0o755);
chmodSync(fakeNode, 0o755);
chmodSync(fakeNpm, 0o755);
@@ -446,12 +486,7 @@ describe("install-cli.sh", () => {
linkRequiredShellTools(bin);
writeFileSync(
fakeApk,
[
"#!/bin/bash",
'printf "%s\\n" "$*" >> "$APK_LOG"',
"exit 0",
"",
].join("\n"),
["#!/bin/bash", 'printf "%s\\n" "$*" >> "$APK_LOG"', "exit 0", ""].join("\n"),
);
writeFileSync(
fakeNode,
@@ -468,14 +503,7 @@ describe("install-cli.sh", () => {
"",
].join("\n"),
);
writeFileSync(
fakeNpm,
[
"#!/bin/bash",
"exit 0",
"",
].join("\n"),
);
writeFileSync(fakeNpm, ["#!/bin/bash", "exit 0", ""].join("\n"));
chmodSync(fakeApk, 0o755);
chmodSync(fakeNode, 0o755);
chmodSync(fakeNpm, 0o755);
@@ -504,7 +532,9 @@ describe("install-cli.sh", () => {
expect(result.status).toBe(1);
expect(readFileSync(apkLog, "utf8")).toContain("add --no-cache nodejs npm");
expect(result.stdout).toContain("Alpine Node package must provide Node >= 22.22.0 with node:sqlite");
expect(result.stdout).toContain(
"Alpine Node package must provide Node >= 22.22.0 with node:sqlite",
);
expect(result.stdout).toContain("found v22.18.0");
} finally {
rmSync(tmp, { force: true, recursive: true });
@@ -513,7 +543,7 @@ describe("install-cli.sh", () => {
it("clears npm freshness filters for package installs", () => {
expect(script).toContain('freshness_flag="--min-release-age=0"');
expect(script).toContain('npm_raw_config_has_key "min-release-age"');
expect(script).toContain('npm_config_has_raw_key "$(npm_bin)" "min-release-age"');
expect(script).toContain('freshness_flag="--before=$(date -u');
expect(script).toContain("env -u NPM_CONFIG_BEFORE -u npm_config_before");
});
@@ -748,4 +778,78 @@ describe("install-cli.sh", () => {
expect(result.stdout).toContain("npm installs do not support OpenClaw GitHub source targets");
expect(result.stdout).toContain("--install-method git --version main");
});
it("does not emit before args when npmrc min-release-age computes a before cutoff", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-freshness-"));
const prefix = join(tmp, "prefix");
const home = join(tmp, "home");
const nodeBin = join(prefix, "tools/node-v22.22.0/bin");
const argsLog = join(tmp, "npm-args.log");
mkdirSync(nodeBin, { recursive: true });
mkdirSync(home, { recursive: true });
writeFileSync(join(home, ".npmrc"), "min-release-age=7\n");
writeNpmFreshnessConflictFixture(join(nodeBin, "npm"), argsLog);
let result: ReturnType<typeof runInstallCliShell> | undefined;
let argsOutput = "";
try {
result = runInstallCliShell(
[
"set -euo pipefail",
`HOME=${JSON.stringify(home)}`,
`OPENCLAW_PREFIX=${JSON.stringify(prefix)}`,
"OPENCLAW_VERSION=2026.5.19",
`source ${JSON.stringify(SCRIPT_PATH)}`,
"ensure_git() { return 0; }",
"install_openclaw",
].join("\n"),
);
argsOutput = readFileSync(argsLog, "utf8");
} finally {
rmSync(tmp, { force: true, recursive: true });
}
expect(result?.status).toBe(0);
expect(argsOutput).toContain("--min-release-age=0");
expect(argsOutput).not.toContain("--before=");
});
it("ignores project npmrc when choosing global install freshness args", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-cli-global-freshness-"));
const prefix = join(tmp, "prefix");
const home = join(tmp, "home");
const project = join(tmp, "project");
const nodeBin = join(prefix, "tools/node-v22.22.0/bin");
const argsLog = join(tmp, "npm-args.log");
mkdirSync(nodeBin, { recursive: true });
mkdirSync(home, { recursive: true });
mkdirSync(project, { recursive: true });
writeFileSync(join(home, ".npmrc"), "before=2026-01-01T00:00:00.000Z\n");
writeFileSync(join(project, ".npmrc"), "min-release-age=7\n");
writeNpmBeforePolicyFixture(join(nodeBin, "npm"), argsLog);
let result: ReturnType<typeof runInstallCliShell> | undefined;
let argsOutput = "";
try {
result = runInstallCliShell(
[
"set -euo pipefail",
`cd ${JSON.stringify(project)}`,
`HOME=${JSON.stringify(home)}`,
`OPENCLAW_PREFIX=${JSON.stringify(prefix)}`,
"OPENCLAW_VERSION=2026.5.19",
`source ${JSON.stringify(process.cwd() + "/" + SCRIPT_PATH)}`,
"ensure_git() { return 0; }",
"install_openclaw",
].join("\n"),
);
argsOutput = readFileSync(argsLog, "utf8");
} finally {
rmSync(tmp, { force: true, recursive: true });
}
expect(result?.status).toBe(0);
expect(argsOutput).toContain("--before=");
expect(argsOutput).not.toContain("--min-release-age=0");
});
});