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.
| The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook. | 1 | import { env } from "cloudflare:workers"; |
| 2 | ||
| 3 | import type { RepoPath, Viewer } from "@g1t/contracts"; | |
| 4 | ||
| 5 | import { LEGACY_KV_UNTIL } from "./run-artifacts"; | |
| 6 | import { 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 | */ | |
| 15 | export 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 | ||
| 25 | type Meta = { size: number; chunks: number; at: number; name: string }; | |
| 26 | ||
| 27 | async 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. */ | |
| 41 | export 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 | */ | |
| 54 | export 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 | ||
| 64 | export 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 | } |