Files
openclaw/scripts/package-openclaw-for-docker.mjs
2026-05-27 10:32:53 +02:00

298 lines
8.7 KiB
JavaScript

#!/usr/bin/env node
// Builds the OpenClaw package artifact used by Docker E2E.
// The script owns the build/inventory/pack sequence so local scheduler, shell
// helpers, and GitHub Actions all prepare the exact same npm tarball.
import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const ROOT_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const DEFAULT_PACKAGE_BUILD_TIMEOUT_MS = 45 * 60 * 1000;
const DEFAULT_PACKAGE_INVENTORY_TIMEOUT_MS = 5 * 60 * 1000;
const DEFAULT_PACKAGE_PACK_TIMEOUT_MS = 5 * 60 * 1000;
const DEFAULT_PACKAGE_TARBALL_CHECK_TIMEOUT_MS = 5 * 60 * 1000;
const DEFAULT_TIMEOUT_KILL_AFTER_MS = 5_000;
const ACTIVE_CHILD_KILLERS = new Set();
const SIGNAL_EXIT_CODES = {
SIGHUP: 129,
SIGINT: 130,
SIGTERM: 143,
};
let forwardedSignalExitCode;
for (const signal of Object.keys(SIGNAL_EXIT_CODES)) {
process.on(signal, () => {
forwardedSignalExitCode ??= SIGNAL_EXIT_CODES[signal];
if (ACTIVE_CHILD_KILLERS.size === 0) {
process.exit(forwardedSignalExitCode);
}
for (const killChild of ACTIVE_CHILD_KILLERS) {
killChild(signal);
}
setTimeout(() => {
for (const killChild of ACTIVE_CHILD_KILLERS) {
killChild("SIGKILL");
}
process.exit(forwardedSignalExitCode);
}, DEFAULT_TIMEOUT_KILL_AFTER_MS);
});
}
function resolveTimeoutMs(envName, defaultValue) {
const raw = process.env[envName];
if (raw === undefined || raw === "") {
return defaultValue;
}
const parsed = Number(raw);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new Error(`${envName} must be a positive timeout in milliseconds`);
}
return Math.trunc(parsed);
}
function parseArgs(argv) {
const options = {
outputDir: "",
outputName: "",
skipBuild: false,
sourceDir: ROOT_DIR,
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--output-dir") {
options.outputDir = argv[(index += 1)] ?? "";
} else if (arg?.startsWith("--output-dir=")) {
options.outputDir = arg.slice("--output-dir=".length);
} else if (arg === "--output-name") {
options.outputName = argv[(index += 1)] ?? "";
} else if (arg?.startsWith("--output-name=")) {
options.outputName = arg.slice("--output-name=".length);
} else if (arg === "--skip-build") {
options.skipBuild = true;
} else if (arg === "--source-dir") {
options.sourceDir = argv[(index += 1)] ?? "";
} else if (arg?.startsWith("--source-dir=")) {
options.sourceDir = arg.slice("--source-dir=".length);
} else {
throw new Error(`unknown argument: ${arg}`);
}
}
return options;
}
function run(command, args, cwd, options = {}) {
return new Promise((resolve, reject) => {
const useProcessGroup = process.platform !== "win32";
const child = spawn(command, args, {
cwd,
stdio: ["ignore", "pipe", "pipe"],
env: options.env ?? process.env,
detached: useProcessGroup,
});
let timedOut = false;
let stdout = "";
let settled = false;
let timeout;
const finish = (error, value = "") => {
if (settled) {
return;
}
settled = true;
if (timeout) {
clearTimeout(timeout);
}
ACTIVE_CHILD_KILLERS.delete(killChild);
if (forwardedSignalExitCode !== undefined && ACTIVE_CHILD_KILLERS.size === 0) {
process.exit(forwardedSignalExitCode);
}
if (error) {
reject(error);
return;
}
resolve(value);
};
const killChild = (signal) => {
if (useProcessGroup && child.pid) {
try {
process.kill(-child.pid, signal);
return;
} catch {
// The direct child may already have exited; fall back to child.kill.
}
}
child.kill(signal);
};
ACTIVE_CHILD_KILLERS.add(killChild);
timeout =
options.timeoutMs === undefined
? undefined
: setTimeout(() => {
timedOut = true;
killChild("SIGTERM");
setTimeout(
() => killChild("SIGKILL"),
options.killAfterMs ?? DEFAULT_TIMEOUT_KILL_AFTER_MS,
).unref?.();
}, options.timeoutMs);
timeout?.unref?.();
if (options.captureStdout) {
child.stdout.on("data", (chunk) => {
stdout += String(chunk);
});
} else {
child.stdout.pipe(process.stderr, { end: false });
}
child.stderr.pipe(process.stderr, { end: false });
child.on("error", (error) => finish(error));
child.on("close", (status, signal) => {
if (timedOut) {
finish(new Error(`${command} ${args.join(" ")} timed out after ${options.timeoutMs}ms`));
return;
}
if (status === 0) {
finish(undefined, stdout);
return;
}
finish(new Error(`${command} ${args.join(" ")} failed with ${status ?? signal}`));
});
});
}
const PACKAGE_ARTIFACT_BUILD_STEPS = [
{
label: "Building OpenClaw package artifacts",
command: "node",
args: ["scripts/build-all.mjs"],
},
];
export async function buildPackageArtifacts(sourceDir, options = {}) {
const runImpl = options.runImpl ?? run;
for (const step of PACKAGE_ARTIFACT_BUILD_STEPS) {
console.error(`==> ${step.label}`);
await runImpl(step.command, step.args, sourceDir, {
env: {
...process.env,
OPENCLAW_BUILD_ALL_NO_PNPM: "1",
OPENCLAW_RUN_NODE_SKIP_DTS_BUILD: "1",
},
timeoutMs: resolveTimeoutMs(
"OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS",
DEFAULT_PACKAGE_BUILD_TIMEOUT_MS,
),
});
}
}
export const runCommandForTest = run;
async function runCapture(command, args, cwd, options = {}) {
return await run(command, args, cwd, { ...options, captureStdout: true });
}
async function newestOpenClawTarball(outputDir, packOutput) {
let fromOutput = "";
for (const line of packOutput.split(/\r?\n/u)) {
const trimmed = line.trim();
if (/^openclaw-.*\.tgz$/u.test(trimmed)) {
fromOutput = trimmed;
}
}
if (fromOutput) {
return path.join(outputDir, fromOutput);
}
const entries = await fs.readdir(outputDir);
const packed = entries
.filter((entry) => /^openclaw-.*\.tgz$/u.test(entry))
.toSorted()
.at(-1);
if (!packed) {
throw new Error(`missing packed OpenClaw tarball in ${outputDir}`);
}
return path.join(outputDir, packed);
}
async function main() {
const options = parseArgs(process.argv.slice(2));
const sourceDir = path.resolve(ROOT_DIR, options.sourceDir || ROOT_DIR);
const outputDir = path.resolve(
ROOT_DIR,
options.outputDir || path.join(".artifacts", "docker-e2e-package"),
);
await fs.mkdir(outputDir, { recursive: true });
if (!options.skipBuild) {
await buildPackageArtifacts(sourceDir);
}
console.error("==> Writing OpenClaw package inventory");
await run(
"node",
[
"--import",
"tsx",
"--input-type=module",
"-e",
"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());",
],
sourceDir,
{
timeoutMs: resolveTimeoutMs(
"OPENCLAW_DOCKER_PACKAGE_INVENTORY_TIMEOUT_MS",
DEFAULT_PACKAGE_INVENTORY_TIMEOUT_MS,
),
},
);
console.error("==> Packing OpenClaw package");
const packOutput = await runCapture(
"npm",
["pack", "--silent", "--ignore-scripts", "--pack-destination", outputDir],
sourceDir,
{
timeoutMs: resolveTimeoutMs(
"OPENCLAW_DOCKER_PACKAGE_PACK_TIMEOUT_MS",
DEFAULT_PACKAGE_PACK_TIMEOUT_MS,
),
},
);
let tarball = await newestOpenClawTarball(outputDir, packOutput);
if (options.outputName) {
const target = path.join(outputDir, options.outputName);
if (target !== tarball) {
await fs.rm(target, { force: true });
await fs.rename(tarball, target);
tarball = target;
}
}
console.error("==> Checking OpenClaw package tarball");
const checkStartedAt = Date.now();
await run(
"node",
[path.join(ROOT_DIR, "scripts/check-openclaw-package-tarball.mjs"), tarball],
sourceDir,
{
timeoutMs: resolveTimeoutMs(
"OPENCLAW_DOCKER_PACKAGE_TARBALL_CHECK_TIMEOUT_MS",
DEFAULT_PACKAGE_TARBALL_CHECK_TIMEOUT_MS,
),
},
);
console.error(
`==> OpenClaw package tarball check finished in ${Math.round((Date.now() - checkStartedAt) / 1000)}s`,
);
process.stdout.write(`${tarball}\n`);
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
await main().catch((error) => {
console.error(error instanceof Error ? error.message : String(error));
process.exit(1);
});
}