mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 17:40:44 +00:00
fix(release): verify package entrypoint imports
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
// prebuilt package artifact with dist inventory, not a source checkout.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { LOCAL_BUILD_METADATA_DIST_PATHS } from "./lib/local-build-metadata-paths.mjs";
|
||||
|
||||
function usage() {
|
||||
@@ -39,6 +41,17 @@ const normalized = entries.map((entry) => entry.replace(/^package\//u, ""));
|
||||
const entrySet = new Set(normalized);
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const unsafeEntries = normalized.filter(
|
||||
(entry) => entry.startsWith("/") || entry.split("/").includes(".."),
|
||||
);
|
||||
const DIST_JS_IMPORT_SPECIFIER_PATTERN =
|
||||
/\b(?:import|export)\s+(?:(?:[^'"()]*?\s+from\s+)|)["'](?<staticSpecifier>[^"']+)["']|\bimport\s*\(\s*["'](?<dynamicSpecifier>[^"']+)["']\s*\)/gu;
|
||||
const DIST_IMPORT_REFERENCE_ENTRYPOINTS = [
|
||||
"dist/entry.js",
|
||||
"dist/cli/run-main.js",
|
||||
"dist/index.js",
|
||||
"dist/index.mjs",
|
||||
];
|
||||
const LEGACY_PACKAGE_ACCEPTANCE_COMPAT_MAX = { year: 2026, month: 4, day: 25 };
|
||||
const LEGACY_LOCAL_BUILD_METADATA_COMPAT_MAX = { year: 2026, month: 4, day: 26 };
|
||||
const FORBIDDEN_LOCAL_BUILD_METADATA_FILES = new Set(LOCAL_BUILD_METADATA_DIST_PATHS);
|
||||
@@ -117,10 +130,73 @@ function readTarEntry(entryPath) {
|
||||
return "";
|
||||
}
|
||||
|
||||
for (const entry of normalized) {
|
||||
if (entry.startsWith("/") || entry.split("/").includes("..")) {
|
||||
errors.push(`unsafe tar entry: ${entry}`);
|
||||
function isRelativeModuleSpecifier(value) {
|
||||
return value.startsWith("./") || value.startsWith("../");
|
||||
}
|
||||
|
||||
function normalizeModuleSpecifierTarget(value) {
|
||||
return value.split(/[?#]/u, 1)[0] ?? value;
|
||||
}
|
||||
|
||||
function normalizeTarPath(value) {
|
||||
return value.replace(/\\/gu, "/");
|
||||
}
|
||||
|
||||
function resolveTarImportTarget(importer, specifier) {
|
||||
const normalizedSpecifier = normalizeModuleSpecifierTarget(specifier);
|
||||
const base = normalizeTarPath(
|
||||
new URL(normalizedSpecifier, `file:///${importer}`).pathname.replace(/^\//u, ""),
|
||||
);
|
||||
const candidates = [
|
||||
base,
|
||||
`${base}.js`,
|
||||
`${base}.mjs`,
|
||||
`${base}.cjs`,
|
||||
`${base}/index.js`,
|
||||
`${base}/index.mjs`,
|
||||
`${base}/index.cjs`,
|
||||
];
|
||||
return candidates.find((candidate) => entrySet.has(candidate)) ?? null;
|
||||
}
|
||||
|
||||
function collectTarImportReferenceErrors() {
|
||||
if (unsafeEntries.length > 0) {
|
||||
return [];
|
||||
}
|
||||
const extractRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-package-check-"));
|
||||
const importErrors = [];
|
||||
try {
|
||||
const extract = spawnSync("tar", ["-xzf", tarball, "-C", extractRoot], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
if (extract.status !== 0) {
|
||||
return [
|
||||
`tar extraction failed for import reference check: ${extract.stderr || extract.status}`,
|
||||
];
|
||||
}
|
||||
|
||||
for (const entry of DIST_IMPORT_REFERENCE_ENTRYPOINTS.filter((entry) => entrySet.has(entry))) {
|
||||
const source = fs.readFileSync(path.join(extractRoot, "package", entry), "utf8");
|
||||
for (const match of source.matchAll(DIST_JS_IMPORT_SPECIFIER_PATTERN)) {
|
||||
const specifier = match.groups?.staticSpecifier ?? match.groups?.dynamicSpecifier ?? "";
|
||||
if (!isRelativeModuleSpecifier(specifier)) {
|
||||
continue;
|
||||
}
|
||||
if (resolveTarImportTarget(entry, specifier)) {
|
||||
continue;
|
||||
}
|
||||
importErrors.push(`missing packaged dist import target ${specifier} from ${entry}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(extractRoot, { recursive: true, force: true });
|
||||
}
|
||||
return importErrors.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
for (const entry of unsafeEntries) {
|
||||
errors.push(`unsafe tar entry: ${entry}`);
|
||||
}
|
||||
|
||||
if (!entrySet.has("package.json")) {
|
||||
@@ -182,6 +258,7 @@ if (entrySet.has("dist/postinstall-inventory.json")) {
|
||||
);
|
||||
}
|
||||
}
|
||||
errors.push(...collectTarImportReferenceErrors());
|
||||
|
||||
if (errors.length > 0) {
|
||||
fail(`OpenClaw package tarball integrity failed:\n${errors.join("\n")}`);
|
||||
|
||||
@@ -262,6 +262,75 @@ export async function readPackageDistInventoryIfPresent(
|
||||
}
|
||||
}
|
||||
|
||||
const DIST_JS_IMPORT_SPECIFIER_PATTERN =
|
||||
/\b(?:import|export)\s+(?:(?:[^'"()]*?\s+from\s+)|)["'](?<staticSpecifier>[^"']+)["']|\bimport\s*\(\s*["'](?<dynamicSpecifier>[^"']+)["']\s*\)/gu;
|
||||
const PACKAGE_DIST_IMPORT_REFERENCE_ENTRYPOINTS = [
|
||||
"dist/entry.js",
|
||||
"dist/cli/run-main.js",
|
||||
"dist/index.js",
|
||||
"dist/index.mjs",
|
||||
] as const;
|
||||
|
||||
function isRelativeModuleSpecifier(value: string): boolean {
|
||||
return value.startsWith("./") || value.startsWith("../");
|
||||
}
|
||||
|
||||
function normalizeModuleSpecifierTarget(value: string): string {
|
||||
return value.split(/[?#]/u, 1)[0] ?? value;
|
||||
}
|
||||
|
||||
function resolveDistImportTarget(importer: string, specifier: string, actualFiles: Set<string>) {
|
||||
const normalizedSpecifier = normalizeModuleSpecifierTarget(specifier);
|
||||
const base = normalizeRelativePath(
|
||||
path.posix.normalize(path.posix.join(path.posix.dirname(importer), normalizedSpecifier)),
|
||||
);
|
||||
const candidates = [
|
||||
base,
|
||||
`${base}.js`,
|
||||
`${base}.mjs`,
|
||||
`${base}.cjs`,
|
||||
`${base}/index.js`,
|
||||
`${base}/index.mjs`,
|
||||
`${base}/index.cjs`,
|
||||
];
|
||||
return candidates.find((candidate) => actualFiles.has(candidate)) ?? null;
|
||||
}
|
||||
|
||||
export async function collectPackageDistImportReferenceErrors(
|
||||
packageRoot: string,
|
||||
): Promise<string[]> {
|
||||
const actualFiles = new Set(await collectPackageDistInventory(packageRoot));
|
||||
const jsFiles = PACKAGE_DIST_IMPORT_REFERENCE_ENTRYPOINTS.filter((relativePath) =>
|
||||
actualFiles.has(relativePath),
|
||||
);
|
||||
const errors: string[] = [];
|
||||
|
||||
await Promise.all(
|
||||
jsFiles.map(async (relativePath) => {
|
||||
let source: string;
|
||||
try {
|
||||
source = await fs.readFile(path.join(packageRoot, relativePath), "utf8");
|
||||
} catch {
|
||||
errors.push(`unable to read packaged dist file ${relativePath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(DIST_JS_IMPORT_SPECIFIER_PATTERN)) {
|
||||
const specifier = match.groups?.staticSpecifier ?? match.groups?.dynamicSpecifier ?? "";
|
||||
if (!isRelativeModuleSpecifier(specifier)) {
|
||||
continue;
|
||||
}
|
||||
if (resolveDistImportTarget(relativePath, specifier, actualFiles)) {
|
||||
continue;
|
||||
}
|
||||
errors.push(`missing packaged dist import target ${specifier} from ${relativePath}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return errors.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export async function collectPackageDistInventoryErrors(packageRoot: string): Promise<string[]> {
|
||||
const expectedFiles = await readPackageDistInventoryIfPresent(packageRoot);
|
||||
if (expectedFiles === null) {
|
||||
|
||||
@@ -457,6 +457,26 @@ describe("update global helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("checks installed dist import references during global verify", async () => {
|
||||
await withTempDir({ prefix: "openclaw-update-global-imports-" }, async (packageRoot) => {
|
||||
await writeGlobalPackageJson(packageRoot, "2026.4.27");
|
||||
const runMain = path.join(packageRoot, "dist", "cli", "run-main.js");
|
||||
await fs.mkdir(path.dirname(runMain), { recursive: true });
|
||||
await fs.writeFile(runMain, 'await import("../memory-state-CcqRgDZU.js");\n', "utf8");
|
||||
await writePackageDistInventory(packageRoot);
|
||||
|
||||
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toContain(
|
||||
"missing packaged dist import target ../memory-state-CcqRgDZU.js from dist/cli/run-main.js",
|
||||
);
|
||||
|
||||
const chunk = path.join(packageRoot, "dist", "memory-state-CcqRgDZU.js");
|
||||
await fs.writeFile(chunk, "export {};\n", "utf8");
|
||||
await writePackageDistInventory(packageRoot);
|
||||
|
||||
await expect(collectInstalledGlobalPackageErrors({ packageRoot })).resolves.toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
it("ignores bundled plugin install stages during installed dist verification", async () => {
|
||||
await withTempDir({ prefix: "openclaw-update-global-plugin-stage-" }, async (packageRoot) => {
|
||||
await writeGlobalPackageJson(packageRoot);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { pathExists } from "../utils.js";
|
||||
import {
|
||||
collectPackageDistInventory,
|
||||
collectPackageDistImportReferenceErrors,
|
||||
PACKAGE_DIST_INVENTORY_RELATIVE_PATH,
|
||||
readPackageDistInventoryIfPresent,
|
||||
} from "./package-dist-inventory.js";
|
||||
@@ -154,15 +155,17 @@ async function collectInstalledPackageDistErrors(params: {
|
||||
missingMessage: (relativePath) => `missing packaged dist file ${relativePath}`,
|
||||
unexpectedMessage: (relativePath) => `unexpected packaged dist file ${relativePath}`,
|
||||
});
|
||||
const importErrors = await collectPackageDistImportReferenceErrors(params.packageRoot);
|
||||
const inventorySet = new Set(inventoryFiles);
|
||||
const supplementalCriticalPaths = criticalPaths.filter(
|
||||
(relativePath) => !inventorySet.has(relativePath),
|
||||
);
|
||||
if (supplementalCriticalPaths.length === 0) {
|
||||
return inventoryErrors;
|
||||
return [...inventoryErrors, ...importErrors];
|
||||
}
|
||||
return [
|
||||
...inventoryErrors,
|
||||
...importErrors,
|
||||
...(await collectInstalledPathErrors({
|
||||
packageRoot: params.packageRoot,
|
||||
expectedFiles: supplementalCriticalPaths,
|
||||
|
||||
@@ -85,6 +85,36 @@ describe("check-openclaw-package-tarball", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects packaged JS imports that point at missing dist chunks", () => {
|
||||
withTarball(
|
||||
["dist/cli/run-main.js"],
|
||||
{ "dist/cli/run-main.js": 'await import("../memory-state-CcqRgDZU.js");\n' },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
"missing packaged dist import target ../memory-state-CcqRgDZU.js from dist/cli/run-main.js",
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("accepts packaged JS imports that resolve to shipped dist chunks", () => {
|
||||
withTarball(
|
||||
["dist/cli/run-main.js", "dist/memory-state-DwGdReW4.js"],
|
||||
{
|
||||
"dist/cli/run-main.js": 'await import("../memory-state-DwGdReW4.js");\n',
|
||||
"dist/memory-state-DwGdReW4.js": "export {};\n",
|
||||
},
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status, result.stderr).toBe(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects local build metadata entries in package tarballs", () => {
|
||||
withTarball(
|
||||
["dist/index.js", ...LOCAL_BUILD_METADATA_DIST_PATHS],
|
||||
|
||||
Reference in New Issue
Block a user