mirror of https://github.com/openclaw/openclaw.git
fix: verify global npm correction installs
This commit is contained in:
parent
50d996a6ec
commit
ce49d8bca9
|
|
@ -72,9 +72,20 @@ pnpm test:install:smoke
|
|||
For a non-root smoke path:
|
||||
|
||||
```bash
|
||||
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
|
||||
OPENCLAW_INSTALL_SMOKE_SKIP_NONROOT=1 pnpm test:install:smoke
|
||||
```
|
||||
|
||||
After npm publish, run:
|
||||
|
||||
```bash
|
||||
node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
|
||||
```
|
||||
|
||||
- This verifies the published registry install path in a fresh temp prefix.
|
||||
- For stable correction releases like `YYYY.M.D-N`, it also verifies the
|
||||
upgrade path from `YYYY.M.D` to `YYYY.M.D-N` so a correction publish cannot
|
||||
silently leave existing global installs on the old base stable payload.
|
||||
|
||||
## Check all relevant release builds
|
||||
|
||||
- Always validate the OpenClaw npm release path before creating the tag.
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ OpenClaw has three public release lanes:
|
|||
- Run `pnpm release:check` before every tagged release
|
||||
- Run `RELEASE_TAG=vYYYY.M.D node --import tsx scripts/openclaw-npm-release-check.ts`
|
||||
(or the matching beta/correction tag) before approval
|
||||
- After npm publish, run
|
||||
`node --import tsx scripts/openclaw-npm-postpublish-verify.ts YYYY.M.D`
|
||||
(or the matching beta/correction version) to verify the published registry
|
||||
install path in a fresh temp prefix
|
||||
- For stable correction releases like `YYYY.M.D-N`, the post-publish verifier
|
||||
also checks the same temp-prefix upgrade path from `YYYY.M.D` to `YYYY.M.D-N`
|
||||
so release corrections cannot silently leave older global installs on the
|
||||
base stable payload
|
||||
- npm release preflight fails closed unless the tarball includes both
|
||||
`dist/control-ui/index.html` and a non-empty `dist/control-ui/assets/` payload
|
||||
so we do not ship an empty browser dashboard again
|
||||
|
|
|
|||
|
|
@ -684,6 +684,7 @@
|
|||
"protocol:gen:swift": "node --import tsx scripts/protocol-gen-swift.ts",
|
||||
"release:check": "pnpm config:docs:check && pnpm plugin-sdk:api:check && node scripts/stage-bundled-plugin-runtime-deps.mjs && pnpm ui:build && node --import tsx scripts/release-check.ts",
|
||||
"release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts",
|
||||
"release:openclaw:npm:verify-published": "node --import tsx scripts/openclaw-npm-postpublish-verify.ts",
|
||||
"release:plugins:npm:check": "node --import tsx scripts/plugin-npm-release-check.ts",
|
||||
"release:plugins:npm:plan": "node --import tsx scripts/plugin-npm-release-plan.ts",
|
||||
"stage:bundled-plugin-runtime-deps": "node scripts/stage-bundled-plugin-runtime-deps.mjs",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
#!/usr/bin/env -S node --import tsx
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
|
||||
|
||||
const REQUIRED_RUNTIME_SIDECARS = [
|
||||
"dist/extensions/whatsapp/light-runtime-api.js",
|
||||
"dist/extensions/whatsapp/runtime-api.js",
|
||||
"dist/extensions/matrix/helper-api.js",
|
||||
"dist/extensions/matrix/runtime-api.js",
|
||||
"dist/extensions/matrix/thread-bindings-runtime.js",
|
||||
"dist/extensions/msteams/runtime-api.js",
|
||||
] as const;
|
||||
|
||||
type InstalledPackageJson = {
|
||||
version?: string;
|
||||
};
|
||||
|
||||
export type PublishedInstallScenario = {
|
||||
name: string;
|
||||
installSpecs: string[];
|
||||
expectedVersion: string;
|
||||
};
|
||||
|
||||
export function buildPublishedInstallScenarios(version: string): PublishedInstallScenario[] {
|
||||
const parsed = parseReleaseVersion(version);
|
||||
if (parsed === null) {
|
||||
throw new Error(`Unsupported release version "${version}".`);
|
||||
}
|
||||
|
||||
const exactSpec = `openclaw@${version}`;
|
||||
const scenarios: PublishedInstallScenario[] = [
|
||||
{
|
||||
name: "fresh-exact",
|
||||
installSpecs: [exactSpec],
|
||||
expectedVersion: version,
|
||||
},
|
||||
];
|
||||
|
||||
if (parsed.channel === "stable" && parsed.correctionNumber !== undefined) {
|
||||
scenarios.push({
|
||||
name: "upgrade-from-base-stable",
|
||||
installSpecs: [`openclaw@${parsed.baseVersion}`, exactSpec],
|
||||
expectedVersion: version,
|
||||
});
|
||||
}
|
||||
|
||||
return scenarios;
|
||||
}
|
||||
|
||||
export function collectInstalledPackageErrors(params: {
|
||||
expectedVersion: string;
|
||||
installedVersion: string;
|
||||
packageRoot: string;
|
||||
}): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (params.installedVersion !== params.expectedVersion) {
|
||||
errors.push(
|
||||
`installed package version mismatch: expected ${params.expectedVersion}, found ${params.installedVersion || "<missing>"}.`,
|
||||
);
|
||||
}
|
||||
|
||||
for (const relativePath of REQUIRED_RUNTIME_SIDECARS) {
|
||||
if (!existsSync(join(params.packageRoot, relativePath))) {
|
||||
errors.push(`installed package is missing required bundled runtime sidecar: ${relativePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
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 resolveGlobalRoot(prefixDir: string, cwd: string): string {
|
||||
return npmExec(["root", "-g", "--prefix", prefixDir], cwd);
|
||||
}
|
||||
|
||||
function installSpec(prefixDir: string, spec: string, cwd: string): void {
|
||||
npmExec(["install", "-g", "--prefix", prefixDir, spec, "--no-fund", "--no-audit"], cwd);
|
||||
}
|
||||
|
||||
function verifyScenario(version: string, scenario: PublishedInstallScenario): void {
|
||||
const workingDir = mkdtempSync(join(tmpdir(), `openclaw-postpublish-${scenario.name}.`));
|
||||
const prefixDir = join(workingDir, "prefix");
|
||||
|
||||
try {
|
||||
for (const spec of scenario.installSpecs) {
|
||||
installSpec(prefixDir, spec, workingDir);
|
||||
}
|
||||
|
||||
const globalRoot = resolveGlobalRoot(prefixDir, workingDir);
|
||||
const packageRoot = join(globalRoot, "openclaw");
|
||||
const pkg = JSON.parse(
|
||||
readFileSync(join(packageRoot, "package.json"), "utf8"),
|
||||
) as InstalledPackageJson;
|
||||
const errors = collectInstalledPackageErrors({
|
||||
expectedVersion: scenario.expectedVersion,
|
||||
installedVersion: pkg.version?.trim() ?? "",
|
||||
packageRoot,
|
||||
});
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(`${scenario.name} failed:\n- ${errors.join("\n- ")}`);
|
||||
}
|
||||
|
||||
console.log(`openclaw-npm-postpublish-verify: ${scenario.name} OK (${version})`);
|
||||
} finally {
|
||||
rmSync(workingDir, { force: true, recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
const version = process.argv[2]?.trim();
|
||||
if (!version) {
|
||||
throw new Error(
|
||||
"Usage: node --import tsx scripts/openclaw-npm-postpublish-verify.ts <version>",
|
||||
);
|
||||
}
|
||||
|
||||
const scenarios = buildPublishedInstallScenarios(version);
|
||||
for (const scenario of scenarios) {
|
||||
verifyScenario(version, scenario);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`openclaw-npm-postpublish-verify: verified published npm install paths for ${version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
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-postpublish-verify: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,14 @@ const pathExists = vi.fn();
|
|||
const syncPluginsForUpdateChannel = vi.fn();
|
||||
const updateNpmInstalledPlugins = vi.fn();
|
||||
const { defaultRuntime: runtimeCapture, resetRuntimeCapture } = createCliRuntimeCapture();
|
||||
const REQUIRED_BUNDLED_RUNTIME_SIDECARS = [
|
||||
"dist/extensions/whatsapp/light-runtime-api.js",
|
||||
"dist/extensions/whatsapp/runtime-api.js",
|
||||
"dist/extensions/matrix/helper-api.js",
|
||||
"dist/extensions/matrix/runtime-api.js",
|
||||
"dist/extensions/matrix/thread-bindings-runtime.js",
|
||||
"dist/extensions/msteams/runtime-api.js",
|
||||
] as const;
|
||||
|
||||
vi.mock("@clack/prompts", () => ({
|
||||
confirm,
|
||||
|
|
@ -615,6 +623,58 @@ describe("update-cli", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("fails package updates when the installed correction version does not match the requested target", async () => {
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
const nodeModules = path.join(tempDir, "node_modules");
|
||||
const pkgRoot = path.join(nodeModules, "openclaw");
|
||||
mockPackageInstallStatus(tempDir);
|
||||
await fs.mkdir(pkgRoot, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(pkgRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.3.23" }),
|
||||
"utf-8",
|
||||
);
|
||||
for (const relativePath of REQUIRED_BUNDLED_RUNTIME_SIDECARS) {
|
||||
const absolutePath = path.join(pkgRoot, relativePath);
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
|
||||
}
|
||||
readPackageVersion.mockResolvedValue("2026.3.23");
|
||||
pathExists.mockImplementation(async (candidate: string) =>
|
||||
REQUIRED_BUNDLED_RUNTIME_SIDECARS.some(
|
||||
(relativePath) => candidate === path.join(pkgRoot, relativePath),
|
||||
),
|
||||
);
|
||||
vi.mocked(runCommandWithTimeout).mockImplementation(async (argv) => {
|
||||
if (Array.isArray(argv) && argv[0] === "npm" && argv[1] === "root" && argv[2] === "-g") {
|
||||
return {
|
||||
stdout: nodeModules,
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
};
|
||||
}
|
||||
return {
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
code: 0,
|
||||
signal: null,
|
||||
killed: false,
|
||||
termination: "exit",
|
||||
};
|
||||
});
|
||||
|
||||
await updateCommand({ yes: true, tag: "2026.3.23-2" });
|
||||
|
||||
expect(defaultRuntime.exit).toHaveBeenCalledWith(1);
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
const logs = vi.mocked(defaultRuntime.log).mock.calls.map((call) => String(call[0]));
|
||||
expect(logs.join("\n")).toContain("global install verify");
|
||||
expect(logs.join("\n")).toContain("expected installed version 2026.3.23-2, found 2026.3.23");
|
||||
});
|
||||
|
||||
it("prepends portable Git PATH for package updates on Windows", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const tempDir = createCaseDir("openclaw-update");
|
||||
|
|
|
|||
|
|
@ -24,10 +24,12 @@ import {
|
|||
checkUpdateStatus,
|
||||
} from "../../infra/update-check.js";
|
||||
import {
|
||||
collectInstalledGlobalPackageErrors,
|
||||
canResolveRegistryVersionForPackageTarget,
|
||||
createGlobalInstallEnv,
|
||||
cleanupGlobalRenameDirs,
|
||||
globalInstallArgs,
|
||||
resolveExpectedInstalledVersionFromSpec,
|
||||
resolveGlobalInstallSpec,
|
||||
resolveGlobalPackageRoot,
|
||||
} from "../../infra/update-global.js";
|
||||
|
|
@ -343,9 +345,27 @@ async function runPackageInstallUpdate(params: {
|
|||
const steps = [updateStep];
|
||||
let afterVersion = beforeVersion;
|
||||
|
||||
if (pkgRoot) {
|
||||
afterVersion = await readPackageVersion(pkgRoot);
|
||||
const entryPath = path.join(pkgRoot, "dist", "entry.js");
|
||||
const verifiedPackageRoot =
|
||||
(await resolveGlobalPackageRoot(manager, runCommand, params.timeoutMs)) ?? pkgRoot;
|
||||
if (verifiedPackageRoot) {
|
||||
afterVersion = await readPackageVersion(verifiedPackageRoot);
|
||||
const expectedVersion = resolveExpectedInstalledVersionFromSpec(packageName, installSpec);
|
||||
const verificationErrors = await collectInstalledGlobalPackageErrors({
|
||||
packageRoot: verifiedPackageRoot,
|
||||
expectedVersion,
|
||||
});
|
||||
if (verificationErrors.length > 0) {
|
||||
steps.push({
|
||||
name: "global install verify",
|
||||
command: `verify ${verifiedPackageRoot}`,
|
||||
cwd: verifiedPackageRoot,
|
||||
durationMs: 0,
|
||||
exitCode: 1,
|
||||
stderrTail: verificationErrors.join("\n"),
|
||||
stdoutTail: null,
|
||||
});
|
||||
}
|
||||
const entryPath = path.join(verifiedPackageRoot, "dist", "entry.js");
|
||||
if (await pathExists(entryPath)) {
|
||||
const doctorStep = await runUpdateStep({
|
||||
name: `${CLI_NAME} doctor`,
|
||||
|
|
@ -361,7 +381,7 @@ async function runPackageInstallUpdate(params: {
|
|||
return {
|
||||
status: failedStep ? "error" : "ok",
|
||||
mode: manager,
|
||||
root: pkgRoot ?? params.root,
|
||||
root: verifiedPackageRoot ?? params.root,
|
||||
reason: failedStep ? failedStep.name : undefined,
|
||||
before: { version: beforeVersion },
|
||||
after: { version: afterVersion },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pathExists } from "../utils.js";
|
||||
import { readPackageVersion } from "./package-json.js";
|
||||
import { applyPathPrepend } from "./path-prepend.js";
|
||||
|
||||
export type GlobalInstallManager = "npm" | "pnpm" | "bun";
|
||||
|
|
@ -15,6 +16,14 @@ const PRIMARY_PACKAGE_NAME = "openclaw";
|
|||
const ALL_PACKAGE_NAMES = [PRIMARY_PACKAGE_NAME] as const;
|
||||
const GLOBAL_RENAME_PREFIX = ".";
|
||||
export const OPENCLAW_MAIN_PACKAGE_SPEC = "github:openclaw/openclaw#main";
|
||||
const REQUIRED_BUNDLED_RUNTIME_SIDECARS = [
|
||||
"dist/extensions/whatsapp/light-runtime-api.js",
|
||||
"dist/extensions/whatsapp/runtime-api.js",
|
||||
"dist/extensions/matrix/helper-api.js",
|
||||
"dist/extensions/matrix/runtime-api.js",
|
||||
"dist/extensions/matrix/thread-bindings-runtime.js",
|
||||
"dist/extensions/msteams/runtime-api.js",
|
||||
] as const;
|
||||
const NPM_GLOBAL_INSTALL_QUIET_FLAGS = ["--no-fund", "--no-audit", "--loglevel=error"] as const;
|
||||
const NPM_GLOBAL_INSTALL_OMIT_OPTIONAL_FLAGS = [
|
||||
"--omit=optional",
|
||||
|
|
@ -41,6 +50,47 @@ export function isExplicitPackageInstallSpec(value: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function resolveExpectedInstalledVersionFromSpec(
|
||||
packageName: string,
|
||||
spec: string,
|
||||
): string | null {
|
||||
const normalizedPackageName = packageName.trim();
|
||||
const normalizedSpec = normalizePackageTarget(spec);
|
||||
if (!normalizedPackageName || !normalizedSpec.startsWith(`${normalizedPackageName}@`)) {
|
||||
return null;
|
||||
}
|
||||
const rawVersion = normalizedSpec.slice(normalizedPackageName.length + 1).trim();
|
||||
if (
|
||||
!rawVersion ||
|
||||
rawVersion.includes("/") ||
|
||||
rawVersion.includes(":") ||
|
||||
rawVersion.includes("#") ||
|
||||
/^(latest|beta|next|main)$/i.test(rawVersion)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return rawVersion;
|
||||
}
|
||||
|
||||
export async function collectInstalledGlobalPackageErrors(params: {
|
||||
packageRoot: string;
|
||||
expectedVersion?: string | null;
|
||||
}): Promise<string[]> {
|
||||
const errors: string[] = [];
|
||||
const installedVersion = await readPackageVersion(params.packageRoot);
|
||||
if (params.expectedVersion && installedVersion !== params.expectedVersion) {
|
||||
errors.push(
|
||||
`expected installed version ${params.expectedVersion}, found ${installedVersion ?? "<missing>"}`,
|
||||
);
|
||||
}
|
||||
for (const relativePath of REQUIRED_BUNDLED_RUNTIME_SIDECARS) {
|
||||
if (!(await pathExists(path.join(params.packageRoot, relativePath)))) {
|
||||
errors.push(`missing bundled runtime sidecar ${relativePath}`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function canResolveRegistryVersionForPackageTarget(value: string): boolean {
|
||||
const trimmed = normalizePackageTarget(value);
|
||||
if (!trimmed) {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,14 @@ import { runGatewayUpdate } from "./update-runner.js";
|
|||
|
||||
type CommandResponse = { stdout?: string; stderr?: string; code?: number | null };
|
||||
type CommandResult = { stdout: string; stderr: string; code: number | null };
|
||||
const REQUIRED_BUNDLED_RUNTIME_SIDECARS = [
|
||||
"dist/extensions/whatsapp/light-runtime-api.js",
|
||||
"dist/extensions/whatsapp/runtime-api.js",
|
||||
"dist/extensions/matrix/helper-api.js",
|
||||
"dist/extensions/matrix/runtime-api.js",
|
||||
"dist/extensions/matrix/thread-bindings-runtime.js",
|
||||
"dist/extensions/msteams/runtime-api.js",
|
||||
] as const;
|
||||
|
||||
function createRunner(responses: Record<string, CommandResponse>) {
|
||||
const calls: string[] = [];
|
||||
|
|
@ -185,6 +193,7 @@ describe("runGatewayUpdate", () => {
|
|||
JSON.stringify({ name: "openclaw", version }),
|
||||
"utf-8",
|
||||
);
|
||||
await writeBundledRuntimeSidecars(pkgRoot);
|
||||
}
|
||||
|
||||
async function writeGlobalPackageVersion(pkgRoot: string, version = "2.0.0") {
|
||||
|
|
@ -193,6 +202,15 @@ describe("runGatewayUpdate", () => {
|
|||
JSON.stringify({ name: "openclaw", version }),
|
||||
"utf-8",
|
||||
);
|
||||
await writeBundledRuntimeSidecars(pkgRoot);
|
||||
}
|
||||
|
||||
async function writeBundledRuntimeSidecars(pkgRoot: string) {
|
||||
for (const relativePath of REQUIRED_BUNDLED_RUNTIME_SIDECARS) {
|
||||
const absolutePath = path.join(pkgRoot, relativePath);
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
async function createGlobalPackageFixture(rootDir: string) {
|
||||
|
|
@ -660,11 +678,7 @@ describe("runGatewayUpdate", () => {
|
|||
return { stdout: "", stderr: "node-gyp failed", code: 1 };
|
||||
},
|
||||
onOmitOptionalInstall: async () => {
|
||||
await fs.writeFile(
|
||||
path.join(pkgRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
||||
"utf-8",
|
||||
);
|
||||
await writeGlobalPackageVersion(pkgRoot);
|
||||
return { stdout: "ok", stderr: "", code: 0 };
|
||||
},
|
||||
});
|
||||
|
|
@ -680,6 +694,47 @@ describe("runGatewayUpdate", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("fails global npm update when the installed version misses the requested correction", async () => {
|
||||
const { calls, result } = await runNpmGlobalUpdateCase({
|
||||
expectedInstallCommand: "npm i -g openclaw@2026.3.23-2 --no-fund --no-audit --loglevel=error",
|
||||
tag: "2026.3.23-2",
|
||||
});
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("global install verify");
|
||||
expect(result.after?.version).toBe("2.0.0");
|
||||
expect(result.steps.at(-1)?.stderrTail).toContain(
|
||||
"expected installed version 2026.3.23-2, found 2.0.0",
|
||||
);
|
||||
expect(calls).toContain("npm i -g openclaw@2026.3.23-2 --no-fund --no-audit --loglevel=error");
|
||||
});
|
||||
|
||||
it("fails global npm update when bundled runtime sidecars are missing after install", async () => {
|
||||
const { nodeModules, pkgRoot } = await createGlobalPackageFixture(tempDir);
|
||||
const expectedInstallCommand = "npm i -g openclaw@latest --no-fund --no-audit --loglevel=error";
|
||||
const { runCommand } = createGlobalInstallHarness({
|
||||
pkgRoot,
|
||||
npmRootOutput: nodeModules,
|
||||
installCommand: expectedInstallCommand,
|
||||
onInstall: async () => {
|
||||
await fs.writeFile(
|
||||
path.join(pkgRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.rm(path.join(pkgRoot, "dist"), { recursive: true, force: true });
|
||||
},
|
||||
});
|
||||
|
||||
const result = await runWithCommand(runCommand, { cwd: pkgRoot });
|
||||
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("global install verify");
|
||||
expect(result.steps.at(-1)?.stderrTail).toContain(
|
||||
"missing bundled runtime sidecar dist/extensions/whatsapp/light-runtime-api.js",
|
||||
);
|
||||
});
|
||||
|
||||
it("prepends portable Git PATH for global Windows npm updates", async () => {
|
||||
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
|
||||
const localAppData = path.join(tempDir, "local-app-data");
|
||||
|
|
|
|||
|
|
@ -21,12 +21,15 @@ import {
|
|||
} from "./update-channels.js";
|
||||
import { compareSemverStrings } from "./update-check.js";
|
||||
import {
|
||||
collectInstalledGlobalPackageErrors,
|
||||
cleanupGlobalRenameDirs,
|
||||
createGlobalInstallEnv,
|
||||
detectGlobalInstallManagerForRoot,
|
||||
globalInstallArgs,
|
||||
globalInstallFallbackArgs,
|
||||
resolveExpectedInstalledVersionFromSpec,
|
||||
resolveGlobalInstallSpec,
|
||||
resolveGlobalPackageRoot,
|
||||
} from "./update-global.js";
|
||||
|
||||
export type UpdateStepResult = {
|
||||
|
|
@ -1045,12 +1048,34 @@ export async function runGatewayUpdate(opts: UpdateRunnerOptions = {}): Promise<
|
|||
}
|
||||
}
|
||||
|
||||
const afterVersion = await readPackageVersion(pkgRoot);
|
||||
const verifiedPackageRoot =
|
||||
(await resolveGlobalPackageRoot(globalManager, runCommand, timeoutMs)) ?? pkgRoot;
|
||||
const expectedVersion = resolveExpectedInstalledVersionFromSpec(packageName, spec);
|
||||
const verificationErrors = await collectInstalledGlobalPackageErrors({
|
||||
packageRoot: verifiedPackageRoot,
|
||||
expectedVersion,
|
||||
});
|
||||
if (verificationErrors.length > 0) {
|
||||
steps.push({
|
||||
name: "global install verify",
|
||||
command: `verify ${verifiedPackageRoot}`,
|
||||
cwd: verifiedPackageRoot,
|
||||
durationMs: 0,
|
||||
exitCode: 1,
|
||||
stderrTail: verificationErrors.join("\n"),
|
||||
});
|
||||
}
|
||||
const afterVersion = await readPackageVersion(verifiedPackageRoot);
|
||||
const failedStep =
|
||||
finalStep.exitCode !== 0
|
||||
? finalStep
|
||||
: (steps.find((step) => step.name === "global install verify" && step.exitCode !== 0) ??
|
||||
null);
|
||||
return {
|
||||
status: finalStep.exitCode === 0 ? "ok" : "error",
|
||||
status: failedStep ? "error" : "ok",
|
||||
mode: globalManager,
|
||||
root: pkgRoot,
|
||||
reason: finalStep.exitCode === 0 ? undefined : finalStep.name,
|
||||
root: verifiedPackageRoot,
|
||||
reason: failedStep ? failedStep.name : undefined,
|
||||
before: { version: beforeVersion },
|
||||
after: { version: afterVersion },
|
||||
steps,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildPublishedInstallScenarios,
|
||||
collectInstalledPackageErrors,
|
||||
} from "../scripts/openclaw-npm-postpublish-verify.ts";
|
||||
|
||||
describe("buildPublishedInstallScenarios", () => {
|
||||
it("uses a single fresh scenario for plain stable releases", () => {
|
||||
expect(buildPublishedInstallScenarios("2026.3.23")).toEqual([
|
||||
{
|
||||
name: "fresh-exact",
|
||||
installSpecs: ["openclaw@2026.3.23"],
|
||||
expectedVersion: "2026.3.23",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds a stable-to-correction upgrade scenario for correction releases", () => {
|
||||
expect(buildPublishedInstallScenarios("2026.3.23-2")).toEqual([
|
||||
{
|
||||
name: "fresh-exact",
|
||||
installSpecs: ["openclaw@2026.3.23-2"],
|
||||
expectedVersion: "2026.3.23-2",
|
||||
},
|
||||
{
|
||||
name: "upgrade-from-base-stable",
|
||||
installSpecs: ["openclaw@2026.3.23", "openclaw@2026.3.23-2"],
|
||||
expectedVersion: "2026.3.23-2",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("collectInstalledPackageErrors", () => {
|
||||
it("flags version mismatches and missing runtime sidecars", () => {
|
||||
expect(
|
||||
collectInstalledPackageErrors({
|
||||
expectedVersion: "2026.3.23-2",
|
||||
installedVersion: "2026.3.23",
|
||||
packageRoot: "/tmp/empty-openclaw",
|
||||
}),
|
||||
).toEqual([
|
||||
"installed package version mismatch: expected 2026.3.23-2, found 2026.3.23.",
|
||||
"installed package is missing required bundled runtime sidecar: dist/extensions/whatsapp/light-runtime-api.js",
|
||||
"installed package is missing required bundled runtime sidecar: dist/extensions/whatsapp/runtime-api.js",
|
||||
"installed package is missing required bundled runtime sidecar: dist/extensions/matrix/helper-api.js",
|
||||
"installed package is missing required bundled runtime sidecar: dist/extensions/matrix/runtime-api.js",
|
||||
"installed package is missing required bundled runtime sidecar: dist/extensions/matrix/thread-bindings-runtime.js",
|
||||
"installed package is missing required bundled runtime sidecar: dist/extensions/msteams/runtime-api.js",
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue