Skip to content

Commit 00f37f7

Browse files
authored
Merge pull request #1959 from Automattic/fix/cloudflare-explicit-publication-changes
Require explicit Cloudflare publication changes
2 parents 99fe7fa + a9b9e8c commit 00f37f7

4 files changed

Lines changed: 8 additions & 7 deletions

File tree

packages/runtime-cloudflare/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ On cold start, the entry Worker uses the acquired pointer to rebuild PHP-WASM's
1010

1111
Canonical browser, health, and mutation boots require the separately managed `WORDPRESS_AUTH_SECRET` Worker secret. The entry Worker derives the eight WordPress auth keys and salts from that secret with a versioned, site-scoped (`default`) SHA-256 domain separator before `wp-load.php`. It never logs, persists, or returns the secret or derived values. Configure it independently from the bootstrap password with `wrangler secret put WORDPRESS_AUTH_SECRET --config packages/runtime-cloudflare/wrangler.jsonc`; rotating `WORDPRESS_ADMIN_PASSWORD` does not rotate authentication salts or invalidate sessions.
1212

13-
After each mutating HTTP request, the entry runtime invokes MDI's explicit request-boundary flush, collects canonical files, stores immutable content-addressed R2 objects and a revision manifest, then commits the new pointer through the held lease. GET, HEAD, and asset requests release without promotion. Failed requests abort their leases; stale leases recover by token/version/expiry checks. The entry isolate can cache one runtime only for the exact acquired pointer revision and exits it after promotion or when another isolate advances the pointer. It does not persist SQLite. Existing manifests are reused when canonical file hashes have not changed. Authenticated requests that produce no canonical MDI changes do not rebuild a publication.
13+
After each mutating HTTP request, the entry runtime invokes MDI's explicit request-boundary flush, collects canonical files, stores immutable content-addressed R2 objects and a revision manifest, then commits the new pointer through the held lease. GET, HEAD, and asset requests release without promotion. Failed requests abort their leases; stale leases recover by token/version/expiry checks. The entry isolate can cache one runtime only for the exact acquired pointer revision and exits it after promotion or when another isolate advances the pointer. It does not persist SQLite. Existing manifests are reused when canonical file hashes have not changed. Canonical mutations such as login session-token writes do not rebuild a publication unless WordPress reports explicit affected routes or a global publication change.
1414

1515
Anonymous HTML responses are stored as create-once, host-independent R2 artifacts under their canonical revision. An authorized `POST ?phase=operator-publish` establishes or replaces a bounded route set. Once that publication exists, WordPress hooks report the public routes affected by post, term, menu, theme, plugin, and relevant option mutations. The Worker renders only those routes in the already-mutated PHP runtime, stages immutable page artifacts and a version 2 publication descriptor, commits canonical state through the coordinator, then conditionally promotes `sites/default/publications/current.json` against its prior R2 ETag. Unchanged route entries retain their older canonical artifacts. A promotion conflict or post-commit R2 failure leaves the prior publication readable and is exposed through `x-wp-codebox-publication`; it never rolls back already-committed WordPress state. Published GET and HEAD requests check a 60-second edge entry and then R2 before coordinator construction, while authenticated, preview, admin, REST, and unpublished routes continue through WordPress. The version 2 reader accepts existing version 1 descriptors during migration. This provides bounded automatic route compilation and local edge invalidation, not a global CDN purge pipeline.
1616

packages/runtime-cloudflare/src/worker.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -399,9 +399,9 @@ function readPublicationChanges(php: PHP): PublicationChanges {
399399
return { all: value.all, upsert, remove }
400400
}
401401

