Skip to content

Commit ce41e59

Browse files
committed
Complete provisioned site publication lifecycle
1 parent e57a542 commit ce41e59

4 files changed

Lines changed: 61 additions & 6 deletions

File tree

packages/runtime-cloudflare/src/published-reader.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export function validatePublishedRevision(value: unknown, site: SiteContext = DE
4949
if (![PUBLISHED_REVISION_SCHEMA, PREVIOUS_PUBLISHED_REVISION_SCHEMA, LEGACY_PUBLISHED_REVISION_SCHEMA].includes(revision.schema as string) || !isRevision(revision.revision) || !isRevision(revision.canonicalRevision)
5050
|| typeof revision.publishedAt !== "string" || !Number.isFinite(Date.parse(revision.publishedAt)) || !Array.isArray(revision.routes)
5151
|| (current && (typeof revision.canonicalVersion !== "number" || !Number.isSafeInteger(revision.canonicalVersion) || revision.canonicalVersion < 0))
52-
|| revision.routes.length === 0 || revision.routes.length > MAX_PUBLISHED_ROUTES) throw new Error("Published revision is invalid.")
52+
|| revision.routes.length > MAX_PUBLISHED_ROUTES) throw new Error("Published revision is invalid.")
5353
let previous = ""
5454
const normalizedRoutes: PublishedRoute[] = []
5555
for (const route of revision.routes) {

packages/runtime-cloudflare/src/worker.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,27 @@ async function readCurrentPublication(bucket: R2Bucket, site: SiteContext): Prom
738738
return { publication: validatePublishedRevision(JSON.parse(await object.text()), site), etag: object.etag }
739739
}
740740

