#!/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); }); }