You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Shared content-addressed store for R2 deploys (#13)
* Use shared content-addressed store for R2 deploys
Content-addressed p/ files are now stored once in a shared top-level
prefix instead of being copied into every release. Deploys diff against
the previous build locally to identify new files, reducing R2 operations
from ~140k to ~2-5k per deploy.
- SyncToR2 partitions files into shared (p/$hash) and per-release (p2/)
- Local diff skips unchanged shared files (zero R2 ops)
- Migration safety: reads R2 root to detect old layout, forces full
upload when shared prefix doesn't exist yet
- Skip logic requires r2_synced_at on previous build to avoid skipping
files from builds that were never synced
- RewritePackagesJSON only prefixes metadata-url; providers-url and
provider-includes point at shared p/ prefix
- Cleanup preserves shared p/ files (GC deferred to follow-up)
- Removes CopyObject logic (no longer needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix gofmt alignment in r2_test.go
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/r2-deployment.md
+18-20Lines changed: 18 additions & 20 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -4,17 +4,22 @@ WP Composer deploys built repository artifacts to Cloudflare R2 for serving via
4
4
5
5
## Versioned Deploy Model
6
6
7
-
Each deploy uploads all files to an immutable release prefix (`releases/<build-id>/`). The only mutable object in the bucket is the root `packages.json`, which acts as an atomic pointer to the current release.
7
+
Content-addressed `p/` files (containing `$` in the filename) are stored once in a shared top-level prefix and never re-uploaded. Per-release files (`p2/`, `packages.json`, `manifest.json`) go under an immutable release prefix. The only mutable object in the bucket is the root `packages.json`, which acts as an atomic pointer to the current release.
The root `packages.json` is rewritten on each deploy so that `metadata-url`, `providers-url`, and `provider-includes` keys point into the new release prefix. This single PUT is the atomic switch — clients either see the old release or the new one, never a mix.
18
+
Since ~95% of files are content-addressed and identical across releases, the deploy diffs the current build against the previous build locally and only uploads new shared files. This reduces R2 operations from ~140k to ~2-5k (only new `p/` files + all `p2/` files + indexes), with zero R2 calls for unchanged files.
19
+
20
+
On the first deploy with this layout (upgrading from the old release-prefixed model, or no previous build), all shared `p/` files are uploaded. The deploy detects this by reading the current root `packages.json` from R2 — if `providers-url` still points into `releases/`, the shared prefix doesn't exist yet and the local diff is bypassed. Subsequent deploys benefit from the local diff immediately.
21
+
22
+
The root `packages.json` is rewritten on each deploy so that `metadata-url` points into the new release prefix. `providers-url` and `provider-includes` point at the shared top-level `p/` prefix. This single PUT is the atomic switch — clients either see the old release or the new one, never a mix.
18
23
19
24
## Prerequisites
20
25
@@ -63,8 +68,8 @@ Find your account ID in the Cloudflare dashboard under **R2 > Overview**.
63
68
When deploying to R2 (`wpcomposer deploy --to-r2`):
64
69
65
70
1. Validates the build (packages.json and manifest.json must exist).
66
-
2.Uploads all files under `releases/<build-id>/` with appropriate `Cache-Control` headers. Provider and package files upload first; `packages.json` uploads last within the release prefix. Each upload retries up to 3 times with exponential backoff.
67
-
3. Rewrites `packages.json` URL templates to point at the new release prefix.
71
+
2.Diffs shared `p/`files against the previous build directory to find new content-addressed files. Uploads only new `p/`files to the shared top-level prefix (zero R2 ops for unchanged files). Uploads per-release files (`p2/`, indexes) under `releases/<build-id>/`. Each upload retries up to 3 times with exponential backoff.
72
+
3. Rewrites `packages.json`: `metadata-url` points into the release prefix; `providers-url` and `provider-includes`point at the shared `p/` prefix.
68
73
4. Uploads the rewritten `packages.json` as the root — the atomic switch.
69
74
5. Promotes the local build symlink (for rollback capability).
70
75
@@ -80,22 +85,15 @@ When using a Cloudflare custom domain on the R2 bucket, cache behavior is contro
80
85
|---|---|---|
81
86
|`packages.json` (root) |`max-age=300`| Atomic pointer, only mutable object |
82
87
|`releases/*` (everything) |`max-age=31536000, immutable`| Entire release prefix is immutable |
83
-
84
-
Legacy flat-path cases (backward compat during transition):
85
-
86
-
| Path pattern | Cache-Control | Rationale |
87
-
|---|---|---|
88
-
|`manifest.json`|`max-age=300`| Build metadata |
89
-
|`p/*$hash.json`|`max-age=31536000, immutable`| Content-addressed, never changes |
90
-
|`p2/*.json`|`max-age=300`| No hash in URL, must revalidate |
88
+
|`p/*$hash.json` (shared) |`max-age=31536000, immutable`| Content-addressed, never changes |
91
89
92
90
## URL Requirements
93
91
94
92
The generated root `packages.json` on R2 contains prefixed URLs pointing into the current release:
`--r2-cleanup` is required — plain `--cleanup` only removes local build directories. The cleanup reads R2 state directly (no local filesystem dependency), identifies release prefixes, and deletes those outside the keep set. It also deletes legacy flat files (anything not under `releases/` except root `packages.json`).
139
+
`--r2-cleanup` is required — plain `--cleanup` only removes local build directories. The cleanup reads R2 state directly (no local filesystem dependency), identifies release prefixes, and deletes those outside the keep set. It also deletes legacy flat files (anything not under `releases/` except root `packages.json` and shared content-addressed `p/` files). Shared `p/` files are preserved — GC of orphaned shared files is deferred to a future release.
142
140
143
141
The keep set is: live release (from root `packages.json`) + releases within `--grace-hours` + top `--retain` most recent. The retain count has a hard minimum of 5 — even if `--retain` is set lower, at least 5 recent releases are always preserved.
0 commit comments