Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
"wp-codebox:source": "node bin/wp-codebox-source.mjs",
"generate:browser-fanout-aggregation-runtime": "tsx scripts/generate-browser-fanout-aggregation-runtime.ts",
"generate:fanout-aggregation-contract": "tsx scripts/generate-fanout-aggregation-contract-fixture.ts",
"generate:cloudflare-mdi-runtime-bundle": "node scripts/build-cloudflare-mdi-runtime-bundle.mjs",
"smoke": "tsx scripts/run-smoke.ts",
"test:redaction": "tsx tests/redaction.test.ts",
"test:cloudflare-runtime": "tsx tests/cloudflare-runtime.test.ts && node ./node_modules/typescript/bin/tsc -p packages/runtime-cloudflare --noEmit",
Expand Down
4 changes: 3 additions & 1 deletion packages/runtime-cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

This additive integration is the first acceptance gate for [wp-codebox#1838](https://github.com/Automattic/wp-codebox/issues/1838). It compiles the current PHP 8.5 Asyncify Wasm asset for workerd, boots WordPress through Playground with the current SQLite integration release, executes PHP, and returns a Codebox runtime-command-result envelope containing the stable `wp-codebox/cloudflare-runtime-health/v1` health payload.

The Worker owns Cloudflare transport and isolate lifecycle. Full WordPress initialization requires the paid-plan five-minute CPU allowance, declared as `limits.cpu_ms: 300000`; Cloudflare's fixed 128 MB isolate memory limit remains unchanged. This gate uses one Playground PHP instance per isolate; Durable Objects will serialize site mutation in a later gate. The response identifies the generic `wordpress-playground` backend and `wordpress` environment; callers do not receive Cloudflare binding or limit details. Runtime snapshots, restore, R2, serialization, and cold restoration are deferred to later gates.
The Worker owns Cloudflare transport, PHP-WASM execution, and the caller-owned disposable SQLite cache. [MDI PR #126](https://github.com/Automattic/markdown-database-integration/pull/126), pinned at `94b9f875ffb8402d5e8eb726893a12324e20f45c`, supplies the constrained public primary runtime: normal MDI driver SQL writes are explicitly flushed to deterministic relative Markdown/JSON paths. R2 stores immutable canonical revisions and the current pointer; the Durable Object owns only the persisted lease, base-revision validation, and CAS pointer promotion. SQLite is never uploaded. This preserves cold reconstruction from canonical R2 files and concurrent mutation serialization without expanding MDI's storage-only boundary.

Existing evidence covers full WordPress initialization and canonical R2 revision behavior. This update changes the source relationship from ad hoc writes to MDI's public constrained runtime, adds source-level bundle and mutation guards, and supports local packaging verification. It does not claim a new remote deployment.

## Verification

Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
1 change: 1 addition & 0 deletions packages/runtime-cloudflare/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"dependencies": {
"@automattic/wp-codebox-core": "file:../runtime-core",
"@php-wasm/stream-compression": "^3.1.45",
"@php-wasm/universal": "3.1.45",
"@php-wasm/web-8-5": "3.1.45",
"@wp-playground/wordpress": "3.1.45"
Expand Down
8 changes: 7 additions & 1 deletion packages/runtime-cloudflare/src/php-wasm-universal.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
declare module "@php-wasm/universal" {
export class PHP {
constructor(runtimeId: number)
run(request: { code: string }): Promise<{ text: string }>
run(request: { code: string }): Promise<{ bytes: Uint8Array; text: string }>
isDir(path: string): boolean
listFiles(path: string): string[]
mkdir(path: string): void
readFileAsBuffer(path: string): Uint8Array
writeFile(path: string, data: Uint8Array): void
exit(code?: number): void
}

export function loadPHPRuntime(loader: {
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime-cloudflare/src/sqlite.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module "*.sqlite" {
const database: ArrayBuffer
export default database
}
638 changes: 630 additions & 8 deletions packages/runtime-cloudflare/src/worker.ts

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/runtime-cloudflare/src/zip.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module "*.zip" {
const archive: ArrayBuffer
export default archive
}
16 changes: 15 additions & 1 deletion packages/runtime-cloudflare/wrangler.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,19 @@
"compatibility_date": "2026-07-18",
"compatibility_flags": ["nodejs_compat"],
"limits": { "cpu_ms": 300000 },
"rules": [{ "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false }]
"r2_buckets": [
{ "binding": "WORDPRESS_STATE_BUCKET", "bucket_name": "wp-codebox-runtime-chubes" }
],
"durable_objects": {
"bindings": [
{ "name": "WORDPRESS_STATE", "class_name": "WordPressStateCoordinator" }
]
},
"migrations": [
{ "tag": "v1", "new_sqlite_classes": ["WordPressStateCoordinator"] }
],
"rules": [
{ "type": "CompiledWasm", "globs": ["**/*.wasm"], "fallthrough": false },
{ "type": "Data", "globs": ["**/*.sqlite", "**/*-runtime.zip"], "fallthrough": false }
]
}
33 changes: 33 additions & 0 deletions scripts/build-cloudflare-mdi-runtime-bundle.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { writeFile } from "node:fs/promises"
import { decodeZip, encodeZip } from "@php-wasm/stream-compression"

const revision = "94b9f875ffb8402d5e8eb726893a12324e20f45c"
const archiveUrl = `https://codeload.github.com/Automattic/markdown-database-integration/zip/${revision}`
const output = new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url)
const response = await fetch(archiveUrl)
if (!response.ok || !response.body) throw new Error(`Unable to fetch Markdown Database Integration: ${response.status}.`)

const runtimePaths = new Set([
"db.php",
"inc/class-wp-markdown-db.php",
"inc/class-wp-markdown-driver.php",
"inc/class-wp-markdown-frontmatter-profiles.php",
"inc/class-wp-markdown-loader.php",
"inc/class-wp-markdown-primary-storage-runtime.php",
"inc/class-wp-markdown-search.php",
"inc/class-wp-markdown-storage.php",
"inc/class-wp-markdown-write-engine.php",
])
const runtimeFiles = []
for await (const entry of decodeZip(response.body)) {
const separator = entry.name.indexOf("/")
const relative = separator === -1 ? "" : entry.name.slice(separator + 1)
if (!runtimePaths.has(relative)) continue
runtimeFiles.push(new File([await entry.arrayBuffer()], relative, { lastModified: 0 }))
}
if (runtimeFiles.length !== runtimePaths.size) throw new Error(`Expected ${runtimePaths.size} MDI runtime files, received ${runtimeFiles.length}.`)

runtimeFiles.sort((left, right) => left.name.localeCompare(right.name))
const archive = await new Response(encodeZip(runtimeFiles)).arrayBuffer()
await writeFile(output, new Uint8Array(archive))
console.log(`Bundled ${runtimeFiles.length} MDI runtime files from ${revision} (${archive.byteLength} bytes).`)
57 changes: 57 additions & 0 deletions tests/cloudflare-runtime.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import assert from "node:assert/strict"
import { readFile } from "node:fs/promises"
import test from "node:test"
import { decodeZip } from "@php-wasm/stream-compression"
import { RUNTIME_COMMAND_RESULT_SCHEMA } from "../packages/runtime-core/src/runtime-contracts.js"
import { CLOUDFLARE_RUNTIME_HEALTH_MARKER, CLOUDFLARE_RUNTIME_HEALTH_SCHEMA, cloudflareRuntimeHealthResponse } from "../packages/runtime-cloudflare/src/health-envelope.js"

Expand Down Expand Up @@ -28,3 +29,59 @@ test("Cloudflare runtime declares the paid-plan WordPress boot CPU budget", asyn
const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as { limits?: { cpu_ms?: number } }
assert.equal(config.limits?.cpu_ms, 300_000)
})

test("Cloudflare runtime packages the disposable WordPress install seed", async () => {
const config = JSON.parse(await readFile(new URL("../packages/runtime-cloudflare/wrangler.jsonc", import.meta.url), "utf8")) as {
rules?: Array<{ type?: string; globs?: string[] }>
r2_buckets?: Array<{ binding?: string; bucket_name?: string }>
durable_objects?: { bindings?: Array<{ name?: string; class_name?: string }> }
migrations?: Array<{ new_sqlite_classes?: string[] }>
}
const seed = await readFile(new URL("../packages/runtime-cloudflare/assets/wordpress-install-seed.sqlite", import.meta.url))
const markdownIndex = await readFile(new URL("../packages/runtime-cloudflare/assets/markdown-primary-bootstrap-index.sqlite", import.meta.url))
const markdownRuntime = await readFile(new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url))

assert.equal(seed.subarray(0, 16).toString(), "SQLite format 3\0")
assert.equal(markdownIndex.subarray(0, 16).toString(), "SQLite format 3\0")
assert.equal(markdownRuntime.subarray(0, 4).toString("hex"), "504b0304")
assert.ok(config.rules?.some((rule) => rule.type === "Data" && rule.globs?.includes("**/*.sqlite")))
assert.ok(config.rules?.some((rule) => rule.type === "Data" && rule.globs?.includes("**/*-runtime.zip")))
assert.deepEqual(config.r2_buckets, [{ binding: "WORDPRESS_STATE_BUCKET", bucket_name: "wp-codebox-runtime-chubes" }])
assert.deepEqual(config.durable_objects?.bindings, [{ name: "WORDPRESS_STATE", class_name: "WordPressStateCoordinator" }])
assert.ok(config.migrations?.some((migration) => migration.new_sqlite_classes?.includes("WordPressStateCoordinator")))
})

test("Cloudflare runtime pins and bundles the public constrained MDI runtime", async () => {
const revision = "94b9f875ffb8402d5e8eb726893a12324e20f45c"
const generator = await readFile(new URL("../scripts/build-cloudflare-mdi-runtime-bundle.mjs", import.meta.url), "utf8")
const worker = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
const runtime = await readFile(new URL("../packages/runtime-cloudflare/assets/markdown-database-integration-runtime.zip", import.meta.url))
const names: string[] = []
for await (const entry of decodeZip(new Blob([runtime]).stream())) names.push(entry.name)

assert.match(generator, new RegExp(`const revision = "${revision}"`))
assert.match(worker, new RegExp(`MARKDOWN_DATABASE_INTEGRATION_REVISION = "${revision}"`))
assert.deepEqual(names.sort(), [
"db.php",
"inc/class-wp-markdown-db.php",
"inc/class-wp-markdown-driver.php",
"inc/class-wp-markdown-frontmatter-profiles.php",
"inc/class-wp-markdown-loader.php",
"inc/class-wp-markdown-primary-storage-runtime.php",
"inc/class-wp-markdown-search.php",
"inc/class-wp-markdown-storage.php",
"inc/class-wp-markdown-write-engine.php",
])
})

test("serialized Cloudflare mutations use the public MDI runtime and its flush paths", async () => {
const source = await readFile(new URL("../packages/runtime-cloudflare/src/worker.ts", import.meta.url), "utf8")
const mutation = source.slice(source.indexOf("const SERIALIZED_MARKDOWN_MUTATION_CODE"), source.indexOf("let bootPromise"))

assert.match(mutation, /WP_Markdown_Primary_Storage_Runtime::bootstrap/)
assert.match(mutation, /new WP_SQLite_Connection\(\['pdo' => \$GLOBALS\['@pdo'\], 'path' => FQDB\]\)/)
assert.match(mutation, /\$runtime->get_driver\(\)/)
assert.match(mutation, /\$runtime->flush\(\)/)
assert.doesNotMatch(mutation, /\$wpdb->dbh|\{\$\{prefix\}|WP_Markdown_Storage|write_post|file_put_contents|wp_codebox_mdi_revision\.json/)
assert.match(source, /validateMarkdownChanges\(mutation\.canonicalChanges\)/)
})
Loading