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.
| g1t.page runs Docker deployments, behind a switch that is off until Cloudflare's container APIs are checked against g1t's account (`CONTAINERS` in the deployments service). Each container app is a Worker of g1t's own in the same Workers for Platforms namespace, so the dispatcher, custom domains, previews and take-downs reach it by name as any app: its `G1tApp` Durable Object holds a Container application (default scheduling policy, one instance, the project's **Container size**, `lite` unless chosen), starts it on the first request with the project's secrets and variables and `PORT` as its environment, waits up to 90 seconds for it to answer on its port, proxies to it, and stops it five minutes after the last request; it charges each minute awake ahead and the bytes it sends, and reports them with a token of its own to `POST /deployments/apps/<app>/meter`, which the API forwards. A build's new `registry` step gets a registry login g1t mints push-only for 15 minutes, with its app's own repository named; g1t runs only an image by digest in that repository, checked with the registry, and never hands a build a login that can pull. Container time is metered at Cloudflare's Containers price plus the margin through new price book meters (vCPU charged as busy while awake, and egress at the most a GB costs anywhere, so g1t never sells below cost), noted for the spend limit as it runs and charged with the month's Deployments line; a paused workspace's container apps stop and start no more. Settings, Deployments gets Build as (Auto, Static site, Workers, Docker), Dockerfile and Container size with each size's price an hour, and says what the last build found with what it was built with ("Static site · Astro", "Container · Dockerfile"), now that the finish report's `framework` is stored. Deploy sandboxes get Docker, `BUILD_AS` and `DOCKERFILE`, and reach `registry.cloudflare.com`; `start_deployment_build` answers `build_as` and `dockerfile` for `g1t/deploy`, which takes them as inputs, as `g1t/detect` does. The Deployments guide has Docker deployments, their prices and how they work with the evidence for it, the limitations page says what is left, and Deploy g1t to Cloudflare lists each live check to make before switching it on. | 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 | }); |