Skip to content

Commit a05a43b

Browse files
committed
Recover completed publication promotion
1 parent ce41e59 commit a05a43b

3 files changed

Lines changed: 20 additions & 1 deletion

File tree

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface PublishedRevision {
1919
revision: string
2020
canonicalRevision: string
2121
canonicalVersion: number
22+
sourceJob?: string
2223
publishedAt: string
2324
routes: PublishedRoute[]
2425
}
@@ -49,6 +50,7 @@ export function validatePublishedRevision(value: unknown, site: SiteContext = DE
4950
if (![PUBLISHED_REVISION_SCHEMA, PREVIOUS_PUBLISHED_REVISION_SCHEMA, LEGACY_PUBLISHED_REVISION_SCHEMA].includes(revision.schema as string) || !isRevision(revision.revision) || !isRevision(revision.canonicalRevision)
5051
|| typeof revision.publishedAt !== "string" || !Number.isFinite(Date.parse(revision.publishedAt)) || !Array.isArray(revision.routes)
5152
|| (current && (typeof revision.canonicalVersion !== "number" || !Number.isSafeInteger(revision.canonicalVersion) || revision.canonicalVersion < 0))
53+
|| (revision.sourceJob !== undefined && (typeof revision.sourceJob !== "string" || !new RegExp(`^${escapeRegExp(siteStorageKeys(site).publicationJobPrefix)}/[0-9]{20}-[a-f0-9-]{36}\\.json$`).test(revision.sourceJob)))
5254
|| revision.routes.length > MAX_PUBLISHED_ROUTES) throw new Error("Published revision is invalid.")
5355
let previous = ""
5456
const normalizedRoutes: PublishedRoute[] = []

packages/runtime-cloudflare/src/worker.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,17 @@ async function drainNextPublicationJobWhileLeased(env: RuntimeEnv, coordinator:
951951
try {
952952
let progress = await readPublicationProgress(env.WORDPRESS_STATE_BUCKET, job, site)
953953
const current = await readCurrentPublication(env.WORDPRESS_STATE_BUCKET, site)
954+
const recoveredPromotion = current?.publication.canonicalVersion === job.coordinatorVersion
955+
&& current.publication.canonicalRevision === job.canonical.revision
956+
&& current.publication.sourceJob === job.key
957+
if (recoveredPromotion) {
958+
await renewLease()
959+
await putImmutableJson(env.WORDPRESS_STATE_BUCKET, publicationReceiptObjectKey(job, site), JSON.stringify({ status: "promoted", job: job.key, publication: current.publication.revision, recordedAt: new Date().toISOString() }))
960+
await operations?.reconcilePublication(site.id, job.key, "promoted", current.publication.revision)
961+
await renewLease()
962+
await env.WORDPRESS_STATE_BUCKET.delete([job.key, publicationProgressObjectKey(job, site)])
963+
return { schema: "wp-codebox/cloudflare-publication/v1", status: "promoted", jobKey: job.key }
964+
}
954965
if (!current || current.publication.canonicalVersion >= job.coordinatorVersion) {
955966
await renewLease()
956967
await putImmutableJson(env.WORDPRESS_STATE_BUCKET, publicationReceiptObjectKey(job, site), JSON.stringify({ status: "superseded", job: job.key, recordedAt: new Date().toISOString() }))
@@ -990,7 +1001,7 @@ async function drainNextPublicationJobWhileLeased(env: RuntimeEnv, coordinator:
9901001
const routes = new Map(current.publication.routes.map((route) => [route.route, route]))
9911002
for (const route of plan.remove) routes.delete(route)
9921003
for (const route of plan.upsert) routes.set(route, { route, objectKey: await publishedPageObjectKey(job.canonical.revision, route, site), canonicalRevision: job.canonical.revision })
993-
const publication: PublishedRevision = { schema: PUBLISHED_REVISION_SCHEMA, revision: crypto.randomUUID(), canonicalRevision: job.canonical.revision, canonicalVersion: job.coordinatorVersion, publishedAt: new Date().toISOString(), routes: [...routes.values()].sort((left, right) => left.route.localeCompare(right.route)) }
1004+
const publication: PublishedRevision = { schema: PUBLISHED_REVISION_SCHEMA, revision: crypto.randomUUID(), canonicalRevision: job.canonical.revision, canonicalVersion: job.coordinatorVersion, sourceJob: job.key, publishedAt: new Date().toISOString(), routes: [...routes.values()].sort((left, right) => left.route.localeCompare(right.route)) }
9941005
const serialized = JSON.stringify(publication)
9951006
if (new TextEncoder().encode(serialized).byteLength > MAX_PUBLISHED_REVISION_BYTES) throw new Error("Incremental publication exceeds its size budget.")
9961007
await renewLease()

tests/cloudflare-runtime.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ test("Cloudflare publication contracts are host-independent, immutable, and boun
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)
137137
assert.deepEqual(validatePublishedRevision({ ...publication, routes: [] }).routes, [])
138+
const sourceJob = `sites/default/publications/jobs/00000000000000000007-${canonicalRevision}.json`
139+
assert.equal(validatePublishedRevision({ ...publication, sourceJob }).sourceJob, sourceJob)
140+
assert.throws(() => validatePublishedRevision({ ...publication, sourceJob: "sites/default/publications/jobs/foreign.json" }), /invalid/)
138141
assert.throws(() => normalizePublishedRoutes([]), /bounded routes array/)
139142
assert.throws(() => validatePublishedRevision({ ...publication, routes: [{ route: "/", objectKey: "sites/default/pages/foreign.json" }] }), /route is invalid/)
140143
})
@@ -440,9 +443,12 @@ test("Cloudflare runtime injects composable coordinators without moving PHP out
440443
assert.match(worker, /coordinator\.committed\(candidate\.coordinatorVersion\)/)
441444
assert.match(worker, /\.list\(\{ prefix: `\$\{siteStorageKeys\(site\)\.publicationJobPrefix\}\/`, limit: 16 \}\)/)
442445
assert.match(worker, /canonicalVersion: job\.coordinatorVersion/)
446+
assert.match(worker, /sourceJob: job\.key/)
443447
assert.match(worker, /readWordPressPageSnapshot\(env\.WORDPRESS_STATE_BUCKET, objectKey, job\.canonical\.revision, route\)/)
444448
assert.match(worker, /if \(!await readWordPressPageSnapshot\(env\.WORDPRESS_STATE_BUCKET, objectKey, job\.canonical\.revision, route\)\) throw error/)
445449
const publicationDrain = worker.slice(worker.indexOf("async function drainNextPublicationJob"), worker.indexOf("async function compilePublicationRoutes"))
450+
assert.match(publicationDrain, /const recoveredPromotion = current\?\.publication\.canonicalVersion === job\.coordinatorVersion[\s\S]*current\.publication\.canonicalRevision === job\.canonical\.revision[\s\S]*current\.publication\.sourceJob === job\.key/)
451+
assert.match(publicationDrain, /if \(recoveredPromotion\)[\s\S]*status: "promoted"[\s\S]*operations\?\.reconcilePublication\(site\.id, job\.key, "promoted", current\.publication\.revision\)/)
446452
assert.match(publicationDrain, /bootRuntime\(env\.WORDPRESS_STATE_BUCKET, job\.canonical, site\.origin/)
447453
assert.match(publicationDrain, /compilePublicationRoutes\(runtime, \[route\], site\.origin\)/)
448454
assert.doesNotMatch(worker, /runtimeSiteOrigin|require '\/wordpress\/wp-load\.php'; echo get_option\('home'\)/)

0 commit comments

Comments
 (0)