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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"postinstall": "node scripts/apply-development-patches.mjs",
"generate:cloudflare-wordpress-runtime-corpus": "tsx scripts/generate-cloudflare-wordpress-runtime-corpus.ts",
"provision:cloudflare-wordpress-runtime-corpus": "node scripts/provision-cloudflare-wordpress-runtime-corpus.mjs",
"provision:cloudflare-d1-coordinator": "node scripts/provision-cloudflare-d1-coordinator.mjs",
"prepare": "npm run build",
"package:wordpress-plugin": "tsx scripts/build-wordpress-plugin-zip.ts",
"release:package": "tsx scripts/package-release-artifact.ts",
Expand All @@ -104,7 +105,7 @@
"generate:cloudflare-canonical-mdi-seed": "npm run generate:cloudflare-mdi-runtime-bundle && php scripts/build-cloudflare-canonical-mdi-seed.php",
"smoke": "tsx scripts/run-smoke.ts",
"test:redaction": "tsx tests/redaction.test.ts",
"test:cloudflare-runtime": "tsx tests/cloudflare-site-context.test.ts && tsx tests/cloudflare-coordinator-site-partitioning.test.ts && tsx tests/cloudflare-runtime.test.ts && node ./node_modules/typescript/bin/tsc -p packages/runtime-cloudflare --noEmit",
"test:cloudflare-runtime": "node --test tests/cloudflare-d1-provisioner.test.mjs && tsx tests/cloudflare-site-context.test.ts && tsx tests/cloudflare-coordinator-site-partitioning.test.ts && tsx tests/cloudflare-runtime.test.ts && node ./node_modules/typescript/bin/tsc -p packages/runtime-cloudflare --noEmit",
"test:cloudflare-wordpress-auth": "tsx tests/cloudflare-wordpress-auth.test.ts",
"test:cloudflare-wordpress-archive-corpus": "tsx tests/cloudflare-wordpress-archive-corpus.test.ts",
"test:agent-task-contracts": "tsx tests/agent-task-contracts.test.ts && tsx tests/agent-task-canonical-evidence.test.ts && npm run test:agent-task-workflow-interface && npm run test:runtime-sources-materialization && tsx tests/agent-task-reusable-workflow.test.ts",
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ SSI is extracted only for import requests, so normal browser, mutation, publicat
2. Run `npm run provision:cloudflare-wordpress-runtime-corpus -- --local --persist-to <directory>` to verify and upload all exact content-addressed artifacts into isolated local R2 storage. For an authorized deployment, run the provisioner with `--remote` and require every upload to succeed before deploying the Worker that imports their manifests.
3. Run `npm run test:cloudflare-runtime` for routing, coordinator composition, canonical-state, artifact validation, source contract, and TypeScript coverage.
4. Run `npm run cloudflare:dry-run` and `npm run cloudflare:dry-run:d1` to compile the Durable Object and D1 profiles without creating Cloudflare resources. The placeholder D1 database ID is for local/dry-run verification; an implementation supplies its provisioned binding at deployment.
Use `npm run provision:cloudflare-d1-coordinator -- --output <production-config>` to idempotently create or resolve the named production database and emit a mode-`0600` deployment config from the checked-in template.
5. Run `npm run cloudflare:local-gate` and `npm run cloudflare:local-gate:d1` for the same isolated workerd workflow through both coordinator implementations. Each gate generates and provisions all artifacts, verifies the selected backend through the state envelope, injects stable test-only admin-password, auth-secret, and operator-token values, uploads and activates a real plugin ZIP, establishes an anonymous homepage and canonical-permalink publication, updates a published post through authenticated REST without immediate rendering, restarts with pending publication work, and proves bounded scheduled draining eventually exposes the updated permalink through coordinator-free R2. The cron gate also proves that a scheduled post becomes readable from the publication path without an operator call. Restart verification requires the immutable R2 publication. A final two-host phase proves isolated mutations, coordinator versions, REST collections, credentials, publications, and caches plus fail-closed unknown-host routing. The remaining coverage includes login, concurrent canonical writes, media, representative frontend/admin/editor assets, PHP diagnostics, session recovery, and a fresh login after restart.

This document describes local candidate verification only. It does not claim remote deployment.
58 changes: 58 additions & 0 deletions scripts/provision-cloudflare-d1-coordinator.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { spawn } from "node:child_process"
import { readFile, writeFile } from "node:fs/promises"
import { resolve } from "node:path"

const args = process.argv.slice(2)
const option = (name, fallback) => {
const index = args.indexOf(name)
return index === -1 ? fallback : args[index + 1]
}
const databaseName = option("--database-name", "wp-codebox-runtime-state")
const binding = option("--binding", "WORDPRESS_STATE_DATABASE")
const templatePath = resolve(option("--template", "packages/runtime-cloudflare/wrangler.d1.jsonc"))
const outputPath = resolve(option("--output"))
const wrangler = option("--wrangler", resolve("node_modules/.bin/wrangler"))
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(databaseName) || !/^[A-Z][A-Z0-9_]*$/.test(binding) || !option("--output")) throw new Error("A safe database name, binding, and --output path are required.")

