Artifacts repos: delete on erase + backfill GC
Status summary#
Implementation complete; awaiting review.
- Done: confirmed Artifacts repos were never covered by erase-data or
preview GC (coverage gap, not a broken deletion); erase-data now wipes the
namespace (budgeted, skipped under
--preserve-auth);pnpm artifacts-gcbackfill script with dry-run/age-cutoff/live-project-skip; doc row; triage unit tests. - Missing: a live run of the backfill against the real pile — start with
dopplercreds available:pnpm --dir apps/os artifacts-gc --env preview_1 --dry-run.
Problem#
The dev/preview Cloudflare account (376ef7ed81b0573f93524de763666c15) holds
~783k Artifacts repos across the per-slot namespaces (os-preview-1-repos:
94k, os-preview-3-repos: 120k, …). Every project repo creation
(getOrCreateArtifact in apps/os/src/domains/repos/artifact-creation.ts)
mints a repo plus a 365-day write token, and nothing ever deletes them:
apps/os/scripts/erase-data.tsdestroys DOs, wipes D1/KV, and walks/lifecycle-expires R2 — Artifacts repos are not mentioned.docs/preview-resource-gc.md's teardown table covers compute, R2, D1/KV — no Artifacts row. The product predates none of this; the repos were simply never added to the teardown inventory.
So every preview acquire→erase cycle strands the slot's repos forever. (Surfaced during the 2026-09-01 Artifacts 403 incident; the pile was a red herring for that incident but is a real hygiene problem.)
Design decisions (made AFK — assumptions marked)#
- Erase path owns steady-state deletion. After an erase, every project in
the slot is gone, so every repo in the slot's
${osWorkerName}-reposnamespace is orphaned. erase-data deletes them all, delete-then-relist (oldest first,sort=created_at&direction=asc,limit=200), with the same 90s deadline budget the R2 walk uses — partial progress is fine, the next erase continues. --preserve-authskips the wipe. Preserve-auth exists for a planned production recreation where projects are recreated under their exact ids; the backing repos are those projects' git history, so deleting them would be data loss. (Assumption: history should survive a preserve-auth erase.)- Backfill is a separate script (
apps/os/scripts/artifacts-gc.ts, run aspnpm artifacts-gc --env preview_N): the existing pile is far too big for erase-data's per-run budget. Oldest-first, rate-limited by the shared 429 choke point, with--max-deletes,--older-than-hours(default 24) and--dry-run. One env per invocation; loop over slots in the shell. - Live repos are skipped twice over in the backfill: repos younger than
the age cutoff are never touched, and repos whose name parses
(
RepoArtifactNameCodec) to a project id present in the slot's project-directory KV (project:<id>keys) are skipped even when old. Global-scope repos (global{sep}...names) get no KV check — age cutoff only. (Assumption: prd is not a backfill target for now; the script still demands--yes-i-mean-prdthere like erase-data.) - Tokens die with their repo. The 365-day write tokens are repo-scoped; deleting the repo is assumed to invalidate them, so no separate revocation pass. (Assumption based on the API shape; worth confirming with CF.)
Checklist#
- erase-data wipes the slot's Artifacts namespace repos (skipped under
--preserve-auth) — delete-then-relist pass inapps/os/scripts/erase-data.ts, 90s budget like the R2 walk -
artifacts-gcbackfill script with dry-run, age cutoff, live-project skip, delete budget —apps/os/scripts/artifacts-gc.ts, run viapnpm artifacts-gc --env <name> -
docs/preview-resource-gc.mdteardown table gains an Artifacts row - tests for the pure parts (live-project skip / cutoff filtering) —
triageArtifactsRepoPagecovered inartifacts-gc.test.ts
Implementation log#
- Confirmed REST surface via the Cloudflare OpenAPI spec:
GET/POST /accounts/{a}/artifacts/namespaces/{ns}/repos(cursor pagination,sort=created_at|updated_at|last_push_at|name,direction,limit≤ 200),DELETE /accounts/{a}/artifacts/namespaces/{ns}/repos/{name}. scripts/lib/env-context.ts'scf()returnsbody.resultonly (cursor discarded) — the erase-data pass uses delete-then-relist instead of cursors, matching the existing R2/KV wipe idiom and staying robust when concurrent deletes would invalidate a cursor. The backfill needs to advance PAST repos it skips, so it makes raw envelope calls (via the shared 429-retry helper) and uses the cursor only for pure-skip pages, restarting from the head after any deletion.- Live-project detection: the project-directory KV holds
project:<id>keys, so the live set is one prefix-listed key scan — no value fetches. - 2026-09-01/02 while implementing: found 13 files in this worktree (prompt
files + prompt-sections explainer) repeatedly mangled by oxfmt. Root cause
(confirmed by
oxfmt --checkfrom the root worktree): format runs in the ROOT worktree recurse into.claude/worktrees/, where the root-relative ignorePatterns don't match the nested copies. Fixed here by adding**/.claude/worktrees/to.oxfmtrc.json— a rider on this PR since the mangle broke this branch's codegen lint check twice. - Review threads from Iterate Review (2 accepted, 1 pushed back — the "inferable annotation" suggestion broke typecheck) handled in 54d098919.