fix(release): verify npm tarball before publish

This commit is contained in:
Peter Steinberger
2026-05-15 12:42:56 +01:00
parent e79e5dbbdf
commit 6330fe607d
4 changed files with 218 additions and 0 deletions

View File

@@ -0,0 +1,107 @@
#!/usr/bin/env -S node --import tsx
import { execFileSync } from "node:child_process";
import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs";
import {
collectInstalledPackageErrors,
normalizeInstalledBinaryVersion,
resolveInstalledBinaryPath,
} from "./openclaw-npm-postpublish-verify.ts";
import { resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
type InstalledPackageJson = {
version?: string;
};
function npmExec(args: string[], cwd: string): string {
const invocation = resolveNpmCommandInvocation({
npmExecPath: process.env.npm_execpath,
nodeExecPath: process.execPath,
platform: process.platform,
});
return execFileSync(invocation.command, [...invocation.args, ...args], {
cwd,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
}
function main(): void {
const tarballPath = process.argv[2]?.trim();
const expectedVersion = process.argv[3]?.trim();
if (!tarballPath) {
throw new Error(
"Usage: node --import tsx scripts/openclaw-npm-prepublish-verify.ts <tarball.tgz> [expected-version]",
);
}
const workingDir = mkdtempSync(join(tmpdir(), "openclaw-prepublish-"));
const prefixDir = join(workingDir, "prefix");
try {
npmExec(
[
"install",
"-g",
"--prefix",
prefixDir,
realpathSync(tarballPath),
"--no-fund",
"--no-audit",
],
workingDir,
);
const globalRoot = npmExec(["root", "-g", "--prefix", prefixDir], workingDir);
const packageRoot = join(globalRoot, "openclaw");
const pkg = JSON.parse(
readFileSync(join(packageRoot, "package.json"), "utf8"),
) as InstalledPackageJson;
const resolvedExpectedVersion = expectedVersion || pkg.version?.trim() || "";
const errors = collectInstalledPackageErrors({
expectedVersion: resolvedExpectedVersion,
installedVersion: pkg.version?.trim() ?? "",
packageRoot,
});
const installedBinaryVersion = execFileSync(
resolveInstalledBinaryPath(prefixDir),
["--version"],
{
cwd: workingDir,
encoding: "utf8",
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
},
).trim();
if (normalizeInstalledBinaryVersion(installedBinaryVersion) !== resolvedExpectedVersion) {
errors.push(
`installed openclaw binary version mismatch: expected ${resolvedExpectedVersion}, found ${installedBinaryVersion || "<missing>"}.`,
);
}
if (errors.length === 0) {
runInstalledWorkspaceBootstrapSmoke({ packageRoot });
}
if (errors.length > 0) {
throw new Error(`prepared tarball install failed:\n- ${errors.join("\n- ")}`);
}
console.log(
`openclaw-npm-prepublish-verify: prepared tarball install OK (${resolvedExpectedVersion}).`,
);
} finally {
rmSync(workingDir, { force: true, recursive: true });
}
}
const entrypoint = process.argv[1] ? pathToFileURL(process.argv[1]).href : null;
if (entrypoint !== null && import.meta.url === entrypoint) {
try {
main();
} catch (error) {
console.error(`openclaw-npm-prepublish-verify: ${formatErrorMessage(error)}`);
process.exitCode = 1;
}
}

View File

@@ -38,6 +38,10 @@ import {
runInstalledWorkspaceBootstrapSmoke,
WORKSPACE_TEMPLATE_PACK_PATHS,
} from "./lib/workspace-bootstrap-smoke.mjs";
import {
collectInstalledPackageErrors,
normalizeInstalledBinaryVersion,
} from "./openclaw-npm-postpublish-verify.ts";
import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs";
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs";
@@ -330,6 +334,58 @@ function runPackedBundledPluginPostinstall(packageRoot: string): void {
});
}
export function collectPackedInstalledPackageVerificationErrors(params: {
expectedVersion: string;
installedBinaryVersion?: string;
packageRoot: string;
}): string[] {
const packageJson = JSON.parse(
readFileSync(join(params.packageRoot, "package.json"), "utf8"),
) as { version?: string };
const errors = collectInstalledPackageErrors({
expectedVersion: params.expectedVersion,
installedVersion: packageJson.version?.trim() ?? "",
packageRoot: params.packageRoot,
});
if (
params.installedBinaryVersion !== undefined &&
normalizeInstalledBinaryVersion(params.installedBinaryVersion) !== params.expectedVersion
) {
errors.push(
`installed openclaw binary version mismatch: expected ${params.expectedVersion}, found ${params.installedBinaryVersion || "<missing>"}.`,
);
}
return errors;
}
function verifyPackedInstalledPackage(params: {
expectedVersion: string;
packageRoot: string;
prefixDir: string;
tmpRoot: string;
}): void {
const installedBinaryVersion = execFileSync(
resolveInstalledBinaryPath(params.prefixDir),
["--version"],
{
cwd: params.tmpRoot,
encoding: "utf8",
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
},
).trim();
const errors = collectPackedInstalledPackageVerificationErrors({
expectedVersion: params.expectedVersion,
installedBinaryVersion,
packageRoot: params.packageRoot,
});
if (errors.length > 0) {
throw new Error(
`release-check: packed installed package verification failed:\n- ${errors.join("\n- ")}`,
);
}
}
export function writePackedBundledPluginActivationConfig(homeDir: string): void {
const configPath = join(homeDir, ".openclaw", "openclaw.json");
mkdirSync(join(homeDir, ".openclaw"), { recursive: true });
@@ -464,6 +520,14 @@ function runPackedCliSmoke(params: {
function runPackedBundledChannelEntrySmoke(): void {
const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-"));
try {
const expectedVersion = (
JSON.parse(readFileSync(resolve("package.json"), "utf8")) as {
version?: string;
}
).version;
if (!expectedVersion) {
throw new Error("release-check: root package.json is missing version.");
}
const packDir = join(tmpRoot, "pack");
mkdirSync(packDir);
@@ -473,6 +537,12 @@ function runPackedBundledChannelEntrySmoke(): void {
installPackedTarball(prefixDir, tarballPath, tmpRoot);
const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw");
verifyPackedInstalledPackage({
expectedVersion,
packageRoot,
prefixDir,
tmpRoot,
});
const homeDir = join(tmpRoot, "home");
const stateDir = join(tmpRoot, "state");
mkdirSync(homeDir, { recursive: true });