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, {