mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 06:20:44 +00:00
fix(release): verify npm tarball before publish
This commit is contained in:
14
.github/workflows/openclaw-npm-release.yml
vendored
14
.github/workflows/openclaw-npm-release.yml
vendored
@@ -287,6 +287,20 @@ jobs:
|
||||
NODE
|
||||
echo "dir=$ARTIFACT_DIR" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Verify prepared npm tarball install
|
||||
env:
|
||||
PREFLIGHT_ARTIFACT_DIR: ${{ steps.packed_tarball.outputs.dir }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TARBALL_PATH="$(find "$PREFLIGHT_ARTIFACT_DIR" -maxdepth 1 -type f -name '*.tgz' -print | sort | tail -n 1)"
|
||||
if [[ -z "$TARBALL_PATH" ]]; then
|
||||
echo "Prepared preflight tarball not found." >&2
|
||||
ls -la "$PREFLIGHT_ARTIFACT_DIR" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
|
||||
node --import tsx scripts/openclaw-npm-prepublish-verify.ts "$TARBALL_PATH" "$PACKAGE_VERSION"
|
||||
|
||||
- name: Upload dependency release evidence
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
|
||||
107
scripts/openclaw-npm-prepublish-verify.ts
Normal file
107
scripts/openclaw-npm-prepublish-verify.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
collectForbiddenPackPaths,
|
||||
collectMissingPackPaths,
|
||||
collectPackUnpackedSizeErrors,
|
||||
collectPackedInstalledPackageVerificationErrors,
|
||||
createPackedCompletionSmokeEnv,
|
||||
createPackedCliSmokeEnv,
|
||||
createPackedBundledPluginPostinstallEnv,
|
||||
@@ -533,6 +534,32 @@ describe("collectMissingPackPaths", () => {
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("runs postpublish package integrity checks against the packed install before publish", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "release-check-packed-install-"));
|
||||
try {
|
||||
const packageRoot = join(root, "openclaw");
|
||||
const distDir = join(packageRoot, "dist");
|
||||
mkdirSync(distDir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "package.json"),
|
||||
`${JSON.stringify({ name: "openclaw", version: "2026.5.14-beta.3", dependencies: {} })}\n`,
|
||||
);
|
||||
writeFileSync(join(distDir, "typescript-compiler.js"), "x".repeat(6 * 1024 * 1024 + 1));
|
||||
|
||||
expect(
|
||||
collectPackedInstalledPackageVerificationErrors({
|
||||
expectedVersion: "2026.5.14-beta.3",
|
||||
installedBinaryVersion: "openclaw 2026.5.14-beta.3",
|
||||
packageRoot,
|
||||
}),
|
||||
).toEqual([
|
||||
"installed package root dist file 'typescript-compiler.js' is invalid or exceeds 6291456 bytes.",
|
||||
]);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("requires bundled plugin runtime sidecars that dynamic plugin boundaries resolve at runtime", () => {
|
||||
expect(requiredBundledPluginPackPaths).not.toContain(
|
||||
bundledDistPluginFile("slack", "runtime-api.js"),
|
||||
|
||||
Reference in New Issue
Block a user