402-
function publicationPlan(current: PublishedRevision, changes: PublicationChanges, fallbackAll = true): { upsert: string[]; remove: string[] } {
402+
function publicationPlan(current: PublishedRevision, changes: PublicationChanges): { upsert: string[]; remove: string[] } {
403403
const existing = current.routes.map(({ route }) => route)
404-
const upsert = changes.all || (fallbackAll && changes.upsert.length === 0 && changes.remove.length === 0) ? existing : changes.upsert
404+
const upsert = changes.all ? existing : changes.upsert
405405
const remove = changes.remove.filter((route) => !upsert.includes(route))
406406
if (new Set([...existing, ...upsert]).size > MAX_PUBLISHED_ROUTES) throw new Error("Incremental publication exceeds its route budget.")
407407
return { upsert, remove }
@@ -536,8 +536,7 @@ async function runCoordinatedWordPressRequest(request: Request, env: RuntimeEnv,
536536
}
537537
if (mutatesCanonicalState) {
538538
if (!canonicalChanges || !publicationChanges) throw new Error("Canonical mutation completed without its persistence evidence.")
539-
const hasCanonicalChanges = canonicalChanges.created.length + canonicalChanges.changed.length + canonicalChanges.deleted.length > 0
540-
const plan = currentPublication ? publicationPlan(currentPublication.publication, publicationChanges, hasCanonicalChanges) : null
539+
const plan = currentPublication ? publicationPlan(currentPublication.publication, publicationChanges) : null
541540
const compiled = plan ? await compilePublicationRoutes(runtime, plan.upsert, new URL(request.url).origin) : []
542541
const next = await persistRuntime(env.WORDPRESS_STATE_BUCKET, runtime, canonicalChanges)
543542
const stagedPublication = currentPublication && plan && (plan.upsert.length || plan.remove.length)
@@ -831,7 +830,7 @@ async function runScheduledWordPressCron(env: RuntimeEnv, coordinator: RevisionC
831830
await discardRuntime(runtime)
832831
break
833832
}
834-
const plan = currentPublication ? publicationPlan(currentPublication.publication, event.publicationChanges, false) : null
833+
const plan = currentPublication ? publicationPlan(currentPublication.publication, event.publicationChanges) : null
835834
const origin = plan && plan.upsert.length ? await runtimeSiteOrigin(runtime) : SITE_URL
836835
const compiled = plan ? await compilePublicationRoutes(runtime, plan.upsert, origin) : []
837836
const next = await persistRuntime(env.WORDPRESS_STATE_BUCKET, runtime, event.canonicalChanges)

scripts/cloudflare-local-gate.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ async function login() {
134134
const form = new URLSearchParams({ log: "admin", pwd: password, redirect_to: `${origin}/wp-admin/`, testcookie: "1", "wp-submit": "Log In" })
135135
const response = await request(`${origin}/wp-login.php`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body: form, redirect: "manual" })
136136
if (![301, 302].includes(response.status)) throw new Error(`Expected login redirect, received ${response.status}: ${await response.text()}`)
137+
if (response.headers.has("x-wp-codebox-publication") || response.headers.has("x-wp-codebox-publication-revision")) throw new Error("Login must not trigger publication work.")
137138
const location = response.headers.get("location")
138139
if (!location?.includes("/wp-admin/")) throw new Error(`Login did not redirect to wp-admin: ${location}`)
139140
const admin = await assertAuthenticatedDashboard(new URL(location, origin))

tests/cloudflare-runtime.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,8 @@ test("Cloudflare runtime injects composable coordinators without moving PHP out
376376
assert.match(worker, /Incremental publication promotion failed after canonical commit/)
377377
assert.ok(coordinatedRequest.indexOf("stageIncrementalPublication") < coordinatedRequest.indexOf("commitLease"))
378378
assert.ok(coordinatedRequest.indexOf("commitLease") < coordinatedRequest.indexOf("safelyPromoteIncrementalPublication"))
379-
assert.match(coordinatedRequest, /hasCanonicalChanges = canonicalChanges\.created\.length \+ canonicalChanges\.changed\.length \+ canonicalChanges\.deleted\.length > 0/)
379+
assert.match(worker, /const upsert = changes\.all \? existing : changes\.upsert/)
380+
assert.doesNotMatch(worker, /fallbackAll|hasCanonicalChanges/)
380381
assert.ok(scheduledCron.indexOf("stageIncrementalPublication") < scheduledCron.indexOf("commitLease"))
381382
assert.ok(scheduledCron.indexOf("commitLease") < scheduledCron.indexOf("safelyPromoteIncrementalPublication"))
382383
assert.match(worker, /https:\/\/wp-codebox-publication\.invalid/)

0 commit comments

Comments
 (0)