741+
async function initializeProvisioningPublication(bucket: R2Bucket, lease: Lease, site: SiteContext): Promise<CurrentPublication> {
742+
const existing = await readCurrentPublication(bucket, site)
743+
if (existing) return existing
744+
if (!lease.pointer) throw new Error("Provisioning publication requires a committed canonical revision.")
745+
const publication: PublishedRevision = {
746+
schema: PUBLISHED_REVISION_SCHEMA,
747+
revision: crypto.randomUUID(),
748+
canonicalRevision: lease.pointer.revision,
749+
canonicalVersion: lease.version,
750+
publishedAt: new Date().toISOString(),
751+
routes: [],
752+
}
753+
const serialized = JSON.stringify(publication)
754+
await putImmutableJson(bucket, publishedRevisionObjectKey(publication.revision, site), serialized)
755+
const created = await bucket.put(siteStorageKeys(site).publishedCurrent, serialized, { onlyIf: { etagDoesNotMatch: "*" }, httpMetadata: { contentType: "application/json" } })
756+
if (created) return { publication, etag: created.etag }
757+
const winner = await readCurrentPublication(bucket, site)
758+
if (!winner) throw new Error("Provisioning publication did not produce a current revision.")
759+
return winner
760+
}
761+
741762
function initializePublicationChanges(php: PHP): void {
742763
php.writeFile(PUBLICATION_CHANGES_PATH, new TextEncoder().encode(JSON.stringify({ all: false, upsert: [], remove: [] })))
743764
}
@@ -1075,7 +1096,7 @@ async function runCoordinatedWordPressRequest(request: Request, env: RuntimeEnv,
10751096
const mutatesCanonicalState = isMutation(request, route)
10761097
const diagnosticsStartedAt = Date.now()
10771098
const retained = new MutationRetainedBytes()
1078-
const currentPublication = mutatesCanonicalState ? await readCurrentPublication(env.WORDPRESS_STATE_BUCKET, site) : null
1099+
let currentPublication = mutatesCanonicalState ? await readCurrentPublication(env.WORDPRESS_STATE_BUCKET, site) : null
10791100
let response: Response | undefined
10801101
let phpResponse: PHPResponseData | undefined
10811102
let responseBodyBytes = 0
@@ -1136,6 +1157,7 @@ async function runCoordinatedWordPressRequest(request: Request, env: RuntimeEnv,
11361157
await discardRuntime(runtime, site)
11371158
runtime = undefined
11381159
if (phpResponse) retained.release(responseBodyBytes)
1160+
if (route === "static-artifact-import" && !currentPublication) currentPublication = await initializeProvisioningPublication(env.WORDPRESS_STATE_BUCKET, lease, site)
11391161
const publicationJob = await enqueuePublicationJob(env.WORDPRESS_STATE_BUCKET, lease, next, currentPublication, publicationChanges, site)
11401162
response.headers.set("x-wp-codebox-publication", publicationJob ? "queued" : "unchanged")
11411163
if (publicationJob) response.headers.set("x-wp-codebox-publication-job", publicationJob.key)

scripts/cloudflare-local-gate.mjs

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ try {
3535
await startWorker()
3636
if (publicProvisioning) {
3737
await assertPublicProvisioning(staticArtifactImport)
38-
console.log("Cloudflare public provisioning gate passed: authenticated artifact staging, idempotent site allocation, scheduled import, cold-restart site read, administrator claim, login, and editable native blocks.")
38+
console.log("Cloudflare public provisioning gate passed: authenticated artifact staging, idempotent site allocation, initial publication, cold-restart site read, administrator claim, native-block edit, automatic republication, and post-restart persistence.")
3939
} else {
4040
await assertFullBootProbe()
4141
await assertWordPressCronDisabled()
@@ -219,7 +219,7 @@ async function assertPublicProvisioning(input) {
219219
const imported = operation?.receipt?.ssiResult
220220
if (operation?.state !== "succeeded" || imported?.status !== "imported" || imported.staticSiteImporterVersion !== "1.3.4" || !imported.themeSlug
221221
|| !imported.pages || Object.keys(imported.pages).length !== expectedPageCount || Object.values(imported.quality ?? {}).some((count) => count !== 0)
222-
|| operation.receipt?.publication?.status !== "none") {
222+
|| operation.receipt?.publication?.status !== "promoted") {
223223
throw new Error(`Public provisioning did not produce a terminal import receipt: ${JSON.stringify(operation)}.`)
224224
}
225225

@@ -236,7 +236,18 @@ async function assertPublicProvisioning(input) {
236236
const claim = JSON.parse(claimBody)
237237
if (!claimResponse.ok || claim.credential?.username !== "admin" || claim.credential?.password !== password) throw new Error(`Administrator claim failed: ${claimResponse.status} ${claimBody}.`)
238238
const adminHtml = await login(claim.credential.password)
239-
await assertImportedArtifactPages(adminHtml, imported)
239+
const importedPages = await assertImportedArtifactPages(adminHtml, imported)
240+
const edited = await updateImportedPage(adminHtml, importedPages.secondary)
241+
const published = await runScheduledPublicationUntil(new URL(edited.route, origin), edited.marker, "provisioned site edit publication", 12)
242+
assertIncludes(published, edited.marker, "provisioned site edit publication")
243+
await stopWorker()
244+
await startWorker()
245+
const persisted = await assertPublishedWordPressPage(new URL(edited.route, origin), "provisioned site edit after restart", ["publication-r2", "publication-edge"])
246+
assertIncludes(persisted, edited.marker, "provisioned site edit after restart")
247+
const restartedAdmin = await assertAuthenticatedDashboard(new URL("/wp-admin/", origin))
248+
const persistedPages = await assertImportedArtifactPages(restartedAdmin, imported)
249+
const persistedEdit = [persistedPages.primary, persistedPages.secondary].find((page) => page.id === edited.id)
250+
if (!persistedEdit?.raw.includes(edited.marker)) throw new Error(`Provisioned site edit was not retained as editable block content after restart: ${JSON.stringify(persistedPages)}.`)
240251
}
241252

242253
async function assertTwoSiteIsolation() {
@@ -420,6 +431,26 @@ async function assertImportedArtifactPages(adminHtml, imported) {
420431
return { primary, secondary: secondary ?? primary }
421432
}
422433

434+
async function updateImportedPage(adminHtml, page) {
435+
const marker = `Provisioned site edit ${Date.now()}`
436+
const content = `<!-- wp:paragraph --><p>${marker}</p><!-- /wp:paragraph -->`
437+
const response = await request(`${origin}/wp-json/wp/v2/pages/${page.id}`, {
438+
method: "POST",
439+
headers: { "content-type": "application/json", "x-wp-nonce": restNonce(adminHtml) },
440+
body: JSON.stringify({ content, status: "publish" }),
441+
})
442+
const body = await response.text()
443+
assertNoPhpDiagnostics(body, "provisioned site page edit")
444+
if (response.status !== 200 || response.headers.get("x-wp-codebox-publication") !== "queued" || !response.headers.get("x-wp-codebox-publication-job")) {
445+
throw new Error(`Provisioned site page edit did not queue publication: status=${response.status} publication=${response.headers.get("x-wp-codebox-publication")} body=${body}.`)
446+
}
447+
const updated = JSON.parse(body)
448+
const raw = updated.content?.raw
449+
if (typeof raw !== "string" || !raw.includes(marker) || /<!-- wp:(?:html|freeform)\b/.test(raw)) throw new Error(`Provisioned site page edit was not retained as native block content: ${body}.`)
450+
const link = new URL(updated.link)
451+
return { id: page.id, marker, route: `${link.pathname}${link.search}` }
452+
}
453+
423454
async function updatePost(adminHtml, post, previousPublicationRevision) {
424455
const title = `Automatically published ${Date.now()}`
425456
const response = await request(`${origin}/wp-json/wp/v2/posts/${post.id}`, {

tests/cloudflare-runtime.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,8 @@ test("Cloudflare publication contracts are host-independent, immutable, and boun
134134
assert.equal(R2_PUBLISHED_CURRENT_KEY, "sites/default/publications/current.json")
135135
assert.equal(validatePublishedRevision({ ...publication, schema: "wp-codebox/published-revision/v1", routes: [{ route: "/", objectKey }] }).routes[0].canonicalRevision, canonicalRevision)
136136
assert.equal(validatePublishedRevision({ ...publication, schema: "wp-codebox/published-revision/v2", canonicalVersion: undefined }).canonicalVersion, 0)
137+
assert.deepEqual(validatePublishedRevision({ ...publication, routes: [] }).routes, [])
138+
assert.throws(() => normalizePublishedRoutes([]), /bounded routes array/)
137139
assert.throws(() => validatePublishedRevision({ ...publication, routes: [{ route: "/", objectKey: "sites/default/pages/foreign.json" }] }), /route is invalid/)
138140
})
139141

@@ -349,7 +351,7 @@ test("Cloudflare runtime injects composable coordinators without moving PHP out
349351
assert.match(worker, /const cachedRuntimes = new Map/)
350352
assert.match(worker, /cachedRuntimes\.get\(site\.id\)/)
351353
assert.match(worker, /promise\.catch\(\(\) =>/)
352-
assert.match(coordinatedRequest, /await discardRuntime\(runtime, site\)\n runtime = undefined[\s\S]{0,100}const publicationJob/)
354+
assert.match(coordinatedRequest, /await discardRuntime\(runtime, site\)\n runtime = undefined[\s\S]*initializeProvisioningPublication[\s\S]*const publicationJob/)
353355
assert.match(worker, /if \(runtime\) await discardRuntime\(runtime, site\)/)
354356
assert.doesNotMatch(worker, /runtime\.pointer = next/)
355357
assert.match(worker, /await abortLease\(coordinator, request\.url, lease\)/)

0 commit comments

Comments
 (0)