diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index 1d7522b70d0e..c9c9ad7458b3 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -699,49 +699,87 @@ async function prepareCandidate(params) { sourceDir: params.sourceDir, logPath: join(params.logsDir, "pnpm-pack-dry-run.log"), }); - logPhase("prepare", "package-candidate"); - const packResult = await runCommand( - process.execPath, - [ - join(params.sourceDir, "scripts", "package-openclaw-for-docker.mjs"), - "--skip-build", - "--source-dir", - params.sourceDir, - "--output-dir", - packDir, - ], - { - cwd: params.sourceDir, - logPath: join(params.logsDir, "package-candidate.log"), - timeoutMs: 15 * 60 * 1000, - }, - ); - const packOutputLines = packResult.stdout.trim().split(/\r?\n/u).filter(Boolean); - const packedTarball = resolvePackDestinationTarball( - packOutputLines.at(-1), + const packCommand = resolvePackageCandidatePackCommand(params.sourceDir, packDir); + logPhase("prepare", packCommand.phase); + const packResult = await runCommand(packCommand.command, packCommand.args, { + cwd: params.sourceDir, + logPath: join(params.logsDir, packCommand.logFileName), + timeoutMs: 15 * 60 * 1000, + }); + const packedCandidate = resolvePackedCandidateFromOutput({ + output: packResult.stdout, packDir, - "package-openclaw-for-docker", - ); - writeFileSync( - packJsonPath, - `${JSON.stringify( - { - filename: packedTarball.fileName, - path: packedTarball.path, - version: packageJson.version, - }, - null, - 2, - )}\n`, - "utf8", - ); + packageJson, + packCommand, + }); + writeFileSync(packJsonPath, packedCandidate.packJson, "utf8"); return { sourceDir: params.sourceDir, sourceSha, - candidateVersion: String(packageJson.version ?? "").trim(), - candidateTgz: packedTarball.path, - candidateFileName: packedTarball.fileName, + candidateVersion: packedCandidate.version, + candidateTgz: packedCandidate.path, + candidateFileName: packedCandidate.fileName, + }; +} + +export function resolvePackageCandidatePackCommand(sourceDir, packDir) { + const packageHelper = join(sourceDir, "scripts", "package-openclaw-for-docker.mjs"); + if (existsSync(packageHelper)) { + return { + args: [packageHelper, "--skip-build", "--source-dir", sourceDir, "--output-dir", packDir], + command: process.execPath, + kind: "docker-helper", + logFileName: "package-candidate.log", + phase: "package-candidate", + }; + } + + return { + args: ["pack", "--config.ignore-scripts=true", "--json", "--pack-destination", packDir], + command: pnpmCommand(), + kind: "pnpm-pack", + logFileName: "pnpm-pack.log", + phase: "pnpm-pack", + }; +} + +function resolvePackedCandidateFromOutput(params) { + if (params.packCommand.kind === "docker-helper") { + const packOutputLines = params.output.trim().split(/\r?\n/u).filter(Boolean); + const packedTarball = resolvePackDestinationTarball( + packOutputLines.at(-1), + params.packDir, + "package-openclaw-for-docker", + ); + return { + fileName: packedTarball.fileName, + packJson: `${JSON.stringify( + { + filename: packedTarball.fileName, + path: packedTarball.path, + version: params.packageJson.version, + }, + null, + 2, + )}\n`, + path: packedTarball.path, + version: String(params.packageJson.version ?? "").trim(), + }; + } + + const parsedPack = JSON.parse(params.output); + const lastPack = Array.isArray(parsedPack) ? parsedPack.at(-1) : parsedPack; + const packedTarball = resolvePackDestinationTarball( + lastPack?.filename, + params.packDir, + "pnpm pack", + ); + return { + fileName: packedTarball.fileName, + packJson: params.output, + path: packedTarball.path, + version: String(lastPack?.version ?? params.packageJson.version ?? "").trim(), }; } @@ -2973,7 +3011,9 @@ export function appendLatestNpmDebugLogTail( env = process.env, platform = process.platform, ) { - const candidates = resolveNpmDebugLogDirs(homeDir, env, platform).flatMap(findNpmDebugLogs); + const candidates = resolveNpmDebugLogDirs(homeDir, env, platform) + .flatMap(findNpmDebugLogs) + .toSorted((left, right) => left.mtimeMs - right.mtimeMs); const latest = candidates.at(-1); if (!latest) { return ""; @@ -2993,8 +3033,16 @@ export function appendLatestNpmDebugLogTail( } export function resolveNpmDebugLogDirs(homeDir, env = process.env, platform = process.platform) { - const configuredLogsDir = String(env.npm_config_logs_dir ?? env.NPM_CONFIG_LOGS_DIR ?? "").trim(); - const configuredCache = String(env.npm_config_cache ?? env.NPM_CONFIG_CACHE ?? "").trim(); + const configuredLogsDir = resolveNpmConfiguredPath( + homeDir, + env.npm_config_logs_dir ?? env.NPM_CONFIG_LOGS_DIR, + platform, + ); + const configuredCache = resolveNpmConfiguredPath( + homeDir, + env.npm_config_cache ?? env.NPM_CONFIG_CACHE, + platform, + ); const localAppData = String(env.LOCALAPPDATA ?? "").trim(); const logDirs = [ configuredLogsDir, @@ -3005,6 +3053,14 @@ export function resolveNpmDebugLogDirs(homeDir, env = process.env, platform = pr return [...new Set(logDirs)]; } +function resolveNpmConfiguredPath(homeDir, value, platform) { + const raw = String(value ?? "").trim(); + if (!raw) { + return ""; + } + return platform === "win32" ? pathWin32.resolve(homeDir, raw) : resolve(homeDir, raw); +} + function normalizeNpmCacheLogDir(logDir) { return logDir.endsWith("/_logs") || logDir.endsWith("\\_logs") ? logDir : join(logDir, "_logs"); } diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index f8ef1683ef3c..35876fb64433 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -8,6 +8,7 @@ import { realpathSync, rmSync, symlinkSync, + utimesSync, writeFileSync, } from "node:fs"; import { createConnection as createNetConnection, createServer as createNetServer } from "node:net"; @@ -77,6 +78,7 @@ import { resolveNpmPackTarballFileName, resolveNpmDebugLogDirs, resolvePackDestinationTarball, + resolvePackageCandidatePackCommand, resolveProviderConfig, resolveDevUpdateVerificationRef, resolveInstalledPrefixDirFromCliPath, @@ -660,6 +662,33 @@ describe("scripts/openclaw-cross-os-release-checks", () => { } }); + it("falls back to pnpm pack for historical refs without the Docker package helper", () => { + const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-pack-command-")); + try { + const packDir = join(dir, "out"); + const fallback = resolvePackageCandidatePackCommand(dir, packDir); + + expect(fallback).toMatchObject({ + args: ["pack", "--config.ignore-scripts=true", "--json", "--pack-destination", packDir], + command: process.platform === "win32" ? "pnpm.cmd" : "pnpm", + kind: "pnpm-pack", + }); + + const helperPath = join(dir, "scripts", "package-openclaw-for-docker.mjs"); + mkdirSync(dirname(helperPath), { recursive: true }); + writeFileSync(helperPath, "export {};\n"); + const helper = resolvePackageCandidatePackCommand(dir, packDir); + + expect(helper).toMatchObject({ + args: [helperPath, "--skip-build", "--source-dir", dir, "--output-dir", packDir], + command: process.execPath, + kind: "docker-helper", + }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("keeps the Windows packaged-upgrade fallback install out of npm lifecycle scripts", () => { const source = readFileSync("scripts/openclaw-cross-os-release-checks.ts", "utf8"); const fallbackInstallSource = source.slice( @@ -1365,6 +1394,16 @@ describe("scripts/openclaw-cross-os-release-checks", () => { const logsDir = join(dir, "custom-logs"); const logPath = join(dir, "install.log"); mkdirSync(logsDir, { recursive: true }); + mkdirSync(join(homeDir, ".npm", "_logs"), { recursive: true }); + writeFileSync( + join(homeDir, ".npm", "_logs", "2026-07-05T00_00_00_000Z-debug-0.log"), + "old fallback log\n", + ); + utimesSync( + join(homeDir, ".npm", "_logs", "2026-07-05T00_00_00_000Z-debug-0.log"), + new Date("2020-01-01T00:00:00Z"), + new Date("2020-01-01T00:00:00Z"), + ); writeFileSync(join(logsDir, "2026-07-05T00_00_00_000Z-debug-0.log"), "custom log\n"); writeFileSync(logPath, "install failed\n"); @@ -1378,6 +1417,26 @@ describe("scripts/openclaw-cross-os-release-checks", () => { } }); + it("resolves relative npm log config from the install working directory", () => { + const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-npm-relative-logs-")); + try { + const homeDir = join(dir, "home"); + const logsDir = join(homeDir, "relative-logs"); + const cacheLogsDir = join(homeDir, "relative-cache", "_logs"); + mkdirSync(logsDir, { recursive: true }); + mkdirSync(cacheLogsDir, { recursive: true }); + + expect(resolveNpmDebugLogDirs(homeDir, { npm_config_logs_dir: "relative-logs" })).toContain( + logsDir, + ); + expect(resolveNpmDebugLogDirs(homeDir, { npm_config_cache: "relative-cache" })).toContain( + cacheLogsDir, + ); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + it("kills timed-out command process groups", async () => { if (process.platform === "win32") { return;