openclaw/scripts/release-check.ts

1469 lines
49 KiB
JavaScript
Executable file

#!/usr/bin/env -S node --import tsx
import { execFileSync, type ExecFileSyncOptions } from "node:child_process";
import {
copyFileSync,
existsSync,
lstatSync,
mkdtempSync,
mkdirSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import type { Dirent } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, posix, resolve, win32 } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { parseArgs } from "node:util";
import { extract } from "tar";
import { expectDefined } from "../packages/normalization-core/src/expect.js";
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../src/cli/completion-runtime.ts";
import { escapeRegExp } from "../src/shared/regexp.js";
import { checkCliBootstrapExternalImports } from "./check-cli-bootstrap-imports.mts";
import {
collectBundledExtensionManifestErrors,
type BundledExtension,
type ExtensionPackageJson as PackageJson,
} from "./lib/bundled-extension-manifest.ts";
import { reportLimitViolations } from "./lib/check-limits.mts";
import { GATEWAY_RUN_CHUNK_METADATA_VERSION } from "./lib/gateway-run-chunk-metadata.mts";
import { importToolingTypeScript } from "./lib/import-tooling-typescript.mts";
import { collectPackUnpackedSizeFindings } from "./lib/npm-pack-budget.mts";
import { readPositiveEnvInt } from "./lib/numeric-options.mjs";
import { isLegacyPluginDependencyInstallStagePath } from "./lib/package-dist-inventory.ts";
import { collectBundledPluginPackageDependencySpecs } from "./lib/plugin-package-dependencies.mts";
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mts";
import { resolveNpmRunner, type NpmRunnerParams } from "./npm-runner.mts";
import {
collectInstalledPackageErrors,
normalizeInstalledBinaryVersion,
resolvePublishedInstallSourceVerification,
} from "./openclaw-npm-postpublish-verify.ts";
import { assertPreparedOpenClawAiDependency } from "./openclaw-npm-prepublish-verify.ts";
import { parseNpmPackJsonOutput, type NpmPackResult } from "./openclaw-npm-release-check.ts";
import type * as OpenClawPrepack from "./openclaw-prepack.ts";
import type * as OpenClawPackage from "./package-openclaw-for-docker.mts";
import { resolvePnpmRunner } from "./pnpm-runner.mts";
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
type ReleaseCheckExecOptions = ExecFileSyncOptions & {
windowsVerbatimArguments?: boolean;
};
export const RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV =
"OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR";
type ReleaseCheckCommandInvocation = {
command: string;
args: string[];
env?: NodeJS.ProcessEnv;
shell?: boolean | string;
windowsVerbatimArguments?: boolean;
};
const forbiddenPrivateQaContentMarkers = [
"//#region extensions/qa-lab/",
"qa-channel/runtime-api.js",
"qa-channel.js",
"qa-channel-protocol.js",
"qa-lab/cli.js",
"qa-lab/runtime-api.js",
] as const;
const forbiddenPrivatePluginSdkDeclarationMarkers = [
"//#region src/agents/test-helpers/",
"//#region src/plugin-sdk/test-helpers/",
"//#region src/test-helpers/",
"//#region src/test-utils/",
] as const;
const forbiddenPrivateQaContentScanPrefixes = ["dist/"] as const;
const appcastPath = resolve("appcast.xml");
const laneBuildMin = 1_000_000_000;
const laneFloorAdoptionReleaseKey = 20260227;
const SAFE_UNIX_SMOKE_PATH = "/usr/bin:/bin";
const DEFAULT_RELEASE_CHECK_COMMAND_TIMEOUT_MS = 5 * 60 * 1000;
const DEFAULT_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES = 100 * 1024 * 1024;
const PACKAGE_TARBALL_VERIFIER_PATH = fileURLToPath(
new URL("./check-openclaw-package-tarball.mjs", import.meta.url),
);
export const MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES = 2 * 1024 * 1024;
const CRITICAL_PLUGIN_SDK_SIZE_CHECK_SPECIFIERS = [
"openclaw/plugin-sdk/core",
"openclaw/plugin-sdk/provider-entry",
"openclaw/plugin-sdk/runtime",
] as const;
const CRITICAL_PLUGIN_SDK_IMPORT_SMOKE_SPECIFIERS = ["openclaw/plugin-sdk/core"] as const;
export const PACKED_CLI_SMOKE_COMMANDS = [
["--help"],
["onboard", "--help"],
["doctor", "--help"],
["status", "--json", "--timeout", "1"],
["config", "schema"],
["models", "list", "--provider", "openai"],
] as const;
export const PACKED_BUNDLED_RUNTIME_DEPS_REPAIR_ARGS = [
"doctor",
"--fix",
"--non-interactive",
] as const;
export const PACKED_COMPLETION_SMOKE_ARGS = [
"completion",
"--write-state",
"--shell",
"zsh",
] as const;
// The checker owns fixture bytes; the target checkout supplies package metadata.
const PACKED_PLUGIN_SDK_TYPESCRIPT_SMOKE_FIXTURE = new URL(
"./fixtures/packed-plugin-sdk-type-smoke.ts",
import.meta.url,
);
const PACKED_PLUGIN_SDK_SETUP_CONSUMER_FIXTURE = new URL(
"./fixtures/packed-plugin-sdk-setup-consumer.ts",
import.meta.url,
);
const PACKED_PLUGIN_SDK_PROGRESS_CONSUMER_FIXTURE = new URL(
"./fixtures/packed-plugin-sdk-progress-consumer.ts",
import.meta.url,
);
const PACKED_PLUGIN_SDK_SETUP_SURFACE_OMISSION_VERSIONS = new Set([
"2026.7.33",
"2026.7.34",
"2026.7.35",
]);
export function packedPluginSdkMayOmitSetupSurface(packageVersion: string): boolean {
return PACKED_PLUGIN_SDK_SETUP_SURFACE_OMISSION_VERSIONS.has(packageVersion);
}
const PACKED_BUNDLED_CHANNEL_ENTRY_SMOKE_ENTRYPOINTS = [
"scripts/test-built-bundled-channel-entry-smoke.mts",
"scripts/test-built-bundled-channel-entry-smoke.mjs",
] as const;
export function resolvePackedBundledChannelEntrySmokeCommand(
fileExists: (path: string) => boolean = existsSync,
nodeExecPath = process.execPath,
) {
const entrypoint = PACKED_BUNDLED_CHANNEL_ENTRY_SMOKE_ENTRYPOINTS.find(fileExists);
if (!entrypoint) {
throw new Error(
"release-check: target does not provide scripts/test-built-bundled-channel-entry-smoke.mts or .mjs",
);
}
return {
command: nodeExecPath,
args: [...(entrypoint.endsWith(".mts") ? ["--import", "tsx"] : []), entrypoint],
};
}
export function runReleaseCheckCommand(
invocation: ReleaseCheckCommandInvocation,
options: {
cwd?: string;
encoding?: BufferEncoding;
env?: NodeJS.ProcessEnv;
maxBuffer?: number;
shell?: boolean | string;
stdio: "inherit" | ["ignore", "pipe", "pipe"];
timeoutMs?: number;
},
): string {
const execOptions: ReleaseCheckExecOptions = {
cwd: options.cwd,
encoding: options.encoding,
env: invocation.env ?? options.env,
killSignal: "SIGKILL",
maxBuffer:
options.maxBuffer ??
readPositiveEnvInt(
"OPENCLAW_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES",
process.env,
DEFAULT_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES,
),
shell: invocation.shell ?? options.shell,
stdio: options.stdio,
timeout:
options.timeoutMs ??
readPositiveEnvInt(
"OPENCLAW_RELEASE_CHECK_COMMAND_TIMEOUT_MS",
process.env,
DEFAULT_RELEASE_CHECK_COMMAND_TIMEOUT_MS,
),
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
};
const output: Buffer | string | null = execFileSync(
invocation.command,
invocation.args,
execOptions,
);
if (output == null) {
return "";
}
return typeof output === "string" ? output : output.toString("utf8");
}
export function collectSkillShellScriptExecutableErrors(rootDir = resolve(".")): string[] {
if (process.platform === "win32") {
return [];
}
const skillsDir = join(rootDir, "skills");
const errors: string[] = [];
let entries: Dirent[];
try {
entries = readdirSync(skillsDir, { withFileTypes: true });
} catch {
return [];
}
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const scriptsDir = join(skillsDir, entry.name, "scripts");
let scriptEntries: Dirent[];
try {
scriptEntries = readdirSync(scriptsDir, { withFileTypes: true });
} catch {
continue;
}
for (const scriptEntry of scriptEntries) {
if (!scriptEntry.isFile() || !scriptEntry.name.endsWith(".sh")) {
continue;
}
const scriptPath = join(scriptsDir, scriptEntry.name);
if ((statSync(scriptPath).mode & 0o111) === 0) {
errors.push(
`skill shell script is not executable: skills/${entry.name}/scripts/${scriptEntry.name}`,
);
}
}
}
return errors;
}
function collectBundledExtensions(): BundledExtension[] {
const extensionsDir = resolve("extensions");
const entries = readdirSync(extensionsDir, { withFileTypes: true }).filter((entry) =>
entry.isDirectory(),
);
return entries.flatMap((entry) => {
const packagePath = join(extensionsDir, entry.name, "package.json");
try {
return [
{
id: entry.name,
packageJson: JSON.parse(readFileSync(packagePath, "utf8")) as PackageJson,
},
];
} catch {
return [];
}
});
}
function checkBundledExtensionMetadata() {
const extensions = collectBundledExtensions();
const manifestErrors = collectBundledExtensionManifestErrors(extensions);
const bundledPackageDependencySpecs = collectBundledPluginPackageDependencySpecs(
resolve("extensions"),
);
const dependencyConflictErrors = [...bundledPackageDependencySpecs.entries()]
.flatMap(([dependencyName, record]) =>
record.conflicts.map(
(conflict) =>
`bundled plugin package dependency '${dependencyName}' has conflicting specs: ${record.pluginIds.join(", ")} use '${record.spec}', ${conflict.pluginId} uses '${conflict.spec}'.`,
),
)
.toSorted((left, right) => left.localeCompare(right));
checkValidationErrors("bundled extension manifest", [
...manifestErrors,
...dependencyConflictErrors,
]);
}
function checkValidationErrors(label: string, errors: string[]) {
if (errors.length > 0) {
console.error(`release-check: ${label} validation failed:`);
for (const error of errors) {
console.error(` - ${error}`);
}
process.exit(1);
}
}
export function resolveReleaseNpmCommand(
args: string[],
params: Omit<NpmRunnerParams, "npmArgs"> = {},
) {
return resolveNpmRunner({ ...params, npmArgs: args });
}
function execNpm(
args: string[],
options: {
cwd?: string;
encoding: BufferEncoding;
env?: NodeJS.ProcessEnv;
maxBuffer?: number;
stdio: "inherit" | ["ignore", "pipe", "pipe"];
},
): string {
const invocation = resolveReleaseNpmCommand(args, { env: options.env ?? process.env });
return runReleaseCheckCommand(invocation, options);
}
function execPnpm(
args: string[],
options: {
cwd?: string;
encoding: BufferEncoding;
env?: NodeJS.ProcessEnv;
maxBuffer?: number;
stdio: "inherit" | ["ignore", "pipe", "pipe"];
},
): string {
const invocation = resolvePnpmRunner({ env: options.env ?? process.env, pnpmArgs: args });
return runReleaseCheckCommand(invocation, options);
}
function runPack(packDestination: string, cwd?: string): NpmPackResult[] {
const raw = execPnpm(["pack", "--json", "--pack-destination", packDestination], {
cwd,
encoding: "utf8",
env: { ...process.env, OPENCLAW_PREPACK_PREPARED: "1" },
stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 1024 * 1024 * 100,
});
return expectDefined(parseNpmPackJsonOutput(raw), "pnpm pack package receipt");
}
async function packRootPackage(packDestination: string): Promise<string> {
// Supplied-tarball tooling must not load the source packer's lifecycle or signal handlers.
const { collectPreparedPrepackErrorsFromDisk, resolvePrepackAllowUnreleasedChangelog } =
(await importToolingTypeScript(
new URL("./openclaw-prepack.ts", import.meta.url).href,
import.meta.url,
)) as typeof OpenClawPrepack;
// The canonical packer skips package hooks; retain prepared prepack's compatibility gate.
execPnpm(["update:compat:check"], { encoding: "utf8", stdio: "inherit" });
const errors = collectPreparedPrepackErrorsFromDisk();
if (errors.length > 0) {
throw new Error(
`release-check: prepared artifact validation failed:\n- ${errors.join("\n- ")}`,
);
}
const { packOpenClawPackageForDocker } = (await importToolingTypeScript(
new URL("./package-openclaw-for-docker.mts", import.meta.url).href,
import.meta.url,
)) as typeof OpenClawPackage;
return await packOpenClawPackageForDocker(process.cwd(), packDestination, {
allowUnreleasedChangelog: resolvePrepackAllowUnreleasedChangelog(),
});
}
export function resolvePackedTarballPath(
packDestination: string,
results: NpmPackResult[],
): string {
const filenames = results
.map((entry) => entry.filename)
.filter((filename): filename is string => typeof filename === "string" && filename.length > 0);
if (filenames.length !== 1) {
throw new Error(
`release-check: npm pack produced ${filenames.length} tarballs; expected exactly one.`,
);
}
const filename = expectDefined(filenames[0], "npm pack tarball filename");
const filenameBasename = basename(filename);
const resolvedDestination = resolve(packDestination);
const resolvedTarball = resolve(resolvedDestination, filenameBasename);
const isLocalFilename = filename === filenameBasename;
const isAbsolutePathInDestination = resolve(filename) === resolvedTarball;
if (
!filenameBasename.endsWith(".tgz") ||
filename.includes("\0") ||
filenameBasename !== win32.basename(filename) ||
(!isLocalFilename && !isAbsolutePathInDestination)
) {
throw new Error(
`release-check: npm pack reported unsafe tarball filename ${JSON.stringify(filename)}.`,
);
}
return resolvedTarball;
}
export function resolveReleaseCheckLocalPackageTarballs(
tarballDir: string | undefined = process.env[RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV],
requiresAi = rootPackageRequiresLocalAiTarball(),
): string[] {
if (!tarballDir) {
return [];
}
const resolvedDir = resolve(tarballDir);
if (!existsSync(resolvedDir) || !statSync(resolvedDir).isDirectory()) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} must name a directory.`,
);
}
const tarballs = readdirSync(resolvedDir, { withFileTypes: true })
.filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
.map((entry) => resolve(resolvedDir, entry.name))
.toSorted((left, right) => left.localeCompare(right));
const aiTarballs = tarballs.filter(
(tarballPath) => localPackageNameForTarball(tarballPath) === "@openclaw/ai",
);
const gatewayProtocolTarballs = tarballs.filter(
(tarballPath) => localPackageNameForTarball(tarballPath) === "@openclaw/gateway-protocol",
);
const gatewayClientTarballs = tarballs.filter(
(tarballPath) => localPackageNameForTarball(tarballPath) === "@openclaw/gateway-client",
);
const recognizedTarballs =
aiTarballs.length + gatewayProtocolTarballs.length + gatewayClientTarballs.length;
if (recognizedTarballs !== tarballs.length) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} contains an unsupported package tarball.`,
);
}
const expectedAiTarballs = requiresAi ? 1 : 0;
const aiTarballRequirement = requiresAi
? "exactly one @openclaw/ai tarball"
: "no @openclaw/ai tarballs";
if (
aiTarballs.length !== expectedAiTarballs ||
gatewayProtocolTarballs.length > 1 ||
gatewayClientTarballs.length > 1
) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} must contain ${aiTarballRequirement}, at most one @openclaw/gateway-protocol tarball, and at most one @openclaw/gateway-client tarball; found ${aiTarballs.length}, ${gatewayProtocolTarballs.length}, and ${gatewayClientTarballs.length}.`,
);
}
return tarballs;
}
function rootPackageRequiresLocalAiTarball(): boolean {
const packageJson = JSON.parse(readFileSync(resolve("package.json"), "utf8")) as {
dependencies?: Record<string, unknown>;
};
return typeof packageJson.dependencies?.["@openclaw/ai"] === "string";
}
function localPackageNameForTarball(tarballPath: string): string | undefined {
const filename = basename(tarballPath);
if (/^openclaw-ai(?:-.+)?\.tgz$/.test(filename)) {
return "@openclaw/ai";
}
if (/^openclaw-gateway-protocol(?:-.+)?\.tgz$/.test(filename)) {
return "@openclaw/gateway-protocol";
}
if (/^openclaw-gateway-client(?:-.+)?\.tgz$/.test(filename)) {
return "@openclaw/gateway-client";
}
return undefined;
}
export function prepareReleaseCheckLocalPackageTarballs(params: {
tmpRoot: string;
tarballDir?: string;
packLocalAi?: (packDestination: string) => NpmPackResult[];
}): string[] {
if (params.tarballDir) {
return resolveReleaseCheckLocalPackageTarballs(params.tarballDir);
}
if (!rootPackageRequiresLocalAiTarball()) {
return [];
}
// The root tarball requires the exact sibling AI version. Never fall back to
// registry bytes, which could silently validate an older publication.
const packDir = join(params.tmpRoot, "ai-pack");
mkdirSync(packDir);
const packResults = params.packLocalAi
? params.packLocalAi(packDir)
: runPack(packDir, resolve("packages/ai"));
return [resolvePackedTarballPath(packDir, packResults)];
}
export function createPackedTarballInstallArgs(prefixDir: string): string[] {
return ["install", "--prefix", prefixDir, "--no-audit", "--no-fund"];
}
export function writePackedTarballInstallManifest(
prefixDir: string,
tarballPath: string,
localPackageTarballs: string[],
requiresAi = rootPackageRequiresLocalAiTarball(),
): void {
const localPackages = localPackageTarballs.map((localPackageTarballPath) => ({
packageName: localPackageNameForTarball(localPackageTarballPath),
tarballPath: localPackageTarballPath,
}));
const aiTarballs = localPackages.filter(({ packageName }) => packageName === "@openclaw/ai");
const expectedAiTarballs = requiresAi ? 1 : 0;
const aiTarballRequirement = requiresAi
? "exactly one @openclaw/ai tarball"
: "no @openclaw/ai tarballs";
if (aiTarballs.length !== expectedAiTarballs) {
throw new Error(
`release-check: packed install requires ${aiTarballRequirement}; found ${aiTarballs.length}.`,
);
}
const unsupportedTarball = localPackages.find(({ packageName }) => !packageName);
if (unsupportedTarball) {
throw new Error(
`release-check: packed install received an unsupported package tarball: ${basename(unsupportedTarball.tarballPath)}.`,
);
}
const dependencies = Object.fromEntries(
localPackages.map(({ packageName, tarballPath: localPackageTarballPath }) => [
packageName as string,
pathToFileURL(localPackageTarballPath).href,
]),
);
if (Object.keys(dependencies).length !== localPackages.length) {
throw new Error("release-check: packed install received duplicate local package tarballs.");
}
dependencies.openclaw = pathToFileURL(tarballPath).href;
mkdirSync(prefixDir, { recursive: true });
writeFileSync(
join(prefixDir, "package.json"),
`${JSON.stringify(
{
private: true,
dependencies,
},
null,
2,
)}\n`,
);
}
function installPackedTarball(
prefixDir: string,
tarballPath: string,
cwd: string,
localPackageTarballs: string[],
): void {
writePackedTarballInstallManifest(prefixDir, tarballPath, localPackageTarballs);
execNpm(createPackedTarballInstallArgs(prefixDir), {
cwd,
encoding: "utf8",
env: { ...process.env, OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1" },
stdio: "inherit",
});
}
export function resolvePackedInstalledBinaryPath(
prefixDir: string,
platform: NodeJS.Platform = process.platform,
): string {
return join(
prefixDir,
"node_modules",
".bin",
platform === "win32" ? "openclaw.cmd" : "openclaw",
);
}
function resolvePackedInstalledBinaryCommandInvocation(
prefixDir: string,
args: string[],
): ReleaseCheckCommandInvocation {
const binaryPath = resolvePackedInstalledBinaryPath(prefixDir);
return process.platform === "win32"
? {
command: resolveWindowsCmdExePath(),
args: ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, args)],
windowsVerbatimArguments: true,
}
: { command: binaryPath, args };
}
export function createPackedCliSmokeEnv(
env: NodeJS.ProcessEnv,
overrides: NodeJS.ProcessEnv = {},
): NodeJS.ProcessEnv {
const allowlistedEnvEntries = [
"HOME",
"TMPDIR",
"TMP",
"TEMP",
"SystemRoot",
"ComSpec",
"PATHEXT",
"WINDIR",
] as const;
const windowsRoot = env.SystemRoot ?? env.WINDIR ?? "C:\\Windows";
const nodeBinDir = dirname(process.execPath);
const trustedCmdPath = join(windowsRoot, "System32", "cmd.exe");
const safePath =
process.platform === "win32"
? `${nodeBinDir};${windowsRoot}\\System32;${windowsRoot}`
: `${nodeBinDir}:${SAFE_UNIX_SMOKE_PATH}`;
const homeDir = overrides.HOME ?? env.HOME ?? env.USERPROFILE ?? "";
const stateDir = overrides.OPENCLAW_STATE_DIR;
return {
...Object.fromEntries(
allowlistedEnvEntries.flatMap((key) => {
const value = env[key];
return typeof value === "string" && value.length > 0 ? [[key, value]] : [];
}),
),
PATH: safePath,
HOME: homeDir,
USERPROFILE: homeDir,
ComSpec: trustedCmdPath,
APPDATA: homeDir ? join(homeDir, "AppData", "Roaming") : undefined,
LOCALAPPDATA: homeDir ? join(homeDir, "AppData", "Local") : undefined,
AWS_EC2_METADATA_DISABLED: "true",
AWS_SHARED_CREDENTIALS_FILE: homeDir ? join(homeDir, ".aws", "credentials") : undefined,
AWS_CONFIG_FILE: homeDir ? join(homeDir, ".aws", "config") : undefined,
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
OPENCLAW_NO_ONBOARD: "1",
OPENCLAW_SERVICE_REPAIR_POLICY: "external",
OPENCLAW_SUPPRESS_NOTES: "1",
...(typeof stateDir === "string" ? { OPENCLAW_STATE_DIR: stateDir } : {}),
};
}
export function createPackedCompletionSmokeEnv(
env: NodeJS.ProcessEnv,
overrides: NodeJS.ProcessEnv = {},
): NodeJS.ProcessEnv {
return {
...env,
...overrides,
OPENCLAW_SUPPRESS_NOTES: "1",
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
[COMPLETION_SKIP_PLUGIN_COMMANDS_ENV]: "1",
};
}
export function collectPackedInstalledPackageVerificationErrors(params: {
additionalCompanionManifestRoots?: string[];
allowLegacyGeneratedOwnership?: boolean;
expectedVersion: string;
installedBinaryVersion?: string;
packageRoot: string;
}): string[] {
const packageJson = JSON.parse(
readFileSync(join(params.packageRoot, "package.json"), "utf8"),
) as { version?: string };
const errors = collectInstalledPackageErrors({
additionalCompanionManifestRoots: params.additionalCompanionManifestRoots,
allowLegacyGeneratedOwnership: params.allowLegacyGeneratedOwnership,
expectedVersion: params.expectedVersion,
installedVersion: packageJson.version?.trim() ?? "",
packageRoot: params.packageRoot,
});
if (
params.installedBinaryVersion !== undefined &&
normalizeInstalledBinaryVersion(params.installedBinaryVersion) !== params.expectedVersion
) {
errors.push(
`installed openclaw binary version mismatch: expected ${params.expectedVersion}, found ${params.installedBinaryVersion || "<missing>"}.`,
);
}
return errors;
}
function verifyPackedInstalledPackage(params: {
expectedVersion: string;
packageRoot: string;
prefixDir: string;
tmpRoot: string;
}): void {
const invocation = resolvePackedInstalledBinaryCommandInvocation(params.prefixDir, ["--version"]);
const installedBinaryVersion = runReleaseCheckCommand(invocation, {
cwd: params.tmpRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
}).trim();
const errors = collectPackedInstalledPackageVerificationErrors({
...resolvePublishedInstallSourceVerification(resolve(), params.expectedVersion),
expectedVersion: params.expectedVersion,
installedBinaryVersion,
packageRoot: params.packageRoot,
});
if (errors.length > 0) {
throw new Error(
`release-check: packed installed package verification failed:\n- ${errors.join("\n- ")}`,
);
}
}
export function createPackedPluginSdkTypescriptSmokeProject(params: {
consumerDir: string;
packageSpec: string;
aiPackageSpec?: string;
progressConsumerOnly?: boolean;
}): void {
const dependencies: Record<string, string> = {
openclaw: params.packageSpec,
// Strict declaration checking needs the release-declared ws types; without
// them skipLibCheck:false reports TS7016 before the __exportAll TS2304.
"@types/ws": "8.18.1",
typescript: "7.0.2",
};
if (params.aiPackageSpec) {
dependencies["@openclaw/ai"] = params.aiPackageSpec;
}
mkdirSync(join(params.consumerDir, "src"), { recursive: true });
writeFileSync(
join(params.consumerDir, "package.json"),
`${JSON.stringify(
{
name: "openclaw-plugin-sdk-type-smoke",
private: true,
type: "module",
dependencies,
},
null,
2,
)}\n`,
"utf8",
);
writeFileSync(
join(params.consumerDir, "tsconfig.json"),
`${JSON.stringify(
{
compilerOptions: {
module: "NodeNext",
moduleResolution: "NodeNext",
noEmit: true,
strict: true,
skipLibCheck: false,
types: ["node"],
target: "ES2022",
},
include: params.progressConsumerOnly
? ["src/packed-plugin-sdk-progress-consumer.ts"]
: ["src/index.ts"],
},
null,
2,
)}\n`,
"utf8",
);
copyFileSync(
PACKED_PLUGIN_SDK_TYPESCRIPT_SMOKE_FIXTURE,
join(params.consumerDir, "src", "index.ts"),
);
copyFileSync(
PACKED_PLUGIN_SDK_SETUP_CONSUMER_FIXTURE,
join(params.consumerDir, "src", "packed-plugin-sdk-setup-consumer.ts"),
);
if (params.progressConsumerOnly) {
copyFileSync(
PACKED_PLUGIN_SDK_PROGRESS_CONSUMER_FIXTURE,
join(params.consumerDir, "src", "packed-plugin-sdk-progress-consumer.ts"),
);
}
}
function runPackedPluginSdkTypescriptSmoke(
tarballPath: string,
tmpRoot: string,
localPackageTarballs: string[],
): void {
const aiTarball = localPackageTarballs.find(
(localPackageTarball) => localPackageNameForTarball(localPackageTarball) === "@openclaw/ai",
);
for (const target of [
{
name: "plugin-sdk-released-setup-consumer",
packageSpec: "2026.9.4",
aiPackageSpec: undefined,
setupConsumerOnly: true,
},
{
name: "plugin-sdk-type-consumer",
packageSpec: `file:${tarballPath}`,
aiPackageSpec: aiTarball ? `file:${aiTarball}` : undefined,
setupConsumerOnly: false,
},
]) {
const consumerDir = join(tmpRoot, target.name);
createPackedPluginSdkTypescriptSmokeProject({ consumerDir, ...target });
if (target.setupConsumerOnly) {
copyFileSync(PACKED_PLUGIN_SDK_SETUP_CONSUMER_FIXTURE, join(consumerDir, "src", "index.ts"));
}
console.log(`release-check: compiling ${target.name} against ${target.packageSpec}`);
execNpm(["install", "--ignore-scripts", "--no-audit", "--no-fund"], {
cwd: consumerDir,
encoding: "utf8",
stdio: "inherit",
});
const installedOpenClawRoot = join(consumerDir, "node_modules", "openclaw");
if (!target.setupConsumerOnly) {
const installedPackageVersion = (
JSON.parse(readFileSync(join(installedOpenClawRoot, "package.json"), "utf8")) as {
version?: unknown;
}
).version;
if (
typeof installedPackageVersion === "string" &&
packedPluginSdkMayOmitSetupSurface(installedPackageVersion)
) {
const indexPath = join(consumerDir, "src", "index.ts");
writeFileSync(
indexPath,
readFileSync(indexPath, "utf8").replace(
'import "./packed-plugin-sdk-setup-consumer.js";\n',
"",
),
);
}
}
const tscPath = join(consumerDir, "node_modules", "typescript", "bin", "tsc");
if (!existsSync(tscPath)) {
throw new Error("release-check: packed plugin SDK TypeScript smoke could not find tsc.");
}
runReleaseCheckCommand(
{ command: process.execPath, args: [tscPath, "-p", "tsconfig.json", "--pretty", "false"] },
{
cwd: consumerDir,
stdio: "inherit",
},
);
console.log(`release-check: ${target.name} compiled successfully`);
}
}
export function writePackedBundledPluginActivationConfig(homeDir: string): void {
const configPath = join(homeDir, ".openclaw", "openclaw.json");
mkdirSync(join(homeDir, ".openclaw"), { recursive: true });
writeFileSync(
configPath,
`${JSON.stringify(
{
agents: {
defaults: {
model: { primary: "openai/gpt-5.6-luna" },
},
},
channels: {
telegram: {
enabled: true,
},
},
models: {
providers: {
openai: {
apiKey: "sk-openclaw-release-check",
baseUrl: "https://api.openai.com/v1",
models: [],
},
},
},
plugins: {
enabled: true,
entries: {
telegram: {
enabled: true,
},
},
},
},
null,
2,
)}\n`,
"utf8",
);
}
function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: string): void {
const homeDir = join(tmpRoot, "activation-home");
mkdirSync(homeDir, { recursive: true });
const env = createPackedCliSmokeEnv(process.env, {
HOME: homeDir,
});
writePackedBundledPluginActivationConfig(homeDir);
runReleaseCheckCommand(
{
command: process.execPath,
args: [join(packageRoot, "openclaw.mjs"), ...PACKED_BUNDLED_RUNTIME_DEPS_REPAIR_ARGS],
},
{
cwd: packageRoot,
stdio: "inherit",
env,
},
);
runReleaseCheckCommand(
{ command: process.execPath, args: [join(packageRoot, "openclaw.mjs"), "plugins", "doctor"] },
{
cwd: packageRoot,
stdio: "inherit",
env,
},
);
}
function runPackedCliSmoke(params: {
prefixDir: string;
cwd: string;
homeDir: string;
stateDir: string;
}): void {
const binaryPath = resolvePackedInstalledBinaryPath(params.prefixDir);
const env = createPackedCliSmokeEnv(process.env, {
HOME: params.homeDir,
OPENCLAW_STATE_DIR: params.stateDir,
});
const windowsRoot = env.SystemRoot ?? env.WINDIR ?? "C:\\Windows";
const trustedCmdPath = join(windowsRoot, "System32", "cmd.exe");
for (const args of PACKED_CLI_SMOKE_COMMANDS) {
if (process.platform === "win32") {
runReleaseCheckCommand(
{
command: trustedCmdPath,
args: ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, [...args])],
shell: false,
windowsVerbatimArguments: true,
},
{
cwd: params.cwd,
stdio: "inherit",
env,
},
);
continue;
}
runReleaseCheckCommand(
{ command: binaryPath, args: [...args], shell: false },
{
cwd: params.cwd,
stdio: "inherit",
env,
},
);
}
}
function runPackedBundledChannelEntrySmoke(tarballPath: string, packedRoot: string): void {
const tmpRoot = mkdtempSync(join(tmpdir(), "openclaw-release-pack-smoke-"));
try {
const expectedVersion = (
JSON.parse(readFileSync(resolve("package.json"), "utf8")) as {
version?: string;
}
).version;
if (!expectedVersion) {
throw new Error("release-check: root package.json is missing version.");
}
const prefixDir = join(tmpRoot, "prefix");
const localPackageTarballs = prepareReleaseCheckLocalPackageTarballs({
tmpRoot,
tarballDir: process.env[RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV],
});
const aiTarball = localPackageTarballs.find(
(localPackageTarball) => localPackageNameForTarball(localPackageTarball) === "@openclaw/ai",
);
if (aiTarball) {
assertPreparedOpenClawAiDependency({
aiManifest: JSON.parse(
execFileSync("tar", ["-xOf", aiTarball, "package/package.json"], {
encoding: "utf8",
maxBuffer: 1024 * 1024,
}),
),
rootManifest: JSON.parse(readFileSync(join(packedRoot, "package.json"), "utf8")),
});
}
// Separately published core packages must not conceal a missing root dependency.
installPackedTarball(prefixDir, tarballPath, tmpRoot, aiTarball ? [aiTarball] : []);
const packageRoot = join(prefixDir, "node_modules", "openclaw");
verifyPackedInstalledPackage({
expectedVersion,
packageRoot,
prefixDir,
tmpRoot,
});
const homeDir = join(tmpRoot, "home");
const stateDir = join(tmpRoot, "state");
mkdirSync(homeDir, { recursive: true });
runPackedCliSmoke({
prefixDir,
cwd: packageRoot,
homeDir,
stateDir,
});
runCriticalPluginSdkEntrypointImportSmoke(packageRoot);
runPackedBundledPluginActivationSmoke(packageRoot, tmpRoot);
runPackedPluginSdkTypescriptSmoke(tarballPath, tmpRoot, localPackageTarballs);
const bundledChannelEntrySmoke = resolvePackedBundledChannelEntrySmokeCommand();
runReleaseCheckCommand(
{
...bundledChannelEntrySmoke,
args: [...bundledChannelEntrySmoke.args, "--package-root", packageRoot],
},
{
stdio: "inherit",
env: {
...process.env,
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK: "1",
},
},
);
runReleaseCheckCommand(
{
command: process.execPath,
args: [join(packageRoot, "openclaw.mjs"), ...PACKED_COMPLETION_SMOKE_ARGS],
},
{
cwd: packageRoot,
stdio: "inherit",
env: createPackedCompletionSmokeEnv(process.env, {
HOME: homeDir,
OPENCLAW_STATE_DIR: stateDir,
}),
},
);
const completionFiles = readdirSync(join(stateDir, "completions")).filter(
(entry) => !entry.startsWith("."),
);
if (completionFiles.length === 0) {
throw new Error("release-check: packed completion smoke produced no completion files.");
}
runInstalledWorkspaceBootstrapSmoke({ packageRoot });
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
}
export function collectForbiddenPackPaths(paths: Iterable<string>): string[] {
return [...paths]
.filter(
(path) =>
isLegacyPluginDependencyInstallStagePath(path) ||
/(^|\/)\.openclaw-runtime-deps-[^/]+(\/|$)/u.test(path) ||
path.endsWith("/.openclaw-runtime-deps-stamp.json"),
)
.toSorted((left, right) => left.localeCompare(right));
}
export function collectForbiddenPackContentPaths(
paths: Iterable<string>,
rootDir = process.cwd(),
): string[] {
const textPathPattern = /\.(?:[cm]?js|d\.ts|json|md|mjs|cjs)$/u;
return [...paths]
.filter((packedPath) => {
if (!forbiddenPrivateQaContentScanPrefixes.some((prefix) => packedPath.startsWith(prefix))) {
return false;
}
if (!textPathPattern.test(packedPath)) {
return false;
}
let content: string;
try {
content = readFileSync(resolve(rootDir, packedPath), "utf8");
} catch {
return false;
}
return (
forbiddenPrivateQaContentMarkers.some((marker) => content.includes(marker)) ||
forbiddenPrivatePluginSdkDeclarationMarkers.some((marker) => content.includes(marker))
);
})
.toSorted((left, right) => left.localeCompare(right));
}
function extractTag(item: string, tag: string): string | null {
const escapedTag = escapeRegExp(tag);
const regex = new RegExp(`<${escapedTag}>([^<]+)</${escapedTag}>`);
return regex.exec(item)?.[1]?.trim() ?? null;
}
export function collectAppcastSparkleVersionErrors(xml: string): string[] {
const itemMatches = [...xml.matchAll(/<item>([\s\S]*?)<\/item>/g)];
const errors: string[] = [];
const calverItems: Array<{ title: string; sparkleBuild: number; floors: SparkleBuildFloors }> =
[];
if (itemMatches.length === 0) {
errors.push("appcast.xml contains no <item> entries.");
}
for (const [index, match] of itemMatches.entries()) {
const item = expectDefined(match[1], `appcast item body at index ${index}`);
const title = extractTag(item, "title") ?? "unknown";
const shortVersion = extractTag(item, "sparkle:shortVersionString");
const sparkleVersion = extractTag(item, "sparkle:version");
const sparkleChannel = extractTag(item, "sparkle:channel");
if (!sparkleVersion) {
errors.push(`appcast item '${title}' is missing sparkle:version.`);
continue;
}
if (!/^[0-9]+$/.test(sparkleVersion)) {
errors.push(`appcast item '${title}' has non-numeric sparkle:version '${sparkleVersion}'.`);
continue;
}
if (!shortVersion) {
continue;
}
if (/(?:^|[.-])beta(?:[.-]|$)/i.test(shortVersion) && sparkleChannel !== "beta") {
errors.push(`appcast item '${title}' must set sparkle:channel to 'beta'.`);
}
const floors = sparkleBuildFloorsFromShortVersion(shortVersion);
if (floors === null) {
errors.push(
`appcast item '${title}' has invalid sparkle:shortVersionString '${shortVersion}'.`,
);
continue;
}
calverItems.push({ title, sparkleBuild: Number(sparkleVersion), floors });
}
const observedLaneAdoptionReleaseKey = calverItems
.filter((item) => item.sparkleBuild >= laneBuildMin)
.map((item) => item.floors.releaseKey)
.toSorted((a, b) => a - b)[0];
const effectiveLaneAdoptionReleaseKey =
typeof observedLaneAdoptionReleaseKey === "number"
? Math.min(observedLaneAdoptionReleaseKey, laneFloorAdoptionReleaseKey)
: laneFloorAdoptionReleaseKey;
for (const item of calverItems) {
const expectLaneFloor =
item.sparkleBuild >= laneBuildMin ||
item.floors.releaseKey >= effectiveLaneAdoptionReleaseKey;
const floor = expectLaneFloor ? item.floors.laneFloor : item.floors.legacyFloor;
if (item.sparkleBuild < floor) {
const floorLabel = expectLaneFloor ? "lane floor" : "legacy floor";
errors.push(
`appcast item '${item.title}' has sparkle:version ${item.sparkleBuild} below ${floorLabel} ${floor}.`,
);
}
}
return errors;
}
// Critical functions that channel extension plugins import from openclaw/plugin-sdk.
// If any are missing from the compiled output, plugins crash at runtime (#27569).
const requiredPluginSdkExports = [
"isDangerousNameMatchingEnabled",
"createAccountListHelpers",
"buildAgentMediaPayload",
"createReplyPrefixOptions",
"createTypingCallbacks",
"logInboundDrop",
"logTypingFailure",
"buildPendingHistoryContextFromMap",
"clearHistoryEntriesIfEnabled",
"recordPendingHistoryEntryIfEnabled",
"resolveControlCommandGate",
"resolveDmGroupAccessWithLists",
"resolveAllowlistProviderRuntimeGroupPolicy",
"resolveDefaultGroupPolicy",
"resolveChannelMediaMaxBytes",
"warnMissingProviderGroupPolicyFallbackOnce",
"emptyPluginConfigSchema",
"onDiagnosticEvent",
"normalizePluginHttpPath",
"registerPluginHttpRoute",
"DEFAULT_ACCOUNT_ID",
"DEFAULT_GROUP_HISTORY_LIMIT",
];
function collectDistPluginSdkExports(rootDir: string): Set<string> {
const pluginSdkDir = join(rootDir, "dist", "plugin-sdk");
let entries: string[];
try {
entries = readdirSync(pluginSdkDir)
.filter((entry) => entry.endsWith(".js"))
.toSorted();
} catch {
throw new Error("release-check: packed dist/plugin-sdk directory not found.");
}
const exportedNames = new Set<string>();
for (const entry of entries) {
const content = readFileSync(join(pluginSdkDir, entry), "utf8");
for (const match of content.matchAll(/export\s*\{([^}]+)\}(?:\s*from\s*["'][^"']+["'])?/g)) {
const names = match[1]?.split(",") ?? [];
for (const name of names) {
const parts = name.trim().split(/\s+as\s+/);
const exportName = (parts[parts.length - 1] || "").trim();
if (exportName) {
exportedNames.add(exportName);
}
}
}
for (const match of content.matchAll(
/export\s+(?:const|function|class|let|var)\s+([A-Za-z0-9_$]+)/g,
)) {
const exportName = match[1]?.trim();
if (exportName) {
exportedNames.add(exportName);
}
}
}
return exportedNames;
}
function checkPluginSdkExports(rootDir: string) {
const exportedNames = collectDistPluginSdkExports(rootDir);
const missingExports = requiredPluginSdkExports.filter((name) => !exportedNames.has(name));
if (missingExports.length > 0) {
throw new Error(
`release-check: missing critical plugin-sdk exports (#27569):\n- ${missingExports.join("\n- ")}`,
);
}
}
export function collectCriticalPluginSdkEntrypointSizeFindings(rootDir = process.cwd()) {
const errors: string[] = [];
const violations: { file: string; title: string; message: string }[] = [];
for (const specifier of CRITICAL_PLUGIN_SDK_SIZE_CHECK_SPECIFIERS) {
const subpath = specifier.slice("openclaw/plugin-sdk/".length);
const relativePath = `dist/plugin-sdk/${subpath}.js`;
const filePath = resolve(rootDir, relativePath);
if (!existsSync(filePath)) {
errors.push(`${relativePath} is missing.`);
continue;
}
const stat = lstatSync(filePath);
if (!stat.isFile()) {
errors.push(`${relativePath} is not a file.`);
continue;
}
if (stat.size > MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES) {
violations.push({
file: `src/plugin-sdk/${subpath}.ts`,
title: "Plugin SDK entrypoint size budget",
message: `${relativePath} is ${stat.size} bytes, exceeding ${MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES} bytes. Keep public SDK package entrypoints lazy and avoid bundling compiler/runtime internals.`,
});
}
}
return { errors, violations };
}
function runCriticalPluginSdkEntrypointImportSmoke(packageRoot: string) {
const script = [
`const specifiers = ${JSON.stringify(CRITICAL_PLUGIN_SDK_IMPORT_SMOKE_SPECIFIERS)};`,
`const importModule = new Function("specifier", "return imp" + "ort(specifier)");`,
"for (const specifier of specifiers) {",
" await importModule(specifier);",
"}",
].join("\n");
runReleaseCheckCommand(
{ command: process.execPath, args: ["--input-type=module", "--eval", script] },
{
cwd: packageRoot,
stdio: "inherit",
},
);
}
async function main() {
const { values } = parseArgs({ options: { tarball: { type: "string" } } });
checkValidationErrors(
"appcast sparkle version",
collectAppcastSparkleVersionErrors(readFileSync(appcastPath, "utf8")),
);
checkValidationErrors("skill shell script permission", collectSkillShellScriptExecutableErrors());
checkBundledExtensionMetadata();
const temporaryDir = mkdtempSync(join(tmpdir(), "openclaw-release-check-"));
try {
const tarballPath = values.tarball
? resolve(values.tarball)
: await packRootPackage(temporaryDir);
const packedDir = join(temporaryDir, "unpacked");
mkdirSync(packedDir);
const files: { path: string }[] = [];
let unpackedSize = 0;
await extract({
cwd: packedDir,
file: tarballPath,
strict: true,
preservePaths: false,
// npm derives this receipt from tar entries too. Reuse extraction so
// prepared-artifact inspection cannot stall in an extra npm process.
onReadEntry(entry) {
files.push({ path: entry.path.replace(/^package\//u, "") });
unpackedSize += entry.size;
},
});
const results: NpmPackResult[] = [{ filename: basename(tarballPath), files, unpackedSize }];
const packedRoot = join(packedDir, "package");
const rootPackage = JSON.parse(readFileSync(resolve("package.json"), "utf8"));
const packedPackage = JSON.parse(readFileSync(join(packedRoot, "package.json"), "utf8"));
if (packedPackage.name !== "openclaw" || packedPackage.version !== rootPackage.version) {
throw new Error("release-check: prepared tarball does not match the target package version.");
}
await verifyPackedContents(results, packedRoot, tarballPath);
runPackedBundledChannelEntrySmoke(tarballPath, packedRoot);
console.log("release-check: final npm tarball contents and installed runtime look OK.");
} finally {
rmSync(temporaryDir, { recursive: true, force: true });
}
}
export async function checkPackedTargetBootstrap(
targetRoot: string,
packedRoot: string,
): Promise<void> {
const workerProducerPath = resolve(targetRoot, "src/worker/worker-deploy-entry.ts");
const workerBundlePath = resolve(targetRoot, "src/shared/worker-bundle-hash.ts");
// Frozen targets can have shared hash helpers without a deploy entrypoint.
const hasWorkerProducer = existsSync(workerProducerPath);
let workerArtifactDeclarations: Array<[string, unknown]> = [];
if (hasWorkerProducer) {
const target = await importToolingTypeScript(
pathToFileURL(workerBundlePath).href,
import.meta.url,
);
if (Object.hasOwn(target, "WORKER_BUNDLE_ARTIFACT_PATHS")) {
const paths = target.WORKER_BUNDLE_ARTIFACT_PATHS;
if (!Array.isArray(paths) || paths.length === 0) {
throw new Error(
"release-check: target WORKER_BUNDLE_ARTIFACT_PATHS must be a non-empty array.",
);
}
workerArtifactDeclarations = paths.map((value, index): [string, unknown] => [
`WORKER_BUNDLE_ARTIFACT_PATHS[${index}]`,
value,
]);
} else {
// v2026.9.4 deploy targets expose individual paths. Remove this fallback once
// every supported frozen release target declares the canonical array.
workerArtifactDeclarations = Object.entries(target).filter(([name]) =>
/^WORKER_BUNDLE_.*_PATH$/u.test(name),
);
}
}
const workerDeployEntrypoints = workerArtifactDeclarations.map(([name, value]) => {
if (typeof value !== "string" || !value.trim()) {
throw new Error(
`release-check: target worker artifact ${name} must be a non-empty path string.`,
);
}
const normalizedPath = posix.normalize(value);
const workerPath = posix.join("dist/worker", normalizedPath);
if (
value !== value.trim() ||
value !== normalizedPath ||
value.includes("\\") ||
normalizedPath.split("/").includes("..") ||
win32.isAbsolute(value) ||
!workerPath.startsWith("dist/worker/")
) {
throw new Error(
`release-check: target worker artifact ${name} must be a normalized relative path within dist/worker.`,
);
}
return workerPath;
});
if (hasWorkerProducer && workerDeployEntrypoints.length === 0) {
throw new Error(
"release-check: target worker producer is missing WORKER_BUNDLE_*_PATH declarations.",
);
}
// New tooling may qualify a frozen target without the build-owned locator generator.
// Never infer legacy mode from missing output: current targets must rebuild missing metadata.
const locatorModulePath = resolve(targetRoot, "scripts/lib/gateway-run-chunk-metadata.mts");
const locatorModule = existsSync(locatorModulePath)
? await importToolingTypeScript(pathToFileURL(locatorModulePath).href, import.meta.url)
: undefined;
if (
locatorModule &&
locatorModule.GATEWAY_RUN_CHUNK_METADATA_VERSION !== GATEWAY_RUN_CHUNK_METADATA_VERSION
) {
throw new Error("release-check: unsupported target gateway run chunk metadata version.");
}
checkCliBootstrapExternalImports({
rootDir: packedRoot,
workerDeployEntrypoints,
legacyGatewayChunkDiscovery: locatorModule === undefined,
logger: {
error: (message: string) => console.error(`release-check: ${message}`),
},
});
}
async function verifyPackedContents(
results: NpmPackResult[],
packedRoot: string,
tarballPath: string,
): Promise<void> {
await checkPackedTargetBootstrap(process.cwd(), packedRoot);
checkPluginSdkExports(packedRoot);
const criticalPluginSdkEntrypoints = collectCriticalPluginSdkEntrypointSizeFindings(packedRoot);
const criticalPluginSdkSizeFailed = reportLimitViolations(
criticalPluginSdkEntrypoints.violations,
);
if (criticalPluginSdkEntrypoints.errors.length > 0 || criticalPluginSdkSizeFailed) {
throw new Error(
`release-check: critical plugin-sdk entrypoint validation failed.${criticalPluginSdkEntrypoints.errors.length > 0 ? `\n- ${criticalPluginSdkEntrypoints.errors.join("\n- ")}` : ""}`,
);
}
// The tarball verifier owns lifecycle and target-declared dist layout. It
// accepts valid historical entries without duplicating current package policy.
runReleaseCheckCommand(
{
command: process.execPath,
args: [PACKAGE_TARBALL_VERIFIER_PATH, tarballPath],
},
{ stdio: "inherit" },
);
const files = results.flatMap((entry) => entry.files ?? []);
const paths = new Set(files.map((file) => file.path));
const forbidden = collectForbiddenPackPaths(paths);
const forbiddenContent = collectForbiddenPackContentPaths(paths, packedRoot);
const packSize = collectPackUnpackedSizeFindings(results);
const packSizeFailed = reportLimitViolations(packSize.violations);
if (
forbidden.length > 0 ||
forbiddenContent.length > 0 ||
packSize.errors.length > 0 ||
packSizeFailed
) {
if (forbidden.length > 0) {
console.error("release-check: forbidden files in npm pack:");
for (const path of forbidden) {
console.error(` - ${path}`);
}
}
if (forbiddenContent.length > 0) {
console.error("release-check: forbidden private QA markers in npm pack:");
for (const path of forbiddenContent) {
console.error(` - ${path}`);
}
}
if (packSize.errors.length > 0) {
console.error("release-check: invalid npm pack unpacked size metadata:");
for (const error of packSize.errors) {
console.error(` - ${error}`);
}
}
throw new Error("release-check: final npm tarball content checks failed.");
}
}
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
void main().catch((error: unknown) => {
console.error(error);
process.exit(1);
});
}