Release: fix npm release preflight under pnpm (#52985)

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
TheRipper
2026-03-24 00:51:09 +07:00
committed by GitHub
parent f9a7427e8e
commit 36d6ba55e3
2 changed files with 126 additions and 20 deletions

View File

@@ -2,6 +2,7 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { basename } from "node:path";
import { pathToFileURL } from "node:url";
type PackageJson = {
@@ -40,7 +41,6 @@ const CORRECTION_TAG_REGEX = /^(?<base>\d{4}\.[1-9]\d?\.[1-9]\d?)-(?<correction>
const EXPECTED_REPOSITORY_URL = "https://github.com/openclaw/openclaw";
const MAX_CALVER_DISTANCE_DAYS = 2;
const REQUIRED_PACKED_PATHS = ["dist/control-ui/index.html"];
const NPM_COMMAND = process.platform === "win32" ? "npm.cmd" : "npm";
function normalizeRepoUrl(value: unknown): string {
if (typeof value !== "string") {
@@ -288,15 +288,31 @@ function loadPackageJson(): PackageJson {
return JSON.parse(readFileSync("package.json", "utf8")) as PackageJson;
}
function runNpmCommand(args: string[]): string {
const npmExecPath = process.env.npm_execpath;
if (typeof npmExecPath === "string" && npmExecPath.length > 0) {
return execFileSync(process.execPath, [npmExecPath, ...args], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
function isNpmExecPath(value: string): boolean {
return /^npm(?:-cli)?(?:\.(?:c?js|cmd|exe))?$/.test(basename(value).toLowerCase());
}
export function resolveNpmCommandInvocation(
params: {
npmExecPath?: string;
nodeExecPath?: string;
platform?: NodeJS.Platform;
} = {},
): { command: string; args: string[] } {
const npmExecPath = params.npmExecPath ?? process.env.npm_execpath;
const nodeExecPath = params.nodeExecPath ?? process.execPath;
const npmCommand = (params.platform ?? process.platform) === "win32" ? "npm.cmd" : "npm";
if (typeof npmExecPath === "string" && npmExecPath.length > 0 && isNpmExecPath(npmExecPath)) {
return { command: nodeExecPath, args: [npmExecPath] };
}
return execFileSync(NPM_COMMAND, args, {
return { command: npmCommand, args: [] };
}
function runNpmCommand(args: string[]): string {
const invocation = resolveNpmCommandInvocation();
return execFileSync(invocation.command, [...invocation.args, ...args], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
@@ -312,16 +328,16 @@ type NpmPackResult = {
};
type ExecFailure = Error & {
stderr?: string | Buffer;
stdout?: string | Buffer;
stderr?: string | Uint8Array;
stdout?: string | Uint8Array;
};
function toTrimmedUtf8(value: string | Buffer | undefined): string {
function toTrimmedUtf8(value: string | Uint8Array | undefined): string {
if (typeof value === "string") {
return value.trim();
}
if (Buffer.isBuffer(value)) {
return value.toString("utf8").trim();
if (value instanceof Uint8Array) {
return new TextDecoder().decode(value).trim();
}
return "";
}
@@ -343,6 +359,32 @@ function describeExecFailure(error: unknown): string {
return details.join(" | ");
}
export function parseNpmPackJsonOutput(stdout: string): NpmPackResult[] | null {
const trimmed = stdout.trim();
if (!trimmed) {
return null;
}
const candidates = [trimmed];
const trailingArrayStart = trimmed.lastIndexOf("\n[");
if (trailingArrayStart !== -1) {
candidates.push(trimmed.slice(trailingArrayStart + 1).trim());
}
for (const candidate of candidates) {
try {
const parsed = JSON.parse(candidate) as unknown;
if (Array.isArray(parsed)) {
return parsed as NpmPackResult[];
}
} catch {
// Try the next candidate. npm lifecycle output can prepend non-JSON logs.
}
}
return null;
}
function collectPackedTarballErrors(): string[] {
const errors: string[] = [];
let stdout = "";
@@ -356,15 +398,11 @@ function collectPackedTarballErrors(): string[] {
return errors;
}
let parsed: unknown;
try {
parsed = JSON.parse(stdout);
} catch {
const packResults = parseNpmPackJsonOutput(stdout);
if (!packResults) {
errors.push("Failed to parse JSON output from `npm pack --json --dry-run`.");
return errors;
}
const packResults = Array.isArray(parsed) ? (parsed as NpmPackResult[]) : [];
const firstResult = packResults[0];
if (!firstResult || !Array.isArray(firstResult.files)) {
errors.push("`npm pack --json --dry-run` did not return a files list to validate.");