release: mirror bundled channel deps at root (#63065)

Merged via squash.

Prepared head SHA: ac26799a54
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Co-authored-by: scoootscooob <167050519+scoootscooob@users.noreply.github.com>
Reviewed-by: @scoootscooob
This commit is contained in:
scoootscooob
2026-04-08 04:00:17 -07:00
committed by GitHub
parent 9bf3482470
commit d52d5ad6ff
11 changed files with 488 additions and 14 deletions

View File

@@ -1,9 +1,17 @@
#!/usr/bin/env -S node --import tsx
import { execFileSync } from "node:child_process";
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import {
existsSync,
lstatSync,
mkdtempSync,
readdirSync,
readFileSync,
realpathSync,
rmSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { isAbsolute, join, relative } from "node:path";
import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts";
import { BUNDLED_RUNTIME_SIDECAR_PATHS } from "../src/plugins/runtime-sidecar-paths.ts";
@@ -11,8 +19,27 @@ import { parseReleaseVersion, resolveNpmCommandInvocation } from "./openclaw-npm
type InstalledPackageJson = {
version?: string;
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
};
type InstalledBundledExtensionPackageJson = {
dependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
openclaw?: {
releaseChecks?: {
rootDependencyMirrorAllowlist?: unknown;
};
};
};
type InstalledBundledExtensionManifestRecord = {
manifest: InstalledBundledExtensionPackageJson;
path: string;
};
const MAX_BUNDLED_EXTENSION_MANIFEST_BYTES = 1024 * 1024;
export type PublishedInstallScenario = {
name: string;
installSpecs: string[];
@@ -64,6 +91,141 @@ export function collectInstalledPackageErrors(params: {
}
}
errors.push(...collectInstalledMirroredRootDependencyManifestErrors(params.packageRoot));
return errors;
}
export function resolveInstalledBinaryPath(prefixDir: string, platform = process.platform): string {
return platform === "win32"
? join(prefixDir, "openclaw.cmd")
: join(prefixDir, "bin", "openclaw");
}
function collectRuntimeDependencySpecs(packageJson: InstalledPackageJson): Map<string, string> {
return new Map([
...Object.entries(packageJson.dependencies ?? {}),
...Object.entries(packageJson.optionalDependencies ?? {}),
]);
}
function readBundledExtensionPackageJsons(packageRoot: string): {
manifests: InstalledBundledExtensionManifestRecord[];
errors: string[];
} {
const extensionsDir = join(packageRoot, "dist", "extensions");
if (!existsSync(extensionsDir)) {
return { manifests: [], errors: [] };
}
const manifests: InstalledBundledExtensionManifestRecord[] = [];
const errors: string[] = [];
for (const entry of readdirSync(extensionsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const extensionDirPath = join(extensionsDir, entry.name);
const packageJsonPath = join(extensionsDir, entry.name, "package.json");
if (!existsSync(packageJsonPath)) {
errors.push(`installed bundled extension manifest missing: ${packageJsonPath}.`);
continue;
}
try {
const packageJsonStats = lstatSync(packageJsonPath);
if (!packageJsonStats.isFile()) {
throw new Error("manifest must be a regular file");
}
if (packageJsonStats.size > MAX_BUNDLED_EXTENSION_MANIFEST_BYTES) {
throw new Error(`manifest exceeds ${MAX_BUNDLED_EXTENSION_MANIFEST_BYTES} bytes`);
}
const realExtensionDirPath = realpathSync(extensionDirPath);
const realPackageJsonPath = realpathSync(packageJsonPath);
const relativeManifestPath = relative(realExtensionDirPath, realPackageJsonPath);
if (
relativeManifestPath.length === 0 ||
relativeManifestPath.startsWith("..") ||
isAbsolute(relativeManifestPath)
) {
throw new Error("manifest resolves outside the bundled extension directory");
}
manifests.push({
manifest: JSON.parse(
readFileSync(realPackageJsonPath, "utf8"),
) as InstalledBundledExtensionPackageJson,
path: realPackageJsonPath,
});
} catch (error) {
errors.push(
`installed bundled extension manifest invalid: failed to parse ${packageJsonPath}: ${formatErrorMessage(error)}.`,
);
}
}
return { manifests, errors };
}
export function collectInstalledMirroredRootDependencyManifestErrors(
packageRoot: string,
): string[] {
const packageJsonPath = join(packageRoot, "package.json");
if (!existsSync(packageJsonPath)) {
return ["installed package is missing package.json."];
}
const rootPackageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as InstalledPackageJson;
const rootRuntimeDeps = collectRuntimeDependencySpecs(rootPackageJson);
const { manifests, errors } = readBundledExtensionPackageJsons(packageRoot);
for (const { manifest: extensionPackageJson } of manifests) {
const allowlist = extensionPackageJson.openclaw?.releaseChecks?.rootDependencyMirrorAllowlist;
if (allowlist === undefined) {
continue;
}
if (!Array.isArray(allowlist)) {
errors.push(
"installed bundled extension manifest invalid: openclaw.releaseChecks.rootDependencyMirrorAllowlist must be an array.",
);
continue;
}
const extensionRuntimeDeps = collectRuntimeDependencySpecs(extensionPackageJson);
for (const entry of allowlist) {
if (typeof entry !== "string" || entry.trim().length === 0) {
errors.push(
"installed bundled extension manifest invalid: openclaw.releaseChecks.rootDependencyMirrorAllowlist entries must be non-empty strings.",
);
continue;
}
const extensionSpec = extensionRuntimeDeps.get(entry);
if (!extensionSpec) {
errors.push(
`installed bundled extension manifest invalid: mirrored dependency '${entry}' must be declared in the extension runtime dependencies.`,
);
continue;
}
const rootSpec = rootRuntimeDeps.get(entry);
if (!rootSpec) {
errors.push(
`installed package is missing mirrored root runtime dependency '${entry}' required by a bundled extension.`,
);
continue;
}
if (rootSpec !== extensionSpec) {
errors.push(
`installed package mirrored dependency '${entry}' version mismatch: root '${rootSpec}', extension '${extensionSpec}'.`,
);
}
}
}
return errors;
}
@@ -89,6 +251,15 @@ function installSpec(prefixDir: string, spec: string, cwd: string): void {
npmExec(["install", "-g", "--prefix", prefixDir, spec, "--no-fund", "--no-audit"], cwd);
}
function readInstalledBinaryVersion(prefixDir: string, cwd: string): string {
return execFileSync(resolveInstalledBinaryPath(prefixDir), ["--version"], {
cwd,
encoding: "utf8",
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
}
function verifyScenario(version: string, scenario: PublishedInstallScenario): void {
const workingDir = mkdtempSync(join(tmpdir(), `openclaw-postpublish-${scenario.name}.`));
const prefixDir = join(workingDir, "prefix");
@@ -108,6 +279,13 @@ function verifyScenario(version: string, scenario: PublishedInstallScenario): vo
installedVersion: pkg.version?.trim() ?? "",
packageRoot,
});
const installedBinaryVersion = readInstalledBinaryVersion(prefixDir, workingDir);
if (installedBinaryVersion !== scenario.expectedVersion) {
errors.push(
`installed openclaw binary version mismatch: expected ${scenario.expectedVersion}, found ${installedBinaryVersion || "<missing>"}.`,
);
}
if (errors.length > 0) {
throw new Error(`${scenario.name} failed:\n- ${errors.join("\n- ")}`);