let databases = await listDatabases()
let matches = databases.filter((database) => database.name === databaseName)
if (matches.length > 1) throw new Error(`Multiple D1 databases are named ${databaseName}.`)
if (!matches.length) {
await run(["d1", "create", databaseName])
databases = await listDatabases()
matches = databases.filter((database) => database.name === databaseName)
}
const database = matches[0]
if (!database || typeof database.uuid !== "string" || !/^[a-f0-9-]{36}$/.test(database.uuid)) throw new Error(`D1 database ${databaseName} was not resolved after provisioning.`)

const template = parseJsonc(await readFile(templatePath, "utf8"))
const configured = template.d1_databases?.find((candidate) => candidate.binding === binding && candidate.database_name === databaseName)
if (!configured) throw new Error(`D1 template does not declare ${binding} for ${databaseName}.`)
configured.database_id = database.uuid
await writeFile(outputPath, `${JSON.stringify(template, null, 2)}\n`, { mode: 0o600 })
process.stdout.write(`${JSON.stringify({ schema: "wp-codebox/cloudflare-d1-provision/v1", databaseName, databaseId: database.uuid, binding, config: outputPath })}\n`)

async function listDatabases() {
const output = await run(["d1", "list", "--json"])
const parsed = JSON.parse(output)
if (!Array.isArray(parsed)) throw new Error("Wrangler returned an invalid D1 database list.")
return parsed
}

async function run(command) {
return new Promise((resolveRun, reject) => {
const child = spawn(wrangler, command, { cwd: process.cwd(), stdio: ["ignore", "pipe", "pipe"] })
let stdout = ""
let stderr = ""
child.stdout.setEncoding("utf8")
child.stderr.setEncoding("utf8")
child.stdout.on("data", (chunk) => { stdout += chunk })
child.stderr.on("data", (chunk) => { stderr += chunk })
child.on("error", reject)
child.on("exit", (code) => code === 0 ? resolveRun(stdout) : reject(new Error(`Wrangler ${command.slice(0, 2).join(" ")} failed with status ${code}: ${stderr}`)))
})
}

function parseJsonc(value) {
return JSON.parse(value.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "").replace(/,\s*([}\]])/g, "$1"))
}
49 changes: 49 additions & 0 deletions tests/cloudflare-d1-provisioner.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import assert from "node:assert/strict"
import { chmod, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join, resolve } from "node:path"
import { spawn } from "node:child_process"
import test from "node:test"

const script = resolve("scripts/provision-cloudflare-d1-coordinator.mjs")

test("D1 provisioner creates once and emits a deployable config deterministically", async () => {
const directory = await mkdtemp(join(tmpdir(), "wp-codebox-d1-provision-"))
try {
const wrangler = join(directory, "wrangler")
const state = join(directory, "state")
const calls = join(directory, "calls")
const template = join(directory, "wrangler.d1.jsonc")
const output = join(directory, "production.json")
await writeFile(state, "missing")
await writeFile(calls, "")
await writeFile(template, `// template\n{"name":"worker","main":"src/worker-d1.ts","d1_databases":[{"binding":"WORDPRESS_STATE_DATABASE","database_name":"wp-codebox-runtime-state","database_id":"00000000-0000-0000-0000-000000000000"}],}`)
await writeFile(wrangler, `#!${process.execPath}\nimport { appendFileSync, readFileSync, writeFileSync } from "node:fs"; const args=process.argv.slice(2); appendFileSync(${JSON.stringify(calls)}, JSON.stringify(args)+"\\n"); if(args[1]==="list") process.stdout.write(readFileSync(${JSON.stringify(state)},"utf8")==="created"?JSON.stringify([{name:"wp-codebox-runtime-state",uuid:"11111111-2222-3333-4444-555555555555"}]):"[]"); else if(args[1]==="create") writeFileSync(${JSON.stringify(state)},"created");`)
await chmod(wrangler, 0o755)

const first = await run(["--template", template, "--output", output, "--wrangler", wrangler])
const second = await run(["--template", template, "--output", output, "--wrangler", wrangler])
assert.equal(first.databaseId, "11111111-2222-3333-4444-555555555555")
assert.deepEqual(second, first)
const config = JSON.parse(await readFile(output, "utf8"))
assert.equal(config.d1_databases[0].database_id, first.databaseId)
const invocations = (await readFile(calls, "utf8")).trim().split("\n").map(JSON.parse)
assert.equal(invocations.filter((args) => args[1] === "create").length, 1)
} finally {
await rm(directory, { recursive: true, force: true })
}
})

function run(args) {
return new Promise((resolveRun, reject) => {
const child = spawn(process.execPath, [script, ...args], { stdio: ["ignore", "pipe", "pipe"] })
let stdout = ""
let stderr = ""
child.stdout.setEncoding("utf8")
child.stderr.setEncoding("utf8")
child.stdout.on("data", (chunk) => { stdout += chunk })
child.stderr.on("data", (chunk) => { stderr += chunk })
child.on("error", reject)
child.on("exit", (code) => code === 0 ? resolveRun(JSON.parse(stdout)) : reject(new Error(stderr)))
})
}
Loading