feat(state): R2-backed ledger/v1 + assets blob transport, migration workflow, and opt-in dual-read (phase 3 foundation) - #899
Merged
Conversation
…tion workflow and opt-in dual-read (phase 3 foundation) Move the last two git-state consumers toward R2 (Cloudflare-canonical phase 3), reusing the STATE_SNAPSHOTS bucket under distinct ledger/v1/ and assets/ prefixes: - Worker: HMAC-authenticated /internal/state/blobs/* endpoints (put, stat, chunk, list, multipart/*) with server-side sha256 verification on single-shot puts, 32MiB range-proxied downloads, and create-only immutability for ledger keys (409 on divergent overwrite, idempotent same-digest re-put; assets may overwrite). - Client: scripts/worker-blobs.ts upload/download/list/materialize helpers on the existing signedPost/signedRequest retry machinery, with a content-addressed digest cache for CI hydration. - Migration: cursor-resumable scripts/migrate-state-blobs.ts plus the dispatchable migrate-state-blobs.yml workflow with digest verification and a JSON summary. - Dual-read: hydrate-state honors CLAWSWEEPER_LEDGER_SOURCE=worker (deny-by-default git) with a loud git fallback when the blob store is unbound or unseeded. No git-lane code is removed and the Worker still needs a deploy before the endpoints exist in production.
| } | ||
|
|
||
| function blobJson(value: unknown, status = 200) { | ||
| return new Response(JSON.stringify(value), { |
…on gate boundary The repo-wide invariant hard-counts setup-state checkouts (30) so new state checkouts must consciously join the coordinator boundary; the new migrate-state-blobs workflow already carries the gate expressions, so admit it by bumping the count to 31.
Contributor
|
ClawSweeper status: review started. I am starting a fresh review of this pull request: feat(state): R2-backed ledger/v1 + assets blob transport, migration workflow, and opt-in dual-read (phase 3 foundation) This is item 1/1 in the current shard. Shard 0/1. This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking. Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted. |
This was referenced Jul 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 3 foundation of the Cloudflare-canonical migration: move the last two git-state consumers — the immutable
ledger/v1tree (~315 MB) and theassetstree (~52 MB) — onto R2 via the Worker, so the git state lane can be deleted in a follow-up. This PR adds the transport, migration tooling, and an opt-in dual-read; defaults stay git and no git-lane code is removed.Worker endpoints (
dashboard/state-blobs.ts, routed indashboard/worker.ts)New HMAC-authenticated routes under
/internal/state/blobs/*, using the sameCLAWSWEEPER_WEBHOOK_SECRETsignature scheme as the existing/internal/state/records/*routes, backed by the existingSTATE_SNAPSHOTSR2 binding (bucketclawsweeper-state-snapshots) under the distinct key prefixesledger/v1/...andassets/...(phase-2 snapshots live under<repoSlug>/<revision>/..., so no collisions):put— single-shot base64 upload (≤24 MiB) with server-side sha256 verification before the object is written.multipart/start|part|complete|abort— chunked upload for larger blobs (8 MiB parts); the claimed digest is stored as client-verified metadata and read back by the migration tooling.stat,list(cursor-paged, digests included), andchunk— 32 MiB range-proxied downloads mirroring the phase-2 snapshot chunk endpoint (R2 bindings cannot presign).ledger/...keys are create-only — a PUT over an existing key with a different digest is a 409ledger_blob_immutable_conflict; a same-digest PUT is an idempotent no-op.assets/...may overwrite. The multipart path re-checks at completion and aborts on conflict.Client (
scripts/worker-blobs.ts)Upload/download/list/materialize helpers on the existing
signedPost/signedRequestretry machinery fromscripts/worker-records.ts(bounded 5xx/network retries).materializeStateBlobsrebuilds theledger/+assets/trees into a worktree with a content-addressed digest cache (CLAWSWEEPER_BLOBS_CACHE_DIR), staged + atomically renamed, and refuses cutover when the store is empty or unbound.Migration (
scripts/migrate-state-blobs.ts+.github/workflows/migrate-state-blobs.yml)Cursor-resumable dispatch workflow (patterned on
backfill-worker-records.yml): sparse-checks-outledger/assetsfrom the state repo and uploads every file with digest verification (single-shot uploads are server-verified; multipart uploads are re-downloaded and hashed —--verify allre-downloads everything). Idempotent: re-runs reportunchanged; a locally diverged immutable ledger file fails loudly with the 409. Prints a JSON summary (files/uploaded/unchanged/verified/bytesUploaded/cursor/nextCursor).Usage: dispatch Migrate state blobs to R2 with
trees=both|ledger|assets, optionalcursor/max_filesto resume a bounded run.Dual-read shim (
scripts/hydrate-state.ts)CLAWSWEEPER_LEDGER_SOURCE=worker(or--ledger-source worker) hydratesledger/+assets/from R2 instead of the git checkout, with a loudWORKER LEDGER CUTOVER REFUSED … FALLING BACK TO GITfallback when the blob store is unbound or unseeded. Deny-by-default: unset ⇒ git, byte-identical to today for every existing caller.Tests
test/worker-state-blobs.test.tsdrives the real Workerfetchhandler end-to-end through the client helpers against an in-memory R2 fake: endpoint auth + fail-closed 503, server-side digest rejection, ledger immutability (incl. the start/complete multipart race), asset overwrite, multipart round-trip, chunk range validation, client retry on transient 502s, cursor-paged listing, migration idempotency/resume/divergence, and hydrate dual-read selection incl. both fallback reasons.Not in this PR / follow-ups
wrangler deployfromdashboard/).CLAWSWEEPER_LEDGER_SOURCE=workerper consumer.git-publish.tslease machinery, publish workflows), and freezing the state repo remain the phase-3 cutover work.