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("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 { 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, { headers: { "content-type": "application/gzip", "content-length": String(archive.row.size), "x-gitfor-content-sha256": archive.row.sha256, "cache-control": "private, no-store", }, }); }