Skip to content
81 linesCodeBlameRaw
1import { env } from "cloudflare:workers";
2
3import type { RepoPath, Viewer } from "@g1t/contracts";
4
5import { LEGACY_KV_UNTIL } from "./run-artifacts";
6import { actions } from "./services.server";
7
8/**
9 * A run's artifacts, for its page. The actions service lists them (their
10 * bytes are in R2, downloaded through the API's signed links); artifacts an
11 * older runner kept in KV (`a/{run}/{name}`, its bytes in chunks `…#0`,
12 * `…#1`) are listed too until KV expires them, for runs the caller has
13 * already been shown (`legacyArtifactsWorthAsking` in run-artifacts.ts).
14 */
15export type ArtifactRow = {
16 /** The artifact's number; null for one kept in KV. */
17 id: number | null;
18 name: string;
19 size: number;
20 /** When it goes, RFC 3339; null for one kept in KV (14 days after it was made). */
21 expiresAt: string | null;
22 createdAt: string;
23};
24
25type Meta = { size: number; chunks: number; at: number; name: string };
26
27async function kvArtifacts(run: string): Promise<ArtifactRow[]> {
28 const listed = await env.BLOBS.list<Meta>({ prefix: `a/${run}/` });
29 return listed.keys
30 .filter((key) => !key.name.includes("#") && key.metadata)
31 .map((key) => ({
32 id: null,
33 name: key.metadata!.name,
34 size: key.metadata!.size,
35 expiresAt: new Date(key.metadata!.at + 14 * 86_400_000).toISOString(),
36 createdAt: new Date(key.metadata!.at).toISOString(),
37 }));
38}
39
40/** A run's artifacts the actions service keeps, which checks who may see them. */
41export async function listArtifacts(repo: RepoPath, viewer: Viewer, run: string): Promise<ArtifactRow[]> {
42 const kept = await actions.artifacts(repo, viewer, { run, per_page: 100 });
43 const rows: ArtifactRow[] = kept.ok
44 ? kept.value.artifacts.map((a) => ({ id: a.id, name: a.name, size: a.size, expiresAt: a.expires_at, createdAt: a.created_at }))
45 : [];
46 return rows.sort((a, b) => a.name.localeCompare(b.name));
47}
48
49/**
50 * `rows` with the artifacts an older runner kept in KV for `run` added.
51 * Only for a run the viewer was allowed to see. Delete after
52 * 2026-10-22T00:00Z (`LEGACY_KV_UNTIL`).
53 */
54export async function withLegacyArtifacts(rows: ArtifactRow[], run: string): Promise<ArtifactRow[]> {
55 if (Date.now() >= LEGACY_KV_UNTIL) return rows;
56 const legacy = await kvArtifacts(run).catch(() => []);
57 const all = [...rows];
58 for (const row of legacy) {
59 if (!all.some((r) => r.name === row.name)) all.push(row);
60 }
61 return all.sort((a, b) => a.name.localeCompare(b.name));
62}
63
64export async function readArtifact(run: string, name: string): Promise<Uint8Array | null> {
65 // Every artifact kept in KV has expired (run-artifacts.ts).
66 if (Date.now() >= LEGACY_KV_UNTIL) return null;
67 const base = `a/${run}/${name}`;
68 const meta = await env.BLOBS.get<Meta>(base, "json");
69 if (!meta) return null;
70 const parts = await Promise.all(
71 Array.from({ length: meta.chunks }, (_, index) => env.BLOBS.get(`${base}#${index}`, "arrayBuffer")),
72 );
73 if (parts.some((part) => part == null)) return null;
74 const out = new Uint8Array(meta.size);
75 let offset = 0;
76 for (const part of parts as ArrayBuffer[]) {
77 out.set(new Uint8Array(part), offset);
78 offset += part.byteLength;
79 }
80 return out;
81}