mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:40:43 +00:00
fix(release): harden package candidate checks
This commit is contained in:
@@ -55,6 +55,8 @@ const DIST_IMPORT_REFERENCE_ENTRYPOINTS = [
|
||||
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);
|
||||
const REQUIRED_PACKAGE_ENTRIES = ["dist/control-ui/index.html", "dist/postinstall-inventory.json"];
|
||||
const REQUIRED_PACKAGE_PREFIXES = ["dist/control-ui/assets/"];
|
||||
|
||||
const LEGACY_OMITTED_PRIVATE_QA_INVENTORY_PREFIXES = [
|
||||
"dist/extensions/qa-channel/",
|
||||
@@ -223,8 +225,15 @@ for (const forbiddenEntry of FORBIDDEN_LOCAL_BUILD_METADATA_FILES) {
|
||||
errors.push(`forbidden local build metadata tar entry ${forbiddenEntry}`);
|
||||
}
|
||||
}
|
||||
if (!entrySet.has("dist/postinstall-inventory.json")) {
|
||||
errors.push("missing dist/postinstall-inventory.json");
|
||||
for (const requiredEntry of REQUIRED_PACKAGE_ENTRIES) {
|
||||
if (!entrySet.has(requiredEntry)) {
|
||||
errors.push(`missing required package tar entry ${requiredEntry}`);
|
||||
}
|
||||
}
|
||||
for (const requiredPrefix of REQUIRED_PACKAGE_PREFIXES) {
|
||||
if (!normalized.some((entry) => entry.startsWith(requiredPrefix))) {
|
||||
errors.push(`missing required package tar entries under ${requiredPrefix}`);
|
||||
}
|
||||
}
|
||||
if (entrySet.has("dist/postinstall-inventory.json")) {
|
||||
try {
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
readlinkSync,
|
||||
realpathSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
@@ -1301,8 +1303,62 @@ export function resolveInstalledPrefixDirFromCliPath(cliPath, platform = process
|
||||
return dirname(dirname(resolvedCliPath));
|
||||
}
|
||||
|
||||
function readInstalledMetadataFromCliPath(cliPath, platform = process.platform) {
|
||||
return readInstalledMetadata(resolveInstalledPrefixDirFromCliPath(cliPath, platform));
|
||||
export function resolveInstalledPackageRootFromCliPath(cliPath, platform = process.platform) {
|
||||
const resolvedCliPath =
|
||||
platform === "win32" ? normalizeWindowsInstalledCliPath(cliPath) : String(cliPath ?? "");
|
||||
const packageRoots: string[] = [];
|
||||
const addPackageRoot = (candidate?: string) => {
|
||||
if (candidate && !packageRoots.includes(candidate)) {
|
||||
packageRoots.push(candidate);
|
||||
}
|
||||
};
|
||||
|
||||
addPackageRoot(
|
||||
installedPackageRoot(resolveInstalledPrefixDirFromCliPath(resolvedCliPath, platform)),
|
||||
);
|
||||
|
||||
if (platform !== "win32") {
|
||||
try {
|
||||
const realpath = realpathSync(resolvedCliPath);
|
||||
const marker = "/node_modules/openclaw/";
|
||||
const markerIndex = realpath.indexOf(marker);
|
||||
if (markerIndex >= 0) {
|
||||
addPackageRoot(realpath.slice(0, markerIndex + marker.length - 1));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const linkTarget = readlinkSync(resolvedCliPath);
|
||||
const absoluteTarget = linkTarget.startsWith("/")
|
||||
? linkTarget
|
||||
: join(dirname(resolvedCliPath), linkTarget);
|
||||
const marker = "/node_modules/openclaw/";
|
||||
const markerIndex = absoluteTarget.indexOf(marker);
|
||||
if (markerIndex >= 0) {
|
||||
addPackageRoot(absoluteTarget.slice(0, markerIndex + marker.length - 1));
|
||||
}
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const shim = readFileSync(resolvedCliPath, "utf8");
|
||||
const match = /(?<root>\/[^ "'\n\r]+\/node_modules\/openclaw)(?:\/|["'\s]|$)/u.exec(shim);
|
||||
addPackageRoot(match?.groups?.root);
|
||||
} catch {}
|
||||
|
||||
if (resolvedCliPath.endsWith("/.local/bin/openclaw")) {
|
||||
const homeDir = dirname(dirname(dirname(resolvedCliPath)));
|
||||
addPackageRoot(join(homeDir, ".npm-global", "lib", "node_modules", "openclaw"));
|
||||
}
|
||||
}
|
||||
|
||||
const found = packageRoots.find((packageRoot) => existsSync(join(packageRoot, "package.json")));
|
||||
return found ?? packageRoots[0];
|
||||
}
|
||||
|
||||
export function readInstalledMetadataFromCliPath(cliPath, platform = process.platform) {
|
||||
return readInstalledMetadataFromPackageRoot(
|
||||
resolveInstalledPackageRootFromCliPath(cliPath, platform),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveInstalledCliInvocation(cliPath, platform = process.platform) {
|
||||
@@ -2780,7 +2836,10 @@ async function runOpenClaw(params) {
|
||||
}
|
||||
|
||||
function readInstalledPackageManifest(prefixDir) {
|
||||
const packageRoot = installedPackageRoot(prefixDir);
|
||||
return readInstalledPackageManifestFromPackageRoot(installedPackageRoot(prefixDir));
|
||||
}
|
||||
|
||||
function readInstalledPackageManifestFromPackageRoot(packageRoot) {
|
||||
const packageJsonPath = join(packageRoot, "package.json");
|
||||
if (!existsSync(packageJsonPath)) {
|
||||
throw new Error(`Installed package manifest missing: ${packageJsonPath}`);
|
||||
@@ -2797,7 +2856,11 @@ export function readInstalledVersion(prefixDir) {
|
||||
}
|
||||
|
||||
function readInstalledMetadata(prefixDir) {
|
||||
const { packageJson, packageRoot } = readInstalledPackageManifest(prefixDir);
|
||||
return readInstalledMetadataFromPackageRoot(installedPackageRoot(prefixDir));
|
||||
}
|
||||
|
||||
function readInstalledMetadataFromPackageRoot(packageRoot) {
|
||||
const { packageJson } = readInstalledPackageManifestFromPackageRoot(packageRoot);
|
||||
const buildInfoPath = join(packageRoot, "dist", "build-info.json");
|
||||
if (!existsSync(buildInfoPath)) {
|
||||
throw new Error(`Installed build info missing: ${buildInfoPath}`);
|
||||
|
||||
@@ -115,6 +115,8 @@ async function main() {
|
||||
if (!options.skipBuild) {
|
||||
console.error("==> Building OpenClaw package artifacts");
|
||||
await run("pnpm", ["build"], sourceDir);
|
||||
console.error("==> Building OpenClaw Control UI assets");
|
||||
await run("pnpm", ["ui:build"], sourceDir);
|
||||
}
|
||||
|
||||
console.error("==> Writing OpenClaw package inventory");
|
||||
|
||||
@@ -12,6 +12,7 @@ function withTarball(
|
||||
files: Record<string, string>,
|
||||
testBody: (tarball: string) => void,
|
||||
version = "0.0.0",
|
||||
includeDefaultUi = true,
|
||||
) {
|
||||
const root = mkdtempSync(join(tmpdir(), "openclaw-package-tarball-test-"));
|
||||
try {
|
||||
@@ -22,7 +23,16 @@ function withTarball(
|
||||
join(packageRoot, "dist", "postinstall-inventory.json"),
|
||||
JSON.stringify(inventory),
|
||||
);
|
||||
for (const [relativePath, body] of Object.entries(files)) {
|
||||
const packageFiles = {
|
||||
...(includeDefaultUi
|
||||
? {
|
||||
"dist/control-ui/assets/index.js": "console.log('openclaw');\n",
|
||||
"dist/control-ui/index.html": '<!doctype html><div id="root"></div>\n',
|
||||
}
|
||||
: {}),
|
||||
...files,
|
||||
};
|
||||
for (const [relativePath, body] of Object.entries(packageFiles)) {
|
||||
const filePath = join(packageRoot, relativePath);
|
||||
mkdirSync(dirname(filePath), { recursive: true });
|
||||
writeFileSync(filePath, body);
|
||||
@@ -159,4 +169,24 @@ describe("check-openclaw-package-tarball", () => {
|
||||
"2026.4.26",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects tarballs missing Control UI assets", () => {
|
||||
withTarball(
|
||||
["dist/index.js"],
|
||||
{ "dist/index.js": "export {};\n" },
|
||||
(tarball) => {
|
||||
const result = spawnSync("node", [CHECK_SCRIPT, tarball], { encoding: "utf8" });
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
"missing required package tar entry dist/control-ui/index.html",
|
||||
);
|
||||
expect(result.stderr).toContain(
|
||||
"missing required package tar entries under dist/control-ui/assets/",
|
||||
);
|
||||
},
|
||||
"2026.4.27",
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,10 +28,12 @@ import {
|
||||
normalizeWindowsInstalledCliPath,
|
||||
parseArgs,
|
||||
packageHasScript,
|
||||
readInstalledMetadataFromCliPath,
|
||||
readInstalledVersion,
|
||||
readRunnerOverrideEnv,
|
||||
resolveExplicitBaselineVersion,
|
||||
resolveDevUpdateVerificationRef,
|
||||
resolveInstalledPackageRootFromCliPath,
|
||||
resolveInstalledPrefixDirFromCliPath,
|
||||
resolvePublishedInstallerUrl,
|
||||
resolveRequestedSuites,
|
||||
@@ -398,6 +400,33 @@ describe("scripts/openclaw-cross-os-release-checks", () => {
|
||||
).toBe("/Users/runner/.npm-global");
|
||||
});
|
||||
|
||||
it("resolves npm package metadata behind installer-local POSIX shims", () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), "openclaw-cross-os-installer-shim-"));
|
||||
try {
|
||||
const shimPath = join(dir, ".local", "bin", "openclaw");
|
||||
const packageRoot = join(dir, ".npm-global", "lib", "node_modules", "openclaw");
|
||||
mkdirSync(join(packageRoot, "dist"), { recursive: true });
|
||||
mkdirSync(join(dir, ".local", "bin"), { recursive: true });
|
||||
writeFileSync(
|
||||
join(packageRoot, "package.json"),
|
||||
JSON.stringify({ version: "2026.4.27-beta.1" }),
|
||||
);
|
||||
writeFileSync(
|
||||
join(packageRoot, "dist", "build-info.json"),
|
||||
JSON.stringify({ commit: "abc123" }),
|
||||
);
|
||||
writeFileSync(shimPath, `#!/bin/sh\nexec node "${packageRoot}/dist/entry.js" "$@"\n`);
|
||||
|
||||
expect(resolveInstalledPackageRootFromCliPath(shimPath, "linux")).toBe(packageRoot);
|
||||
expect(readInstalledMetadataFromCliPath(shimPath, "linux")).toEqual({
|
||||
commit: "abc123",
|
||||
version: "2026.4.27-beta.1",
|
||||
});
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("detects whether a managed gateway listener is still reachable on loopback", async () => {
|
||||
const server = createNetServer();
|
||||
await new Promise((resolvePromise) => {
|
||||
|
||||
Reference in New Issue
Block a user