190 lines · 6.4 KB
#!/usr/bin/env bun
/**
 * stig self-hosted runner.
 *
 *   STIG_URL=https://forge.example.com STIG_RUNNER_TOKEN=stig_runner_... bun runner/stig-runner.ts
 *
 * Polls the forge for queued jobs whose `runs-on` labels match, executes the generated job script
 * with bash, streams logs back and uploads artifacts. Requires bash, git and tar on the host.
 */
import { spawn } from "node:child_process";
import { mkdir, rm, stat, writeFile, readFile } from "node:fs/promises";
import { hostname, tmpdir } from "node:os";
import { join } from "node:path";

type ClaimedJob = {
  id: string;
  name: string;
  repository: string;
  timeoutMinutes: number;
  script: string;
  env: Record<string, string>;
};

const SERVER = requireEnv("STIG_URL").replace(/\/$/, "");
const TOKEN = requireEnv("STIG_RUNNER_TOKEN");
const LABELS = (process.env.STIG_RUNNER_LABELS ?? "")
  .split(",")
  .map((label) => label.trim())
  .filter(Boolean);
const WORK_ROOT = process.env.STIG_RUNNER_WORKDIR ?? join(tmpdir(), "stig-runner");
const IDLE_POLL_MS = 5000;
const ERROR_BACKOFF_MS = 15_000;
const LOG_FLUSH_MS = 1000;
const MAX_LOG_CHUNK = 512 * 1024;
const ARTIFACT_MARKER = /^::stig::artifact (\S+) "?(.+?)"?$/;
const KILL_GRACE_MS = 15_000;
const HTTP_NO_CONTENT = 204;

function requireEnv(name: string): string {
  const value = process.env[name];