49 lines · 2.2 KB
import { first } from "../../db";
import { jobDb } from "../scope";
import { ciRunAccount } from "../../metering/attribution";
import type { CompiledJob } from "../compile";
import { ensureArchive, forgetBrokenArchive, readArchive, touch } from "./archive";
import type { ArchiveRow } from "./archive";

const ARCHIVE_KEY = /^([^/@]+)\/([^/@]+)@([0-9a-f]{40,64})$/;

/**
 * A run can outlive its archive (pruned after 30 idle days, or its R2 object lost). Archives are
 * deterministic, so re-downloading recreates the same bytes and the sha256 recorded in the job still matches;
 * the runner verifies it either way.
 */
async function restoreArchive(key: string, runId: string): Promise<{ row: ArchiveRow; body: ReadableStream } | null> {
  const match = ARCHIVE_KEY.exec(key);
  const billing = await ciRunAccount(runId);
  if (!match || !billing) return null;
  const broken = await first<ArchiveRow>("SELECT * FROM action_archives WHERE key = ?", key);
  if (broken) await forgetBrokenArchive(broken);
  await ensureArchive(match[1], match[2], match[3], { accountId: billing.accountId, repoId: billing.repoId, runId });
  return readArchive(key);
}

/** Serves an action archive to a job whose own spec references it; any other key is refused. */
export async function serveActionArchive(jobId: string, key: string): Promise<Response> {
  const db = await jobDb(jobId);
  const job = await db?.first<{ spec: string; run_id: string }>(
    "SELECT spec, run_id FROM workflow_jobs WHERE id = ?",
    jobId,
  );
  if (!job) return Response.json({ error: "job not found" }, { status: 404 });
  const spec: CompiledJob = JSON.parse(job.spec);
  if (!key || !spec.steps.some((step) => step.action?.archive?.key === key)) {
    return Response.json({ error: "this job does not use that action" }, { status: 403 });
  }
  const archive = (await readArchive(key)) ?? (await restoreArchive(key, job.run_id));
  if (!archive) return Response.json({ error: "action archive not found" }, { status: 404 });
  await touch(archive.row);
  return new Response(archive.body, {