Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 1 | import { env } from "cloudflare:workers"; |
| 2 | ||
| Merge main into the run-protection branch | 3 | import type { RepoPath, Viewer } from "@g1t/contracts"; |
| 4 | ||
| 5 | import { actions } from "./services.server"; | |
| 6 | ||
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 7 | /** |
| Merge main into the run-protection branch | 8 | * A run's artifacts, for its page. The actions service lists them (their |
| 9 | * bytes are in R2, downloaded through the API's signed links); artifacts an | |
| 10 | * older runner kept in KV (`a/{run}/{name}`, its bytes in chunks `…#0`, | |
| 11 | * `…#1`) are listed too until KV expires them. | |
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 12 | */ |
| Merge main into the run-protection branch | 13 | export type ArtifactRow = { |
| 14 | /** The artifact's number; null for one kept in KV. */ | |
| 15 | id: number | null; | |
| 16 | name: string; | |
| 17 | size: number; | |
| 18 | /** When it goes, RFC 3339; null for one kept in KV (14 days after it was made). */ | |
| 19 | expiresAt: string | null; | |
| 20 | createdAt: string; | |
| 21 | }; | |
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 22 | |
| 23 | type Meta = { size: number; chunks: number; at: number; name: string }; | |
| 24 | ||
| Merge main into the run-protection branch | 25 | async function kvArtifacts(run: string): Promise<ArtifactRow[]> { |
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 26 | const listed = await env.BLOBS.list<Meta>({ prefix: `a/${run}/` }); |
| 27 | return listed.keys | |
| 28 | .filter((key) => !key.name.includes("#") && key.metadata) | |
| Merge main into the run-protection branch | 29 | .map((key) => ({ |
| 30 | id: null, | |
| 31 | name: key.metadata!.name, | |
| 32 | size: key.metadata!.size, | |
| 33 | expiresAt: new Date(key.metadata!.at + 14 * 86_400_000).toISOString(), | |
| 34 | createdAt: new Date(key.metadata!.at).toISOString(), | |
| 35 | })); | |
| 36 | } | |
| 37 | ||
| 38 | export async function listArtifacts(repo: RepoPath, viewer: Viewer, run: string): Promise<ArtifactRow[]> { | |
| 39 | const [kept, legacy] = await Promise.all([ | |
| 40 | actions.artifacts(repo, viewer, { run, per_page: 100 }), | |
| 41 | kvArtifacts(run).catch(() => []), | |
| 42 | ]); | |
| 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 | for (const row of legacy) { | |
| 47 | if (!rows.some((r) => r.name === row.name)) rows.push(row); | |
| 48 | } | |
| 49 | return rows.sort((a, b) => a.name.localeCompare(b.name)); | |
| Actions: workflow_run, workflow.completed, artifacts on the run page, Node 24 | 50 | } |
| 51 | ||
| 52 | export async function readArtifact(run: string, name: string): Promise<Uint8Array | null> { | |
| 53 | const base = `a/${run}/${name}`; | |
| 54 | const meta = await env.BLOBS.get<Meta>(base, "json"); | |
| 55 | if (!meta) return null; | |
| 56 | const parts = await Promise.all( | |
| 57 | Array.from({ length: meta.chunks }, (_, index) => env.BLOBS.get(`${base}#${index}`, "arrayBuffer")), | |
| 58 | ); | |
| 59 | if (parts.some((part) => part == null)) return null; | |
| 60 | const out = new Uint8Array(meta.size); | |
| 61 | let offset = 0; | |
| 62 | for (const part of parts as ArrayBuffer[]) { | |
| 63 | out.set(new Uint8Array(part), offset); | |
| 64 | offset += part.byteLength; | |
| 65 | } | |
| 66 | return out; | |
| 67 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.