197 lines
6.5 KiB
JavaScript
197 lines
6.5 KiB
JavaScript
import { spawn } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { CHECK_PLAN_VERSION, checkProfiles, listCheckTasks } from "./check-plan.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
|
|
export async function runCheckRunner(argv, io = process) {
|
|
const options = parseCheckRunnerArgs(argv);
|
|
if (options.help) {
|
|
writeJson(io.stdout, helpPayload());
|
|
return { exitCode: 0 };
|
|
}
|
|
|
|
const selected = selectTasks(options);
|
|
const payload = {
|
|
ok: true,
|
|
command: "check-runner",
|
|
version: CHECK_PLAN_VERSION,
|
|
profile: options.profile,
|
|
groups: options.groups,
|
|
ids: options.ids,
|
|
taskCount: selected.length,
|
|
tasks: selected.map(serializeTask)
|
|
};
|
|
|
|
if (options.list || options.dryRun) {
|
|
writeJson(io.stdout, { ...payload, mode: options.list ? "list" : "dry-run" });
|
|
return { exitCode: 0 };
|
|
}
|
|
|
|
writeJson(io.stdout, { ...payload, mode: "run-start", tasks: undefined });
|
|
const startedAt = Date.now();
|
|
for (let index = 0; index < selected.length; index += 1) {
|
|
const task = selected[index];
|
|
const resolved = resolveTaskCommand(task);
|
|
writeJson(io.stdout, {
|
|
ok: true,
|
|
event: "task-start",
|
|
index: index + 1,
|
|
total: selected.length,
|
|
task: serializeTask(task),
|
|
resolvedCommand: resolved.command
|
|
});
|
|
const exitCode = await runTask(resolved);
|
|
if (exitCode !== 0) {
|
|
writeJson(io.stdout, {
|
|
ok: false,
|
|
event: "task-failed",
|
|
task: serializeTask(task),
|
|
exitCode,
|
|
elapsedMs: Date.now() - startedAt
|
|
});
|
|
return { exitCode };
|
|
}
|
|
}
|
|
|
|
writeJson(io.stdout, {
|
|
ok: true,
|
|
event: "run-complete",
|
|
profile: options.profile,
|
|
taskCount: selected.length,
|
|
elapsedMs: Date.now() - startedAt
|
|
});
|
|
return { exitCode: 0 };
|
|
}
|
|
|
|
export function parseCheckRunnerArgs(argv) {
|
|
const options = { profile: "check", groups: [], ids: [], list: false, dryRun: false, help: false };
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const arg = argv[index];
|
|
const readValue = (name) => {
|
|
const inline = arg.match(new RegExp(`^${name}=(.+)$`, "u"));
|
|
if (inline) return inline[1];
|
|
const value = argv[index + 1];
|
|
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
index += 1;
|
|
return value;
|
|
};
|
|
if (arg === "--help" || arg === "-h") options.help = true;
|
|
else if (arg === "--profile" || arg.startsWith("--profile=")) options.profile = readValue("--profile");
|
|
else if (arg === "--group" || arg.startsWith("--group=")) options.groups.push(...readValue("--group").split(",").filter(Boolean));
|
|
else if (arg === "--id" || arg.startsWith("--id=")) options.ids.push(...readValue("--id").split(",").filter(Boolean));
|
|
else if (arg === "--list") options.list = true;
|
|
else if (arg === "--dry-run") options.dryRun = true;
|
|
else throw new Error(`unknown check-runner argument: ${arg}`);
|
|
}
|
|
if (!checkProfiles[options.profile]) throw new Error(`unknown check profile: ${options.profile}`);
|
|
return options;
|
|
}
|
|
|
|
export function selectTasks(options) {
|
|
const groups = new Set(options.groups);
|
|
const ids = new Set(options.ids);
|
|
const profileTasks = listCheckTasks(options.profile);
|
|
const availableGroups = new Set(profileTasks.map((task) => task.group));
|
|
const availableIds = new Set(profileTasks.map((task) => task.id));
|
|
for (const group of groups) {
|
|
if (!availableGroups.has(group)) throw new Error(`unknown check group for profile ${options.profile}: ${group}`);
|
|
}
|
|
for (const id of ids) {
|
|
if (!availableIds.has(id)) throw new Error(`unknown check task id for profile ${options.profile}: ${id}`);
|
|
}
|
|
const selected = profileTasks.filter((task) => {
|
|
if (groups.size > 0 && !groups.has(task.group)) return false;
|
|
if (ids.size > 0 && !ids.has(task.id)) return false;
|
|
return true;
|
|
});
|
|
if (selected.length === 0) throw new Error(`check-runner selected no tasks for profile ${options.profile}`);
|
|
return selected;
|
|
}
|
|
|
|
export function resolveTaskCommand(task) {
|
|
const [bin, ...args] = task.command;
|
|
if (!bin) throw new Error(`task has empty command: ${task.id}`);
|
|
const cwd = task.cwd ? path.resolve(repoRoot, task.cwd) : repoRoot;
|
|
return {
|
|
task,
|
|
command: bin === "bun" ? resolveBunCommand(cwd) : bin,
|
|
args,
|
|
cwd
|
|
};
|
|
}
|
|
|
|
function runTask(task) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(task.command, task.args, {
|
|
cwd: task.cwd,
|
|
env: process.env,
|
|
stdio: ["ignore", "inherit", "inherit"]
|
|
});
|
|
child.on("error", (error) => {
|
|
process.stderr.write(JSON.stringify({ ok: false, event: "task-spawn-error", task: serializeTask(task.task), error: error.message }, null, 2) + "\n");
|
|
resolve(127);
|
|
});
|
|
child.on("exit", (code, signal) => {
|
|
if (signal) {
|
|
process.stderr.write(JSON.stringify({ ok: false, event: "task-signal", task: serializeTask(task.task), signal }, null, 2) + "\n");
|
|
resolve(128);
|
|
return;
|
|
}
|
|
resolve(code ?? 0);
|
|
});
|
|
});
|
|
}
|
|
|
|
function resolveBunCommand(cwd) {
|
|
for (const candidate of bunCandidates(cwd)) {
|
|
if (!candidate) continue;
|
|
if (!candidate.includes(path.sep)) return candidate;
|
|
if (existsSync(candidate)) return candidate;
|
|
}
|
|
return "bun";
|
|
}
|
|
|
|
function bunCandidates(cwd) {
|
|
const exe = process.platform === "win32" ? "bun.exe" : "bun";
|
|
return [
|
|
process.env.HWLAB_BUN_COMMAND,
|
|
process.env.HWLAB_BUN_BINARY,
|
|
process.env.BUN_BINARY,
|
|
path.join(cwd, "node_modules", ".bin", exe),
|
|
path.join(repoRoot, "node_modules", ".bin", exe),
|
|
path.join(os.homedir(), ".bun", "bin", exe),
|
|
"/root/.bun/bin/bun",
|
|
"/usr/local/bin/bun",
|
|
"bun"
|
|
].filter(Boolean);
|
|
}
|
|
|
|
function serializeTask(task) {
|
|
return { id: task.id, group: task.group, cwd: task.cwd ?? ".", command: task.command };
|
|
}
|
|
|
|
function writeJson(stream, value) {
|
|
stream.write(JSON.stringify(value, null, 2) + "\n");
|
|
}
|
|
|
|
function helpPayload() {
|
|
return {
|
|
ok: true,
|
|
command: "check-runner",
|
|
usage: [
|
|
"node scripts/check-runner.mjs --profile check",
|
|
"node scripts/check-runner.mjs --profile validate",
|
|
"node scripts/check-runner.mjs --profile check --group cloud-api",
|
|
"node scripts/check-runner.mjs --profile check --list",
|
|
"node scripts/check-runner.mjs --profile check --dry-run"
|
|
],
|
|
profiles: Object.keys(checkProfiles),
|
|
groups: [...new Set(Object.values(checkProfiles).flat().map((task) => task.group))].sort()
|
|
};
|
|
}
|