fix(release): bound release check commands

This commit is contained in:
Vincent Koc
2026-05-28 14:11:55 +02:00
parent 2b0e399da1
commit 98d6331d10
2 changed files with 182 additions and 61 deletions

View File

@@ -60,6 +60,13 @@ export { packageNameFromSpecifier } from "./lib/plugin-package-dependencies.mjs"
type PackFile = { path: string };
type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number };
type ReleaseCheckCommandInvocation = {
command: string;
args: string[];
env?: NodeJS.ProcessEnv;
shell?: boolean | string;
windowsVerbatimArguments?: boolean;
};
const rootPackageExcludedExtensionDirs = collectRootPackageExcludedExtensionDirs();
const requiredPathGroups = [
@@ -135,6 +142,8 @@ const appcastPath = resolve("appcast.xml");
const laneBuildMin = 1_000_000_000;
const laneFloorAdoptionDateKey = 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;
export const MAX_CRITICAL_PLUGIN_SDK_ENTRYPOINT_BYTES = 2 * 1024 * 1024;
export const CRITICAL_PLUGIN_SDK_SIZE_CHECK_SPECIFIERS = [
"openclaw/plugin-sdk/core",
@@ -164,6 +173,55 @@ export const PACKED_COMPLETION_SMOKE_ARGS = [
const PACKED_PLUGIN_SDK_TYPESCRIPT_SMOKE_FIXTURE = resolve(
"scripts/fixtures/packed-plugin-sdk-type-smoke.ts",
);
function positiveEnvInt(name: string, fallback: number): number {
const raw = process.env[name];
if (raw === undefined || raw === "") {
return fallback;
}
const value = Number.parseInt(raw, 10);
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
}
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 output = execFileSync(invocation.command, invocation.args, {
cwd: options.cwd,
encoding: options.encoding ?? "utf8",
env: invocation.env ?? options.env,
killSignal: "SIGKILL",
maxBuffer:
options.maxBuffer ??
positiveEnvInt(
"OPENCLAW_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES",
DEFAULT_RELEASE_CHECK_COMMAND_MAX_BUFFER_BYTES,
),
shell: invocation.shell ?? options.shell,
stdio: options.stdio,
timeout:
options.timeoutMs ??
positiveEnvInt(
"OPENCLAW_RELEASE_CHECK_COMMAND_TIMEOUT_MS",
DEFAULT_RELEASE_CHECK_COMMAND_TIMEOUT_MS,
),
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
});
if (output == null) {
return "";
}
return typeof output === "string" ? output : output.toString("utf8");
}
export function collectSkillShellScriptExecutableErrors(rootDir = resolve(".")): string[] {
if (process.platform === "win32") {
return [];
@@ -291,14 +349,7 @@ function execNpm(
},
): string {
const invocation = resolveReleaseNpmCommand(args, { env: process.env });
return execFileSync(invocation.command, invocation.args, {
...options,
...(invocation.env ? { env: invocation.env } : {}),
...(invocation.shell !== undefined ? { shell: invocation.shell } : {}),
...(invocation.windowsVerbatimArguments !== undefined
? { windowsVerbatimArguments: invocation.windowsVerbatimArguments }
: {}),
});
return runReleaseCheckCommand(invocation, options);
}
function runPackDry(): PackResult[] {
@@ -432,11 +483,17 @@ export function createPackedCompletionSmokeEnv(
}
function runPackedBundledPluginPostinstall(packageRoot: string): void {
execFileSync(process.execPath, [join(packageRoot, "scripts/postinstall-bundled-plugins.mjs")], {
cwd: packageRoot,
stdio: "inherit",
env: createPackedBundledPluginPostinstallEnv(),
});
runReleaseCheckCommand(
{
command: process.execPath,
args: [join(packageRoot, "scripts/postinstall-bundled-plugins.mjs")],
},
{
cwd: packageRoot,
stdio: "inherit",
env: createPackedBundledPluginPostinstallEnv(),
},
);
}
export function collectPackedInstalledPackageVerificationErrors(params: {
@@ -479,12 +536,18 @@ function verifyPackedInstalledPackage(params: {
tmpRoot: string;
}): void {
const invocation = resolveInstalledBinaryCommandInvocation(params.prefixDir, ["--version"]);
const installedBinaryVersion = execFileSync(invocation.command, invocation.args, {
cwd: params.tmpRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
}).trim();
const installedBinaryVersion = runReleaseCheckCommand(
{
command: invocation.command,
args: invocation.args,
windowsVerbatimArguments: invocation.windowsVerbatimArguments,
},
{
cwd: params.tmpRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
},
).trim();
const errors = collectPackedInstalledPackageVerificationErrors({
expectedVersion: params.expectedVersion,
installedBinaryVersion,
@@ -563,10 +626,13 @@ function runPackedPluginSdkTypescriptSmoke(tarballPath: string, tmpRoot: string)
if (!tscPath) {
throw new Error("release-check: packed plugin SDK TypeScript smoke could not find tsc.");
}
execFileSync(process.execPath, [tscPath, "-p", "tsconfig.json", "--pretty", "false"], {
cwd: consumerDir,
stdio: "inherit",
});
runReleaseCheckCommand(
{ command: process.execPath, args: [tscPath, "-p", "tsconfig.json", "--pretty", "false"] },
{
cwd: consumerDir,
stdio: "inherit",
},
);
}
export function writePackedBundledPluginActivationConfig(homeDir: string): void {
@@ -620,20 +686,25 @@ function runPackedBundledPluginActivationSmoke(packageRoot: string, tmpRoot: str
});
writePackedBundledPluginActivationConfig(homeDir);
execFileSync(
process.execPath,
[join(packageRoot, "openclaw.mjs"), ...PACKED_BUNDLED_RUNTIME_DEPS_REPAIR_ARGS],
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,
},
);
execFileSync(process.execPath, [join(packageRoot, "openclaw.mjs"), "plugins", "doctor"], {
cwd: packageRoot,
stdio: "inherit",
env,
});
}
function runPackedTaskRegistryControlRuntimeSmoke(packageRoot: string): void {
@@ -654,11 +725,14 @@ if (typeof runtime.killSubagentRunAdmin !== "function") {
throw new Error("missing killSubagentRunAdmin export");
}
`;
execFileSync(process.execPath, ["--input-type=module", "--eval", source], {
cwd: packageRoot,
stdio: "inherit",
env: createPackedCliSmokeEnv(process.env),
});
runReleaseCheckCommand(
{ command: process.execPath, args: ["--input-type=module", "--eval", source] },
{
cwd: packageRoot,
stdio: "inherit",
env: createPackedCliSmokeEnv(process.env),
},
);
}
function runPackedCliSmoke(params: {
@@ -678,25 +752,29 @@ function runPackedCliSmoke(params: {
for (const args of PACKED_CLI_SMOKE_COMMANDS) {
if (process.platform === "win32") {
execFileSync(
trustedCmdPath,
["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, [...args])],
runReleaseCheckCommand(
{
command: trustedCmdPath,
args: ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, [...args])],
shell: false,
windowsVerbatimArguments: true,
},
{
cwd: params.cwd,
stdio: "inherit",
env,
shell: false,
windowsVerbatimArguments: true,
},
);
continue;
}
execFileSync(binaryPath, [...args], {
cwd: params.cwd,
stdio: "inherit",
env,
shell: false,
});
runReleaseCheckCommand(
{ command: binaryPath, args: [...args], shell: false },
{
cwd: params.cwd,
stdio: "inherit",
env,
},
);
}
}
@@ -739,13 +817,15 @@ function runPackedBundledChannelEntrySmoke(): void {
runPackedBundledPluginActivationSmoke(packageRoot, tmpRoot);
runPackedTaskRegistryControlRuntimeSmoke(packageRoot);
runPackedPluginSdkTypescriptSmoke(tarballPath, tmpRoot);
execFileSync(
process.execPath,
[
resolve("scripts/test-built-bundled-channel-entry-smoke.mjs"),
"--package-root",
packageRoot,
],
runReleaseCheckCommand(
{
command: process.execPath,
args: [
resolve("scripts/test-built-bundled-channel-entry-smoke.mjs"),
"--package-root",
packageRoot,
],
},
{
stdio: "inherit",
env: {
@@ -755,9 +835,11 @@ function runPackedBundledChannelEntrySmoke(): void {
},
);
execFileSync(
process.execPath,
[join(packageRoot, "openclaw.mjs"), ...PACKED_COMPLETION_SMOKE_ARGS],
runReleaseCheckCommand(
{
command: process.execPath,
args: [join(packageRoot, "openclaw.mjs"), ...PACKED_COMPLETION_SMOKE_ARGS],
},
{
cwd: packageRoot,
stdio: "inherit",
@@ -1043,10 +1125,13 @@ function runCriticalPluginSdkEntrypointImportSmoke() {
" await importModule(specifier);",
"}",
].join("\n");
execFileSync(process.execPath, ["--input-type=module", "--eval", script], {
cwd: process.cwd(),
stdio: "inherit",
});
runReleaseCheckCommand(
{ command: process.execPath, args: ["--input-type=module", "--eval", script] },
{
cwd: process.cwd(),
stdio: "inherit",
},
);
}
async function main() {

View File

@@ -34,6 +34,7 @@ import {
packageNameFromSpecifier,
resolveReleaseNpmCommand,
resolveMissingPackBuildHint,
runReleaseCheckCommand,
} from "../scripts/release-check.ts";
import { COMPLETION_SKIP_PLUGIN_COMMANDS_ENV } from "../src/cli/completion-runtime.ts";
import {
@@ -164,6 +165,41 @@ describe("packed CLI smoke", () => {
});
});
describe("runReleaseCheckCommand", () => {
it("returns captured command output", () => {
expect(
runReleaseCheckCommand(
{ command: process.execPath, args: ["--eval", "process.stdout.write('ok')"] },
{ stdio: ["ignore", "pipe", "pipe"] },
),
).toBe("ok");
});
it("bounds commands that ignore termination", () => {
const startedAt = Date.now();
expect(() =>
runReleaseCheckCommand(
{
command: process.execPath,
args: ["--eval", "process.on('SIGTERM', () => {}); setInterval(() => {}, 1000);"],
},
{ stdio: ["ignore", "pipe", "pipe"], timeoutMs: 100 },
),
).toThrow();
expect(Date.now() - startedAt).toBeLessThan(2500);
});
it("bounds captured command output", () => {
expect(() =>
runReleaseCheckCommand(
{ command: process.execPath, args: ["--eval", "process.stdout.write('x'.repeat(4096))"] },
{ maxBuffer: 1024, stdio: ["ignore", "pipe", "pipe"] },
),
).toThrow();
});
});
describe("resolveReleaseNpmCommand", () => {
it("wraps Windows npm.cmd release checks through cmd.exe without shell mode", () => {
const nodeDir = "C:\\Program Files\\nodejs";