Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge GitHub pull request #8 (claude/cool-newton-7nm8r9): Docker deployments on g1t.page behind a switch that stays off until Cloudflare's container APIs are checked live, workflows a workspace manages in its repositories, tarball and zipball downloads, refs/pull/* kept to g1t alone, framework detection for g1t.page builds, and a workflow's event payload matching GitHub's. | 1 | /** |
| 2 | * A container build through the whole service, on Node: a D1 made of | |
| 3 | * node:sqlite with the service's migrations, the other services as fakes, | |
| 4 | * and Cloudflare's API (and its registry) as a recorded fetch. Run with | |
| 5 | * `--experimental-transform-types` (see package.json). | |
| 6 | */ | |
| 7 | ||
| 8 | import assert from "node:assert/strict"; | |
| 9 | import { createHash } from "node:crypto"; | |
| 10 | import { readdirSync, readFileSync } from "node:fs"; | |
| 11 | import { registerHooks } from "node:module"; | |
| 12 | import { DatabaseSync } from "node:sqlite"; | |
| 13 | import { beforeEach, test } from "node:test"; | |
| 14 | ||
| 15 | registerHooks({ | |
| 16 | resolve(specifier, context, next) { | |
| 17 | try { | |
| 18 | return next(specifier, context); | |
| 19 | } catch (error) { | |
| 20 | if (specifier.startsWith(".")) return next(`${specifier}.ts`, context); | |
| 21 | throw error; | |
| 22 | } | |
| 23 | }, | |
| 24 | }); | |
| 25 | const { default: worker } = await import("./index.ts"); | |
| 26 | ||
| 27 | function d1(db: DatabaseSync) { | |
| 28 | const value = (v: unknown) => (v === undefined ? null : typeof v === "boolean" ? Number(v) : v); | |
| 29 | const statement = (sql: string, params: unknown[] = []) => ({ | |
| 30 | sql, | |
| 31 | params, | |
| 32 | bind: (...args: unknown[]) => statement(sql, args.map(value)), | |
| 33 | async first<T>() { | |
| 34 | return (db.prepare(sql).get(...(params as never[])) as T) ?? null; | |
| 35 | }, | |
| 36 | async all<T>() { | |
| 37 | return { results: db.prepare(sql).all(...(params as never[])) as T[], success: true }; | |
| 38 | }, | |
| 39 | async run() { | |
| 40 | const r = db.prepare(sql).run(...(params as never[])); | |
| 41 | return { success: true, meta: { changes: Number(r.changes) } }; | |
| 42 | }, | |
| 43 | }); | |
| 44 | return { | |
| 45 | prepare: (sql: string) => statement(sql), | |
| 46 | async batch(statements: ReturnType<typeof statement>[]) { | |
| 47 | return statements.map((s) => ({ results: db.prepare(s.sql).all(...(s.params as never[])) })); | |
| 48 | }, | |
| 49 | }; | |
| 50 | } | |
| 51 | ||
| 52 | const PROJECT = "prj_web"; | |
| 53 | const SCRIPT = "web-acme"; | |
| 54 | const TOKEN = "build-token"; | |
| 55 | const DIGEST = "b".repeat(64); | |
| 56 | const IMAGE = `registry.cloudflare.com/acct/g1t-${SCRIPT}@sha256:${DIGEST}`; | |
| 57 | ||
| 58 | function world(containers: string) { | |
| 59 | const sqlite = new DatabaseSync(":memory:"); | |
| 60 | const dir = new URL("../migrations/", import.meta.url); | |
| 61 | for (const file of readdirSync(dir).sort()) sqlite.exec(readFileSync(new URL(file, dir), "utf8")); | |
| 62 | const project = { | |
| 63 | id: PROJECT, | |
| 64 | workspace: "acme", | |
| 65 | slug: "web", | |
| 66 | name: "web", | |
| 67 | primary: true, | |
| 68 | source: { kind: "hosted", repoId: "rep_web", repo: { namespace: "acme", name: "web" }, defaultBranch: "main", rootDir: null }, | |
| 69 | }; | |
| 70 | const ok = (value: unknown) => ({ ok: true, value }); | |
| 71 | const service = (answer: (method: string, args: any) => unknown) => ({ | |
| 72 | async fetch(input: string | Request, init?: RequestInit) { | |
| 73 | const url = typeof input === "string" ? input : input.url; | |
| 74 | const method = new URL(url).pathname.split("/").pop()!; | |
| 75 | const args = init?.body ? JSON.parse(String(init.body)) : {}; | |
| 76 | return Response.json((await answer(method, args)) ?? ok(null)); | |
| 77 | }, | |
| 78 | }); | |
| 79 | const state = { sqlite, charged: [] as any[], pending: [] as any[] }; | |
| 80 | const env = { | |
| 81 | DB: d1(sqlite), | |
| 82 | CLOUDFLARE_API_TOKEN: "test", | |
| 83 | CLOUDFLARE_ACCOUNT_ID: "acct", | |
| 84 | DISPATCH_NAMESPACE: "g1t-deployments", | |
| 85 | SITE: "https://g1t.sh", | |
| 86 | CONTAINERS: containers, | |
| 87 | REPOS: service(() => ok({ isPrivate: true })), | |
| 88 | PROJECTS: service((method) => (method === "by_repo" ? [project] : ok(project))), | |
| 89 | IDENTITY: service(() => ok(null)), | |
| 90 | BILLING: service((method, args) => { | |
| 91 | if (method === "prices") return { prices: [] }; | |
| 92 | if (method === "charge_feature") state.charged.push(args); | |
| 93 | if (method === "note_pending") state.pending.push(args); | |
| 94 | return ok(true); | |
| 95 | }), | |
| 96 | WORK: service(() => ok(null)), | |
| 97 | ACTIONS: service(() => ({ secrets: { DATABASE_URL: "postgres://db" }, variables: { NODE_ENV: "production" } })), | |
| 98 | RUNNER: service(() => ok(true)), | |
| 99 | }; | |
| 100 | return { state, env }; | |
| 101 | } | |
| 102 | ||
| 103 | let w: ReturnType<typeof world>; | |
| 104 | let calls: string[] = []; | |
| 105 | let uploads: Record<string, any>[] = []; | |
| 106 | let applications: { id: string; name: string }[] = []; | |
| 107 | let registryHas = true; | |
| 108 | const realFetch = globalThis.fetch; | |
| 109 | ||
| 110 | function setUp(containers: string) { | |
| 111 | w = world(containers); | |
| 112 | calls = []; | |
| 113 | uploads = []; | |
| 114 | applications = []; | |
| 115 | registryHas = true; | |
| 116 | globalThis.fetch = (async (input: string | Request, init?: RequestInit) => { | |
| 117 | const url = new URL(typeof input === "string" ? input : input.url); | |
| 118 | const method = init?.method ?? "GET"; | |
| 119 | calls.push(`${method} ${url.host === "api.cloudflare.com" ? url.pathname.replace("/client/v4/accounts/acct", "") : url.host + url.pathname}`); | |
| 120 | const result = (value: unknown) => Response.json({ success: true, result: value }); | |
| 121 | if (url.host === "registry.cloudflare.com") return new Response(null, { status: registryHas ? 200 : 404 }); | |
| 122 | if (url.pathname.endsWith("/credentials")) return result({ username: "v1", password: "push-secret", registry_host: "registry.cloudflare.com" }); | |
| 123 | if (method === "PUT" && url.pathname.includes("/scripts/")) { | |
| 124 | const form = init?.body as FormData; | |
| 125 | uploads.push(JSON.parse(String(form.get("metadata")))); | |
| 126 | return result({}); | |
| 127 | } | |
| 128 | if (url.pathname.endsWith("/durable_objects/namespaces")) { | |
| 129 | return result([{ id: "ns1", class: "G1tApp", script: SCRIPT, dispatch_namespace: "g1t-deployments" }]); | |
| 130 | } | |
| 131 | if (url.pathname.endsWith("/containers/applications") && method === "GET") return result(applications); | |
| 132 | if (url.pathname.endsWith("/containers/applications") && method === "POST") { | |
| 133 | applications.push({ id: "app1", name: JSON.parse(String(init?.body)).name }); | |
| 134 | return result({ id: "app1" }); | |
| 135 | } | |
| 136 | if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } }); | |
| 137 | return result([]); | |
| 138 | }) as typeof fetch; | |
| 139 | const db = w.state.sqlite; | |
| 140 | db.prepare( | |
| 141 | `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at, build_as, instance_type) | |
| 142 | VALUES (?, 'rep_web', 'acme', 'web', 1, 1, 1, 7, '2026-10-12T00:00:00Z', 'docker', 'basic')`, | |
| 143 | ).run(PROJECT); | |
| 144 | open("dpl_1"); | |
| 145 | } | |
| 146 | ||
| 147 | /** A build under way, as `open` leaves it. */ | |
| 148 | function open(id: string) { | |
| 149 | w.state.sqlite | |
| 150 | .prepare( | |
| 151 | `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, commit_sha, script, status, token_hash, | |
| 152 | trusted, created_by, created_at) | |
| 153 | VALUES (?, ?, 'acme', 'web', 'rep_web', 'acme/web', 'production', 'abc1234', ?, 'building', ?, 1, 'g1t', ?)`, | |
| 154 | ) | |
| 155 | .run(id, PROJECT, SCRIPT, createHash("sha256").update(TOKEN).digest("hex"), new Date().toISOString()); | |
| 156 | } | |
| 157 | ||
| 158 | beforeEach(() => setUp("on")); | |
| 159 | ||
| 160 | process.on("exit", () => { | |
| 161 | globalThis.fetch = realFetch; | |
| 162 | }); | |
| 163 | ||
| 164 | async function post(path: string, body: unknown): Promise<any> { | |
| 165 | const response = await worker.fetch( | |
| 166 | new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }), | |
| 167 | w.env as never, | |
| 168 | {} as never, | |
| 169 | ); | |
| 170 | return { status: response.status, ...(await response.json()) }; | |
| 171 | } | |
| 172 | ||
| 173 | const rows = (sql: string, ...params: unknown[]) => | |
| 174 | (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row })); | |
| 175 | ||
| 176 | const finish = (container: unknown) => | |
| 177 | post("/jobs/dpl_1/finish", { token: TOKEN, worker: {}, detected: "container", framework: "node", container, buildSeconds: 20 }); | |
| 178 | ||
| 179 | test("while containers are off, the registry step says so in its own words", async () => { | |
| 180 | setUp("off"); | |
| 181 | const answer = await post("/jobs/dpl_1/registry", { token: TOKEN }); | |
| 182 | assert.equal(answer.status, 409); | |
| 183 | assert.equal(answer.error.code, "not_enabled"); | |
| 184 | assert.ok(!calls.some((call) => call.includes("credentials")), "no login is minted"); | |
| 185 | }); | |
| 186 | ||
| 187 | test("a build gets a push-only login for its app's repository, held to its size", async () => { | |
| 188 | const answer = await post("/jobs/dpl_1/registry", { token: TOKEN }); | |
| 189 | assert.deepEqual(answer.value, { | |
| 190 | registry: "registry.cloudflare.com", | |
| 191 | repository: `acct/g1t-${SCRIPT}`, | |
| 192 | tag: "dpl_1", | |
| 193 | username: "v1", | |
| 194 | password: "push-secret", | |
| 195 | maxImageBytes: 4_000_000_000, | |
| 196 | instanceType: "basic", | |
| 197 | }); | |
| 198 | // Another build's token gets nothing. | |
| 199 | assert.equal((await post("/jobs/dpl_1/registry", { token: "nope" })).status, 404); | |
| 200 | }); | |
| 201 | ||
| 202 | test("an image outside the app's repository, or not in the registry, is never run", async () => { | |
| 203 | const other = await finish({ image: `registry.cloudflare.com/acct/g1t-other@sha256:${DIGEST}`, port: 3000 }); | |
| 204 | assert.deepEqual(other.value, false); | |
| 205 | assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_1'")[0].error, /did not report an image by digest/); | |
| 206 | assert.equal(uploads.length, 0); | |
| 207 | open("dpl_2"); | |
| 208 | registryHas = false; | |
| 209 | const missing = await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE } }); | |
| 210 | assert.deepEqual(missing.value, false); | |
| 211 | assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_2'")[0].error, /is not in the registry/); | |
| 212 | }); | |
| 213 | ||
| 214 | test("a container app runs behind g1t's Worker, is metered by its own token, and comes down whole", async () => { | |
| 215 | const done = await finish({ image: IMAGE, port: 3000, sizeBytes: 123 }); | |
| 216 | assert.deepEqual(done.value, true); | |
| 217 | // g1t's Worker, its class, and the project's entries bound to it. | |
| 218 | const [metadata] = uploads; | |
| 219 | assert.deepEqual(metadata.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } }); | |
| 220 | const binding = (name: string) => metadata.bindings.find((b: any) => b.name === name); | |
| 221 | assert.equal(binding("G1T_PORT").text, "3000"); | |
| 222 | assert.equal(binding("G1T_INSTANCE").text, "basic"); | |
| 223 | assert.equal(binding("G1T_METER_URL").text, `https://api.g1t.sh/deployments/apps/${SCRIPT}/meter`); | |
| 224 | assert.deepEqual(binding("DATABASE_URL"), { type: "secret_text", name: "DATABASE_URL", text: "postgres://db" }); | |
| 225 | assert.ok(calls.includes("POST /containers/applications")); | |
| 226 | assert.deepEqual(rows("SELECT runtime, container_app_id, instance_type FROM apps"), [{ runtime: "container", container_app_id: "app1", instance_type: "basic" }]); | |
| 227 | assert.deepEqual(rows("SELECT detected, framework, image, status FROM deployments WHERE id = 'dpl_1'"), [ | |
| 228 | { detected: "container", framework: "node", image: IMAGE, status: "ready" }, | |
| 229 | ]); | |
| 230 | // The next deploy rolls the application out to the new image. | |
| 231 | open("dpl_2"); | |
| 232 | await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE, port: 3000 } }); | |
| 233 | assert.ok(calls.includes("POST /containers/applications/app1/rollouts")); | |
| 234 | ||
| 235 | // Its reports: only with the token it was deployed with, each counter only raised. | |
| 236 | const token = uploads.at(-1)!.bindings.find((b: any) => b.name === "G1T_METER_TOKEN").text; | |
| 237 | const month = new Date().toISOString().slice(0, 7); | |
| 238 | const report = { token, epoch: "epoch-0001", month, awakeSeconds: 3600, egressBytes: 1_000_000_000, instanceType: "basic" }; | |
| 239 | assert.equal((await post(`/apps/${SCRIPT}/meter`, { ...report, token: "wrong" })).status, 404); | |
| 240 | assert.equal((await post(`/apps/${SCRIPT}/meter`, report)).status, 200); | |
| 241 | await post(`/apps/${SCRIPT}/meter`, { ...report, awakeSeconds: 60 }); | |
| 242 | assert.deepEqual(rows("SELECT awake_seconds, egress_bytes FROM container_meters"), [{ awake_seconds: 3600, egress_bytes: 1_000_000_000 }]); | |
| 243 | ||
| 244 | // It is noted for the spend limit as it runs: basic for an hour, and 1 GB at $0.05. | |
| 245 | w.state.sqlite.exec("INSERT OR IGNORE INTO meters (namespace, month) VALUES ('acme', strftime('%Y-%m', 'now'))"); | |
| 246 | await worker.scheduled({} as never, w.env as never); | |
| 247 | const pending = w.state.pending.find((p) => p.workspace === "acme"); | |
| 248 | const second = 0.25 * 20 + 1 * 2.5 + 4 * 0.07 + 1.5625; | |
| 249 | assert.equal(pending.costMicros, Math.ceil(second * 3600 + 50_000)); | |
| 250 | assert.match(pending.detail, /1 container hours and 1 GB sent/); | |
| 251 | ||
| 252 | // Taken down: its container first, then its Worker. | |
| 253 | calls = []; | |
| 254 | await post("/rpc/take_down", { actor: { id: "usr_1", username: "a", kind: "user", workspaces: [{ slug: "acme", role: "owner" }] }, project: { workspace: "acme", slug: "web" }, branch: null }); | |
| 255 | const deleteApp = calls.indexOf("DELETE /containers/applications/app1"); | |
| 256 | const deleteScript = calls.findIndex((call) => call.startsWith("DELETE /workers/dispatch/namespaces/g1t-deployments/scripts/web-acme")); | |
| 257 | assert.ok(deleteApp >= 0 && deleteScript > deleteApp, calls.join("\n")); | |
| 258 | }); | |
| 259 | ||
| 260 | test("a paused container app keeps its class and stops serving", async () => { | |
| 261 | await finish({ image: IMAGE, port: 3000 }); | |
| 262 | uploads = []; | |
| 263 | // Its workspace reached its limit. | |
| 264 | w.env.BILLING = { | |
| 265 | async fetch(input: string | Request, init?: RequestInit) { | |
| 266 | const method = new URL(typeof input === "string" ? input : input.url).pathname.split("/").pop(); | |
| 267 | if (method === "check_limit") return Response.json({ ok: true, value: { state: "stopped" } }); | |
| 268 | if (method === "prices") return Response.json({ prices: [] }); | |
| 269 | return Response.json({ ok: true, value: true, init: !!init }); | |
| 270 | }, | |
| 271 | } as never; | |
| 272 | await worker.scheduled({} as never, w.env as never); | |
| 273 | const paused = uploads.find((u) => u.bindings.some((b: any) => b.name === "G1T_MODE" && b.text === "paused")); | |
| 274 | assert.ok(paused, "the Worker was put back paused"); | |
| 275 | assert.deepEqual(paused.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } }); | |
| 276 | assert.ok(!paused.bindings.some((b: any) => b.name === "DATABASE_URL"), "a paused app holds no secrets"); | |
| 277 | assert.ok(rows("SELECT paused_at FROM apps")[0].paused_at); | |
| 278 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.