50 lines · 2.3 KB
const READY_ATTEMPTS = 60;
const READY_DELAY_MS = 500;

/**
 * `start()` returns before the container accepts `exec()`, which throws "exec() requires a running container"
 * until it has booted; probe with a no-op until it answers.
 */
export async function waitForContainer(container: Container): Promise<void> {
  for (let attempt = 0; attempt < READY_ATTEMPTS; attempt++) {
    try {
      if (container.running) {
        const probe = await container.exec(["true"], { stdout: "ignore", stderr: "ignore" });
        if ((await probe.exitCode) === 0) return;
      }
    } catch {
      // still booting
    }
    await new Promise((resolve) => setTimeout(resolve, READY_DELAY_MS));
  }
  throw new Error("The runner container didn't start in time");
}

/** Host cloud jobs use to reach the forge API; requests to it never leave workerd (see `startRunnerContainer`). */
export const INTERNAL_API_HOST = "gitfor.internal";
export const INTERNAL_API_URL = `http://${INTERNAL_API_HOST}`;

const intercepted = new WeakSet<Container>();

/**
 * Starts the runner container if needed and routes its HTTP requests for INTERNAL_API_HOST to `api` (the
 * JobApiGateway entrypoint, which serves only the job API), so the job API (cache, action archives) works without PUBLIC_URL being reachable from inside the
 * container (in local development it is `localhost`, which is the container itself). The interception is
 * re-applied once per Durable Object instance, since a restarted object may find the container still running.
 *
 * Egress is deliberately open (CI-P9): jobs install packages and call arbitrary APIs like GitHub-hosted runners,
 * so any code a job runs can reach the internet with whatever secrets it was given. A host allowlist isn't
 * enforced because `interceptOutboundHttp` only sees plain HTTP, so it can't gate HTTPS or raw TCP; the
 * protections are instead that fork and untrusted runs get no secrets and CI tokens are job-scoped and short-lived.
 */
export async function ensureRunnerContainer(container: Container, api: Fetcher): Promise<void> {