mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
fix(update): verify packaged dist inventory
This commit is contained in:
@@ -4,6 +4,7 @@ import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { formatErrorMessage } from "../src/infra/errors.ts";
|
||||
import { writePackageDistInventory } from "../src/infra/package-dist-inventory.ts";
|
||||
|
||||
const skipPrepackPreparedEnv = "OPENCLAW_PREPACK_PREPARED";
|
||||
const requiredPreparedPathGroups = [
|
||||
@@ -116,18 +117,24 @@ function runBuildSmoke(): void {
|
||||
run(process.execPath, ["scripts/test-built-bundled-channel-entry-smoke.mjs"]);
|
||||
}
|
||||
|
||||
function main(): void {
|
||||
async function writeDistInventory(): Promise<void> {
|
||||
await writePackageDistInventory(process.cwd());
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const pnpmCommand = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
|
||||
if (shouldSkipPrepack()) {
|
||||
ensurePreparedArtifacts();
|
||||
await writeDistInventory();
|
||||
runBuildSmoke();
|
||||
return;
|
||||
}
|
||||
run(pnpmCommand, ["build"]);
|
||||
run(pnpmCommand, ["ui:build"]);
|
||||
await writeDistInventory();
|
||||
runBuildSmoke();
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
main();
|
||||
await main();
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TEST_BUNDLED_RUNTIME_SIDECAR_PATHS } from "../../test/helpers/bundled-runtime-sidecars.js";
|
||||
import type { OpenClawConfig, ConfigFileSnapshot } from "../config/types.openclaw.js";
|
||||
import { writePackageDistInventory } from "../infra/package-dist-inventory.js";
|
||||
import type { UpdateRunResult } from "../infra/update-runner.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { createCliRuntimeCapture } from "./test-runtime-capture.js";
|
||||
@@ -785,12 +786,8 @@ describe("update-cli", () => {
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
|
||||
}
|
||||
await writePackageDistInventory(pkgRoot);
|
||||
readPackageVersion.mockResolvedValue("2026.3.23");
|
||||
pathExists.mockImplementation(async (candidate: string) =>
|
||||
TEST_BUNDLED_RUNTIME_SIDECAR_PATHS.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 {
|
||||
|
||||
45
src/infra/package-dist-inventory.test.ts
Normal file
45
src/infra/package-dist-inventory.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import {
|
||||
collectPackageDistInventoryErrors,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
writePackageDistInventory,
|
||||
} from "./package-dist-inventory.js";
|
||||
|
||||
describe("package dist inventory", () => {
|
||||
it("tracks missing and stale dist files", async () => {
|
||||
await withTempDir({ prefix: "openclaw-dist-inventory-" }, async (packageRoot) => {
|
||||
const currentFile = path.join(packageRoot, "dist", "current-BR6xv1a1.js");
|
||||
await fs.mkdir(path.dirname(currentFile), { recursive: true });
|
||||
await fs.writeFile(currentFile, "export {};\n", "utf8");
|
||||
|
||||
await expect(writePackageDistInventory(packageRoot)).resolves.toEqual([
|
||||
"dist/current-BR6xv1a1.js",
|
||||
]);
|
||||
await expect(collectPackageDistInventoryErrors(packageRoot)).resolves.toEqual([]);
|
||||
|
||||
await fs.rm(currentFile);
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "dist", "stale-CJUAgRQR.js"),
|
||||
"export {};\n",
|
||||
"utf8",
|
||||
);
|
||||
|
||||
await expect(collectPackageDistInventoryErrors(packageRoot)).resolves.toEqual([
|
||||
"missing packaged dist file dist/current-BR6xv1a1.js",
|
||||
"unexpected packaged dist file dist/stale-CJUAgRQR.js",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when the inventory is missing", async () => {
|
||||
await withTempDir({ prefix: "openclaw-dist-inventory-missing-" }, async (packageRoot) => {
|
||||
await fs.mkdir(path.join(packageRoot, "dist"), { recursive: true });
|
||||
await expect(collectPackageDistInventoryErrors(packageRoot)).resolves.toEqual([
|
||||
`missing package dist inventory ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
86
src/infra/package-dist-inventory.ts
Normal file
86
src/infra/package-dist-inventory.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export const PACKAGE_DIST_INVENTORY_RELATIVE_PATH = "dist/postinstall-inventory.json";
|
||||
|
||||
function normalizeRelativePath(value: string): string {
|
||||
return value.replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
async function collectRelativeFiles(rootDir: string, baseDir: string): Promise<string[]> {
|
||||
try {
|
||||
const entries = await fs.readdir(rootDir, { withFileTypes: true });
|
||||
const files = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
return await collectRelativeFiles(entryPath, baseDir);
|
||||
}
|
||||
if (entry.isFile() || entry.isSymbolicLink()) {
|
||||
const relativePath = normalizeRelativePath(path.relative(baseDir, entryPath));
|
||||
return relativePath === PACKAGE_DIST_INVENTORY_RELATIVE_PATH ? [] : [relativePath];
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
);
|
||||
return files.flat().toSorted((left, right) => left.localeCompare(right));
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return [];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectPackageDistInventory(packageRoot: string): Promise<string[]> {
|
||||
return await collectRelativeFiles(path.join(packageRoot, "dist"), packageRoot);
|
||||
}
|
||||
|
||||
export async function writePackageDistInventory(packageRoot: string): Promise<string[]> {
|
||||
const inventory = await collectPackageDistInventory(packageRoot);
|
||||
const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH);
|
||||
await fs.mkdir(path.dirname(inventoryPath), { recursive: true });
|
||||
await fs.writeFile(inventoryPath, `${JSON.stringify(inventory, null, 2)}\n`, "utf8");
|
||||
return inventory;
|
||||
}
|
||||
|
||||
export async function readPackageDistInventory(packageRoot: string): Promise<string[]> {
|
||||
const inventoryPath = path.join(packageRoot, PACKAGE_DIST_INVENTORY_RELATIVE_PATH);
|
||||
const raw = await fs.readFile(inventoryPath, "utf8");
|
||||
const parsed = JSON.parse(raw) as unknown;
|
||||
if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
|
||||
throw new Error(`Invalid package dist inventory at ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`);
|
||||
}
|
||||
return [...new Set(parsed.map(normalizeRelativePath))].toSorted((left, right) =>
|
||||
left.localeCompare(right),
|
||||
);
|
||||
}
|
||||
|
||||
export async function collectPackageDistInventoryErrors(packageRoot: string): Promise<string[]> {
|
||||
let expectedFiles: string[];
|
||||
try {
|
||||
expectedFiles = await readPackageDistInventory(packageRoot);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||
return [`missing package dist inventory ${PACKAGE_DIST_INVENTORY_RELATIVE_PATH}`];
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const actualFiles = await collectPackageDistInventory(packageRoot);
|
||||
const expectedSet = new Set(expectedFiles);
|
||||
const actualSet = new Set(actualFiles);
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const relativePath of expectedFiles) {
|
||||
if (!actualSet.has(relativePath)) {
|
||||
errors.push(`missing packaged dist file ${relativePath}`);
|
||||
}
|
||||
}
|
||||
for (const relativePath of actualFiles) {
|
||||
if (!expectedSet.has(relativePath)) {
|
||||
errors.push(`unexpected packaged dist file ${relativePath}`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { bundledDistPluginFile } from "../../test/helpers/bundled-plugin-paths.j
|
||||
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.js";
|
||||
import { withTempDir } from "../test-helpers/temp-dir.js";
|
||||
import { captureEnv } from "../test-utils/env.js";
|
||||
import { writePackageDistInventory } from "./package-dist-inventory.js";
|
||||
import {
|
||||
canResolveRegistryVersionForPackageTarget,
|
||||
collectInstalledGlobalPackageErrors,
|
||||
@@ -365,7 +366,7 @@ describe("update global helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("checks bundled runtime sidecars, including Matrix helper-api", async () => {
|
||||
it("checks installed dist against the packaged inventory", async () => {
|
||||
await withTempDir({ prefix: "openclaw-update-global-pkg-" }, async (packageRoot) => {
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "package.json"),
|
||||
@@ -377,12 +378,22 @@ describe("update global helpers", () => {
|
||||
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
|
||||
await fs.writeFile(absolutePath, "export {};\n", "utf-8");
|
||||
}
|
||||
await writePackageDistInventory(packageRoot);
|
||||
|
||||
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
|
||||
|
||||
await fs.rm(path.join(packageRoot, MATRIX_HELPER_API));
|
||||
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
|
||||
`missing bundled runtime sidecar ${MATRIX_HELPER_API}`,
|
||||
`missing packaged dist file ${MATRIX_HELPER_API}`,
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(packageRoot, "dist", "stale-CJUAgRQR.js"),
|
||||
"export {};\n",
|
||||
"utf8",
|
||||
);
|
||||
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
|
||||
"unexpected packaged dist file dist/stale-CJUAgRQR.js",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,9 @@ import fsSync from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.js";
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { pathExists } from "../utils.js";
|
||||
import { collectPackageDistInventoryErrors } from "./package-dist-inventory.js";
|
||||
import { readPackageVersion } from "./package-json.js";
|
||||
import { applyPathPrepend } from "./path-prepend.js";
|
||||
|
||||
@@ -89,11 +89,7 @@ export async function collectInstalledGlobalPackageErrors(params: {
|
||||
`expected installed version ${params.expectedVersion}, found ${installedVersion ?? "<missing>"}`,
|
||||
);
|
||||
}
|
||||
for (const relativePath of BUNDLED_RUNTIME_SIDECAR_PATHS) {
|
||||
if (!(await pathExists(path.join(params.packageRoot, relativePath)))) {
|
||||
errors.push(`missing bundled runtime sidecar ${relativePath}`);
|
||||
}
|
||||
}
|
||||
errors.push(...(await collectPackageDistInventoryErrors(params.packageRoot)));
|
||||
return errors;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../plugins/runtime-sidecar-paths.
|
||||
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import { pathExists } from "../utils.js";
|
||||
import { writePackageDistInventory } from "./package-dist-inventory.js";
|
||||
import { resolveStableNodePath } from "./stable-node-path.js";
|
||||
import { runGatewayUpdate } from "./update-runner.js";
|
||||
|
||||
@@ -231,6 +232,7 @@ describe("runGatewayUpdate", () => {
|
||||
"utf-8",
|
||||
);
|
||||
await writeBundledRuntimeSidecars(pkgRoot);
|
||||
await writePackageDistInventory(pkgRoot);
|
||||
}
|
||||
|
||||
async function writeGlobalPackageVersion(pkgRoot: string, version = "2.0.0") {
|
||||
@@ -240,6 +242,7 @@ describe("runGatewayUpdate", () => {
|
||||
"utf-8",
|
||||
);
|
||||
await writeBundledRuntimeSidecars(pkgRoot);
|
||||
await writePackageDistInventory(pkgRoot);
|
||||
}
|
||||
|
||||
async function writeBundledRuntimeSidecars(pkgRoot: string) {
|
||||
@@ -1176,7 +1179,9 @@ describe("runGatewayUpdate", () => {
|
||||
JSON.stringify({ name: "openclaw", version: "2.0.0" }),
|
||||
"utf-8",
|
||||
);
|
||||
await fs.rm(path.join(pkgRoot, "dist"), { recursive: true, force: true });
|
||||
await writeBundledRuntimeSidecars(pkgRoot);
|
||||
await writePackageDistInventory(pkgRoot);
|
||||
await fs.rm(path.join(pkgRoot, WHATSAPP_LIGHT_RUNTIME_API), { force: true });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1185,7 +1190,7 @@ describe("runGatewayUpdate", () => {
|
||||
expect(result.status).toBe("error");
|
||||
expect(result.reason).toBe("global install verify");
|
||||
expect(result.steps.at(-1)?.stderrTail).toContain(
|
||||
`missing bundled runtime sidecar ${WHATSAPP_LIGHT_RUNTIME_API}`,
|
||||
`missing packaged dist file ${WHATSAPP_LIGHT_RUNTIME_API}`,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user