96 lines
3.1 KiB
TypeScript
96 lines
3.1 KiB
TypeScript
import { $ } from "bun";
|
|||
|
|
import { unlinkSync, writeFileSync } from "node:fs";
|
||
|
|
import { join } from "node:path";
|
||
|
|
import { config } from "../lib/config";
|
||
|
|
import { guest } from "../lib/guest";
|
||
|
|
import { fail } from "../lib/shell";
|
||
|
|
import { tart } from "../lib/tart";
|
||
|
|
|
||
|
|
const env = process.env;
|
||
|
|
const command = env.BUILDKITE_COMMAND ?? "";
|
||
|
|
const jobId = env.BUILDKITE_JOB_ID ?? String(process.pid);
|
||
|
|
const checkout = env.BUILDKITE_BUILD_CHECKOUT_PATH ?? fail("no BUILDKITE_BUILD_CHECKOUT_PATH");
|
||
|
|
const vm = `bk-${jobId}`;
|
||
|
|
|
||
|
|
const hostOnlyEnv = new Set([
|
||
|
|
"BUILDKITE_AGENT_JOB_API_SOCKET",
|
||
|
|
"BUILDKITE_AGENT_JOB_API_TOKEN",
|
||
|
|
"BUILDKITE_BIN_PATH",
|
||
|
|
"BUILDKITE_BUILD_CHECKOUT_PATH",
|
||
|
|
"BUILDKITE_BUILD_PATH",
|
||
|
|
"BUILDKITE_ENV_FILE",
|
||
|
|
"BUILDKITE_HOOKS_PATH",
|
||
|
|
"BUILDKITE_PLUGINS_PATH",
|
||
|
|
"BUILDKITE_SOCKETS_PATH",
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (!command.includes("runner.node.mjs")) {
|
||
|
|
console.log("--- running on host (not a test step)");
|
||
|
|
process.exit((await $`bash -c ${command}`.nothrow()).exitCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
let cleaned = false;
|
||
|
|
async function cleanup(): Promise<void> {
|
||
|
|
if (cleaned) return;
|
||
|
|
cleaned = true;
|
||
|
|
await tart.destroy(vm);
|
||
|
|
}
|
||
|
|
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"] as const) {
|
||
|
|
process.on(signal, () => cleanup().finally(() => process.exit(130)));
|
||
|
|
}
|
||
|
|
|
||
|
|
let status = 1;
|
||
|
|
try {
|
||
|
|
status = await runInGuest();
|
||
|
|
} finally {
|
||
|
|
await cleanup();
|
||
|
|
}
|
||
|
|
process.exit(status);
|
||
|
|
|
||
|
|
async function runInGuest(): Promise<number> {
|
||
|
|
console.log(`--- :tart: ${vm} from ${config.tart.image}`);
|
||
|
|
const ip = (await boot()) ?? (await retryBoot()) ?? fail(`guest never came up; see /tmp/${vm}.log`);
|
||
|
|
console.log(` guest ${ip}`);
|
||
|
|
const g = guest(ip);
|
||
|
|
|
||
|
|
console.log("--- :package: sync checkout");
|
||
|
|
await g.syncTo(checkout, "work");
|
||
|
|
await g.push(join(import.meta.dir, "..", "guest", "job.sh"), "job.sh");
|
||
|
|
await pushEnv(g);
|
||
|
|
|
||
|
|
console.log(`+++ :test_tube: ${command}`);
|
||
|
|
const socket = "/tmp/bk-job-api.sock";
|
||
|
|
const forward = ["-o", "StreamLocalBindUnlink=yes", "-R", `${socket}:${env.BUILDKITE_AGENT_JOB_API_SOCKET}`];
|
||
|
|
const status = await g.run(
|
||
|
|
`BUILDKITE_AGENT_JOB_API_SOCKET=${socket} BUILDKITE_AGENT_JOB_API_TOKEN=${env.BUILDKITE_AGENT_JOB_API_TOKEN ?? ""} /bin/bash ~/job.sh`,
|
||
|
|
forward,
|
||
|
|
);
|
||
|
|
|
||
|
|
console.log("--- :outbox_tray: collect reports");
|
||
|
|
await g.collectReports("work", checkout);
|
||
|
|
return status;
|
||
|
|
}
|
||
|
|
|
||
|
|
async function boot(): Promise<string | undefined> {
|
||
|
|
await tart.cloneLocked(config.tart.image, vm);
|
||
|
|
tart.start(vm, `/tmp/${vm}.log`);
|
||
|
|
return tart.waitForSsh(vm);
|
||
|
|
}
|
||
|
|
|
||
|
|
// the first boot after a host restart can lose a race with vmnet coming up
|
||
|
|
async function retryBoot(): Promise<string | undefined> {
|
||
|
|
console.log(" first boot failed; retrying once");
|
||
|
|
await tart.destroy(vm);
|
||
|
|
return boot();
|
||
|
|
}
|
||
|
|
|
||
|
|
async function pushEnv(g: ReturnType<typeof guest>): Promise<void> {
|
||
|
|
const lines = Object.entries(env)
|
||
|
|
.filter(([key]) => /^(BUILDKITE|BUN_|EXPECTED_PLATFORM_)|^(CI|ASAN_OPTIONS)$/.test(key) && !hostOnlyEnv.has(key))
|
||
|
|
.map(([key, value]) => `${key}=${$.escape(String(value ?? ""))}`);
|
||
|
|
const file = `/tmp/${vm}.env`;
|
||
|
|
writeFileSync(file, lines.join("\n") + "\n");
|
||
|
|
await g.push(file, "job.env");
|
||
|
|
unlinkSync(file);
|
||
|
|
}
|