Public: anyone can read this; the crew can change it.
The spawned-forge front door runs unchanged on celld: wrangler.json, a pinned binary, and a test through it #493
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/490-the-spawned-forge-front-door-runs-unchan → main 1 commit fork
4 files
changed +201
−0
infra/forge-cloudflare/README.md +33
| @@ -21,3 +21,36 @@ bucket (`use_lockfile`). | ||
| 21 | 21 | **Not here:** the Container body. The Cloudflare provider has no Containers |
| 22 | 22 | resource, so that body is deployed with `wrangler` and its URL given as |
| 23 | 23 | `body_url`. See `docs/SPIKE-CLOUDFLARE.md`. |
| 24 | ||
| 25 | ## Run it on celld | |
| 26 | ||
| 27 | The same `worker.js`, unchanged, runs on [](https://github.com/denoland/celld) | |
| 28 | from `wrangler.json` — no Cloudflare account, no Docker, no bucket. A child forge | |
| 29 | can have its front door on our own machines. | |
| 30 | ||
| 31 | ||
| 32 | bun -e 'import {celld} from "./scripts/celld"; console.log(await celld())' # path of the pinned binary | |
| 33 | echo 'BODY_URL=http://127.0.0.1:8080' > infra/forge-cloudflare/.dev.vars # the body; overrides wrangler.json | |
| 34 | $(that path) dev infra/forge-cloudflare --port 9876 # esbuild must be on PATH | |
| 35 | ||
| 36 | ||
| 37 | `wrangler.json` holds only keys celld accepts, no secret and no real hostname; | |
| 38 | its `BODY_URL` is a loopback placeholder, and `.dev.vars` overrides it. The | |
| 39 | Durable Object migration is `new_sqlite_classes` (celld refuses `new_classes`). | |
| 40 | `scripts/celld.ts` fetches release **v0.6.1** only, checks a SHA-256 written in | |
| 41 | the script, and caches the binary under the temp dir (linux x64/arm64, macOS | |
| 42 | arm64; any other platform is an error). celld is a beta whose releases break | |
| 43 | compatibility, so an upgrade is a deliberate pull: change the version and | |
| 44 | digests together and run `test/forge-cloudflare-celld.test.ts`. That test skips, | |
| 45 | saying why, when the download fails or `esbuild` is missing, and is not in the | |
| 46 | `forge.json` gate. | |
| 47 | ||
| 48 | Measured on a 4-vCPU x86_64 Linux VM (DO-Regular), celld 0.6.1, stub body on | |
| 49 | loopback, esbuild 0.28.2: | |
| 50 | ||
| 51 | | | | | |
| 52 | |---|---| | |
| 53 | | Cold start: `celld dev --clean` spawned → first request answered | ≈ 2.0 s | | |
| 54 | | Next request | ≈ 150 ms | | |
| 55 | | p50 over 100 requests, through the Worker and Durable Object to the body | ≈ 29 ms | | |
| 56 | | p99 | ≈ 126 ms | |
infra/forge-cloudflare/wrangler.json added +10
| @@ -0,0 +1,10 @@ | ||
| 1 | { | |
| 2 | "name": "forge-front-door", | |
| 3 | "main": "worker.js", | |
| 4 | "compatibility_date": "2025-01-01", | |
| 5 | "durable_objects": { | |
| 6 | "bindings": [{ "name": "FORGE", "class_name": "Forge" }] | |
| 7 | }, | |
| 8 | "migrations": [{ "tag": "v1", "new_sqlite_classes": ["Forge"] }], | |
| 9 | "vars": { "BODY_URL": "http://127.0.0.1:8080" } | |
| 10 | } |
scripts/celld.ts added +57
| @@ -0,0 +1,57 @@ | ||
| 1 | /** | |
| 2 | * celld, pinned: fetch the one release this repository has been tried against, | |
| 3 | * check its SHA-256, unpack it into a cache under the temp dir, return its path. | |
| 4 | * A new celld is a deliberate pull: change VERSION and every digest together. | |
| 5 | */ | |
| 6 | import { createHash } from "node:crypto"; | |
| 7 | import { chmodSync, existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs"; | |
| 8 | import { tmpdir } from "node:os"; | |
| 9 | import { join } from "node:path"; | |
| 10 | ||
| 11 | export const VERSION = "v0.6.1"; | |
| 12 | const BASE = `https://github.com/denoland/celld/releases/download/${VERSION}/`; | |
| 13 | ||
| 14 | /** Asset name and SHA-256 of the compressed download, by `${platform}-${arch}`. */ | |
| 15 | const ASSETS: Record<string, { file: string; sha256: string }> = { | |
| 16 | "linux-x64": { | |
| 17 | file: "celld-x86_64-unknown-linux-gnu.gz", | |
| 18 | sha256: "79a8253cff5d4e8a4a9f7a2611e393390f7fe9025f00e88467875b007c44866b", | |
| 19 | }, | |
| 20 | "linux-arm64": { | |
| 21 | file: "celld-aarch64-unknown-linux-gnu.gz", | |
| 22 | sha256: "ab99053bcced225bb5b54f428792260c905b782b8a61947362a12ce3a9c22def", | |
| 23 | }, | |
| 24 | "darwin-arm64": { | |
| 25 | file: "celld-aarch64-apple-darwin.gz", | |
| 26 | sha256: "3033cc4f428433f4239ac616a04092cd4b6c7db19f2f5ba9926c86ec56c34c95", | |
| 27 | }, | |
| 28 | }; | |
| 29 | ||
| 30 | /** The path of the pinned celld binary, fetched on first use. Throws when it cannot be had. */ | |
| 31 | export async function celld( | |
| 32 | platform: string = process.platform, | |
| 33 | arch: string = process.arch, | |
| 34 | ): Promise<string> { | |
| 35 | const asset = ASSETS[`${platform}-${arch}`]; | |
| 36 | if (!asset) | |
| 37 | throw new Error( | |
| 38 | `celld ${VERSION} has no build for ${platform}-${arch}; it has ${Object.keys(ASSETS).join(", ")}`, | |
| 39 | ); | |
| 40 | const dir = join(tmpdir(), `forge-celld-${VERSION}-${asset.sha256.slice(0, 12)}`); | |
| 41 | const bin = join(dir, "celld"); | |
| 42 | if (existsSync(bin)) return bin; | |
| 43 | ||
| 44 | const res = await fetch(BASE + asset.file, { signal: AbortSignal.timeout(120_000) }); | |
| 45 | if (!res.ok) throw new Error(`celld download: ${res.status} for ${asset.file}`); | |
| 46 | const gz = new Uint8Array(await res.arrayBuffer()); | |
| 47 | const sum = createHash("sha256").update(gz).digest("hex"); | |
| 48 | if (sum !== asset.sha256) | |
| 49 | throw new Error(`celld download: SHA-256 ${sum} is not the pinned ${asset.sha256}`); | |
| 50 | ||
| 51 | mkdirSync(dir, { recursive: true }); | |
| 52 | const tmp = `${bin}.${process.pid}.part`; | |
| 53 | writeFileSync(tmp, Bun.gunzipSync(gz)); | |
| 54 | chmodSync(tmp, 0o755); | |
| 55 | renameSync(tmp, bin); | |
| 56 | return bin; | |
| 57 | } |
test/forge-cloudflare-celld.test.ts added +101
| @@ -0,0 +1,101 @@ | ||
| 1 | /** | |
| 2 | * The front door, unchanged, on celld: the same worker.js the Cloudflare test | |
| 3 | * drives, served by `celld dev`. Skips, saying why, when celld cannot be | |
| 4 | * fetched or esbuild is not on PATH. Not in the forge.json gate: it needs a | |
| 5 | * GitHub download. | |
| 6 | */ | |
| 7 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 8 | import { cpSync, mkdtempSync, rmSync } from "node:fs"; | |
| 9 | import { tmpdir } from "node:os"; | |
| 10 | import { join } from "node:path"; | |
| 11 | import { celld } from "../scripts/celld"; | |
| 12 | ||
| 13 | function freePort(): number { | |
| 14 | const s = Bun.serve({ port: 0, fetch: () => new Response() }); | |
| 15 | const port = s.port as number; | |
| 16 | s.stop(true); | |
| 17 | return port; | |
| 18 | } | |
| 19 | ||
| 20 | let skip = ""; | |
| 21 | let bin = ""; | |
| 22 | if (!Bun.which("esbuild")) skip = "esbuild is not on PATH (celld bundles with it)"; | |
| 23 | else | |
| 24 | try { | |
| 25 | bin = await celld(); | |
| 26 | } catch (e) { | |
| 27 | skip = `celld cannot be fetched: ${(e as Error).message}`; | |
| 28 | } | |
| 29 | if (skip) console.warn(`forge-cloudflare-celld skipped: ${skip}`); | |
| 30 | ||
| 31 | const seen: { method: string; path: string; proto: string | null; body: string }[] = []; | |
| 32 | let stub: ReturnType<typeof Bun.serve> | undefined; | |
| 33 | let proc: ReturnType<typeof Bun.spawn> | undefined; | |
| 34 | let dir = ""; | |
| 35 | let base = ""; | |
| 36 | ||
| 37 | async function until(path: string): Promise<Response> { | |
| 38 | const end = Date.now() + 60_000; | |
| 39 | for (;;) { | |
| 40 | try { | |
| 41 | return await fetch(base + path); | |
| 42 | } catch (e) { | |
| 43 | if (Date.now() > end) throw e; | |
| 44 | await Bun.sleep(100); | |
| 45 | } | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | beforeAll(async () => { | |
| 50 | if (skip) return; | |
| 51 | stub = Bun.serve({ | |
| 52 | port: 0, | |
| 53 | hostname: "127.0.0.1", | |
| 54 | async fetch(req) { | |
| 55 | const u = new URL(req.url); | |
| 56 | seen.push({ | |
| 57 | method: req.method, | |
| 58 | path: u.pathname + u.search, | |
| 59 | proto: req.headers.get("x-forwarded-proto"), | |
| 60 | body: await req.text(), | |
| 61 | }); | |
| 62 | return new Response("body ok"); | |
| 63 | }, | |
| 64 | }); | |
| 65 | dir = mkdtempSync(join(tmpdir(), "forge-celld-test-")); | |
| 66 | cpSync(join(import.meta.dir, "../infra/forge-cloudflare"), dir, { recursive: true }); | |
| 67 | // .dev.vars overrides the config's vars, as for wrangler dev | |
| 68 | await Bun.write(join(dir, ".dev.vars"), `BODY_URL=http://127.0.0.1:${stub.port}\n`); | |
| 69 | const port = freePort(); | |
| 70 | base = `http://127.0.0.1:${port}`; | |
| 71 | proc = Bun.spawn([bin, "dev", dir, "--port", String(port), "--clean", "--no-watch"], { | |
| 72 | stdout: "ignore", | |
| 73 | stderr: "ignore", | |
| 74 | }); | |
| 75 | }, 30_000); | |
| 76 | ||
| 77 | afterAll(async () => { | |
| 78 | proc?.kill("SIGKILL"); | |
| 79 | await proc?.exited; | |
| 80 | stub?.stop(true); | |
| 81 | if (dir) rmSync(dir, { recursive: true, force: true }); | |
| 82 | }, 15_000); | |
| 83 | ||
| 84 | describe.skipIf(!!skip)("forge-cloudflare worker on celld", () => { | |
| 85 | test("a GET and a POST reach the body, told https", async () => { | |
| 86 | const get = await until("/a/b?x=1"); | |
| 87 | expect(get.status).toBe(200); | |
| 88 | expect(await get.text()).toBe("body ok"); | |
| 89 | const post = await fetch(`${base}/post`, { method: "POST", body: "hello" }); | |
| 90 | expect(post.status).toBe(200); | |
| 91 | expect(seen[0]).toMatchObject({ method: "GET", path: "/a/b?x=1", proto: "https" }); | |
| 92 | expect(seen.at(-1)).toMatchObject({ method: "POST", path: "/post", proto: "https", body: "hello" }); | |
| 93 | }, 70_000); | |
| 94 | ||
| 95 | test("/healthz is 503 {ok:false} once the body is stopped", async () => { | |
| 96 | stub!.stop(true); | |
| 97 | const r = await fetch(`${base}/healthz`); | |
| 98 | expect(r.status).toBe(503); | |
| 99 | expect(((await r.json()) as { ok: boolean }).ok).toBe(false); | |
| 100 | }); | |
| 101 | }); |
new above
offline