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 | * `@g1t/contracts` imports its modules without extensions, so a resolve | |
| 3 | * hook adds them, as in moves.flow.test.ts. | |
| 4 | */ | |
| 5 | import { test } from "node:test"; | |
| 6 | import assert from "node:assert/strict"; | |
| 7 | import { registerHooks } from "node:module"; | |
| 8 | ||
| 9 | registerHooks({ | |
| 10 | resolve(specifier, context, next) { | |
| 11 | try { | |
| 12 | return next(specifier, context); | |
| 13 | } catch (error) { | |
| 14 | if (specifier.startsWith(".")) return next(`${specifier}.ts`, context); | |
| 15 | throw error; | |
| 16 | } | |
| 17 | }, | |
| 18 | }); | |
| 19 | ||
| 20 | const { DEPLOYMENT_COSTS } = await import("@g1t/contracts"); | |
| 21 | const { | |
| 22 | APP_WORKER, | |
| 23 | appWorkerMetadata, | |
| 24 | applicationRequest, | |
| 25 | containerCost, | |
| 26 | containerOffer, | |
| 27 | maxImageBytes, | |
| 28 | meterReport, | |
| 29 | pushLoginRequest, | |
| 30 | pushedImage, | |
| 31 | repositoryFor, | |
| 32 | rolloutRequest, | |
| 33 | } = await import("./containers.ts"); | |
| 34 | ||
| 35 | const COSTS = DEPLOYMENT_COSTS.container; | |
| 36 | const DIGEST = "a".repeat(64); | |
| 37 | ||
| 38 | test("a build gets a push-only login, for minutes, and pushes to its app's repository", () => { | |
| 39 | assert.deepEqual(pushLoginRequest(), { permissions: ["push"], expiration_minutes: 15 }); | |
| 40 | assert.equal(repositoryFor("acc", "web-acme"), "acc/g1t-web-acme"); | |
| 41 | assert.equal(maxImageBytes("lite"), 2_000_000_000); | |
| 42 | assert.equal(maxImageBytes("standard-4"), 20_000_000_000); | |
| 43 | assert.equal(maxImageBytes("huge"), 2_000_000_000, "an unknown size is the smallest"); | |
| 44 | }); | |
| 45 | ||
| 46 | test("only an image by digest in the app's own repository is run", () => { | |
| 47 | const repo = "acc/g1t-web-acme"; | |
| 48 | const ok = pushedImage({ image: `registry.cloudflare.com/${repo}@sha256:${DIGEST}`, port: 3000, sizeBytes: 12 }, repo); | |
| 49 | assert.deepEqual(ok, { image: `registry.cloudflare.com/${repo}@sha256:${DIGEST}`, digest: `sha256:${DIGEST}`, port: 3000, sizeBytes: 12 }); | |
| 50 | // Another app's repository, another registry, a tag, or no report. | |
| 51 | assert.equal(pushedImage({ image: `registry.cloudflare.com/acc/g1t-other@sha256:${DIGEST}` }, repo), null); | |
| 52 | assert.equal(pushedImage({ image: `docker.io/${repo}@sha256:${DIGEST}` }, repo), null); | |
| 53 | assert.equal(pushedImage({ image: `registry.cloudflare.com/${repo}:latest` }, repo), null); | |
| 54 | assert.equal(pushedImage(null, repo), null); | |
| 55 | assert.equal(pushedImage({ image: `registry.cloudflare.com/${repo}@sha256:${DIGEST}`, port: 0 }, repo)?.port, 8080); | |
| 56 | }); | |
| 57 | ||
| 58 | test("the app's Worker is g1t's, with its settings and the project's entries as bindings", () => { | |
| 59 | const metadata = appWorkerMetadata( | |
| 60 | { mode: "serve", port: 3000, instanceType: "basic", meterUrl: "https://api.g1t.sh/deployments/apps/web/meter", meterToken: "t0ken" }, | |
| 61 | { secrets: { DATABASE_URL: "postgres://x", G1T_MODE: "sneaky" }, variables: { NODE_ENV: "production", DATABASE_URL: "shadowed" } }, | |
| 62 | ["workspace:acme"], | |
| 63 | ); | |
| 64 | const bindings = metadata.bindings as { type: string; name: string; text?: string; class_name?: string }[]; | |
| 65 | const named = (name: string) => bindings.filter((b) => b.name === name); | |
| 66 | assert.deepEqual(named("APP"), [{ type: "durable_object_namespace", name: "APP", class_name: "G1tApp" }]); | |
| 67 | assert.deepEqual(named("G1T_MODE"), [{ type: "plain_text", name: "G1T_MODE", text: "serve" }], "a project's entry never replaces g1t's"); | |
| 68 | assert.deepEqual(named("G1T_METER_TOKEN"), [{ type: "secret_text", name: "G1T_METER_TOKEN", text: "t0ken" }]); | |
| 69 | assert.deepEqual(named("DATABASE_URL"), [{ type: "secret_text", name: "DATABASE_URL", text: "postgres://x" }], "a secret wins over a variable"); | |
| 70 | assert.deepEqual(named("NODE_ENV"), [{ type: "plain_text", name: "NODE_ENV", text: "production" }]); | |
| 71 | assert.deepEqual(metadata.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } }); | |
| 72 | assert.deepEqual(metadata.containers, [{ class_name: "G1tApp" }]); | |
| 73 | }); | |
| 74 | ||
| 75 | test("the container application runs one instance of the app's image at its size", () => { | |
| 76 | const image = `registry.cloudflare.com/acc/g1t-web@sha256:${DIGEST}`; | |
| 77 | assert.deepEqual(applicationRequest("g1t-web", image, "basic", "ns1"), { | |
| 78 | name: "g1t-web", | |
| 79 | scheduling_policy: "default", | |
| 80 | configuration: { image, instance_type: "basic" }, | |
| 81 | instances: 0, | |
| 82 | max_instances: 1, | |
| 83 | durable_objects: { namespace_id: "ns1" }, | |
| 84 | }); | |
| 85 | const rollout = rolloutRequest(image, "nonsense", "dpl_1"); | |
| 86 | assert.deepEqual(rollout.target_configuration, { image, instance_type: "lite" }); | |
| 87 | assert.equal(rollout.step_percentage, 100); | |
| 88 | }); | |
| 89 | ||
| 90 | test("an app's reports are read strictly", () => { | |
| 91 | const good = { token: "t", epoch: "0b8f2c4e-1", month: "2026-10", awakeSeconds: 61.2, egressBytes: 10, instanceType: "basic" }; | |
| 92 | assert.deepEqual(meterReport(good), { ...good, awakeSeconds: 62 }); | |
| 93 | assert.equal(meterReport({ ...good, month: "October" }), null); | |
| 94 | assert.equal(meterReport({ ...good, awakeSeconds: -1 }), null); | |
| 95 | assert.equal(meterReport({ ...good, awakeSeconds: 40 * 24 * 3600 }), null); | |
| 96 | assert.equal(meterReport({ ...good, epoch: "x" }), null); | |
| 97 | assert.equal(meterReport({ ...good, instanceType: "huge" })?.instanceType, "lite"); | |
| 98 | }); | |
| 99 | ||
| 100 | test("container time costs what Cloudflare charges at most, and the margin goes on top", () => { | |
| 101 | // lite: 1/16 vCPU, 0.25 GiB, 2 GB, and the Durable Object, a second. | |
| 102 | const second = (1 / 16) * 20 + 0.25 * 2.5 + 2 * 0.07 + 1.5625; | |
| 103 | const hour = containerCost([{ instance_type: "lite", awake_seconds: 3600, egress_bytes: 0 }], COSTS); | |
| 104 | assert.equal(hour.micros, Math.ceil(second * 3600)); | |
| 105 | assert.equal(hour.detail, "1 container hours and 0 GB sent"); | |
| 106 | const sent = containerCost([{ instance_type: "standard-1", awake_seconds: 0, egress_bytes: 2_000_000_000 }], COSTS); | |
| 107 | assert.equal(sent.micros, 100_000, "2 GB at $0.05"); | |
| 108 | assert.deepEqual(containerCost([], COSTS), { micros: 0, seconds: 0, detail: null }); | |
| 109 | const offer = containerOffer(COSTS, 20, false); | |
| 110 | assert.equal(offer.available, false); | |
| 111 | assert.equal(offer.prices[0].type, "lite"); | |
| 112 | assert.equal(offer.prices[0].hourlyMicros, Math.ceil(second * 3600 * 1.2)); | |
| 113 | assert.equal(offer.egressGbMicros, 60_000); | |
| 114 | // Larger sizes cost more, every one. | |
| 115 | for (let i = 1; i < offer.prices.length; i++) assert.ok(offer.prices[i].hourlyMicros > offer.prices[i - 1].hourlyMicros); | |
| 116 | }); | |
| 117 | ||
| 118 | /** The app's Worker, loaded with a stand-in for the Workers runtime. */ | |
| 119 | async function appWorker() { | |
| 120 | const code = APP_WORKER.replace( | |
| 121 | 'import { DurableObject } from "cloudflare:workers";', | |
| 122 | "class DurableObject { constructor(ctx, env) { this.ctx = ctx; this.env = env; } }", | |
| 123 | ); | |
| 124 | return (await import(`data:text/javascript,${encodeURIComponent(code)}`)) as { | |
| 125 | G1tApp: new (ctx: unknown, env: unknown) => { fetch(r: Request): Promise<Response>; alarm(): Promise<void> }; | |
| 126 | }; | |
| 127 | } | |
| 128 | ||
| 129 | function runtime() { | |
| 130 | const stored = new Map<string, unknown>(); | |
| 131 | const container = { | |
| 132 | running: false, | |
| 133 | started: null as null | { env: Record<string, string>; enableInternet: boolean }, | |
| 134 | destroyed: 0, | |
| 135 | start(options: { env: Record<string, string>; enableInternet: boolean }) { | |
| 136 | this.running = true; | |
| 137 | this.started = options; | |
| 138 | }, | |
| 139 | async destroy() { | |
| 140 | this.running = false; | |
| 141 | this.destroyed++; | |
| 142 | }, | |
| 143 | async setInactivityTimeout() {}, | |
| 144 | getTcpPort(port: number) { | |
| 145 | return { fetch: async () => new Response(`hello from ${port}`) }; | |
| 146 | }, | |
| 147 | }; | |
| 148 | const ctx = { | |
| 149 | container, | |
| 150 | storage: { | |
| 151 | get: async (key: string) => stored.get(key), | |
| 152 | put: async (key: string, value: unknown) => void stored.set(key, structuredClone(value)), | |
| 153 | setAlarm: async (at: number) => void stored.set("alarm", at), | |
| 154 | }, | |
| 155 | }; | |
| 156 | return { ctx, container, stored }; | |
| 157 | } | |
| 158 | ||
| 159 | test("the app's Worker starts its container with the project's entries, meters it and lets it sleep", async (t) => { | |
| 160 | const reports: Record<string, unknown>[] = []; | |
| 161 | const realFetch = globalThis.fetch; | |
| 162 | globalThis.fetch = (async (_url: string, init: RequestInit) => { | |
| 163 | reports.push(JSON.parse(String(init.body))); | |
| 164 | return new Response("{}"); | |
| 165 | }) as typeof fetch; | |
| 166 | t.after(() => { | |
| 167 | globalThis.fetch = realFetch; | |
| 168 | }); | |
| 169 | const realNow = Date.now; | |
| 170 | let now = Date.parse("2026-10-12T10:00:00Z"); | |
| 171 | Date.now = () => now; | |
| 172 | t.after(() => { | |
| 173 | Date.now = realNow; | |
| 174 | }); | |
| 175 | const { G1tApp } = await appWorker(); | |
| 176 | const { ctx, container, stored } = runtime(); | |
| 177 | const env = { | |
| 178 | G1T_MODE: "serve", | |
| 179 | G1T_PORT: "3000", | |
| 180 | G1T_SLEEP_MS: "300000", | |
| 181 | G1T_METER_URL: "https://api.g1t.sh/deployments/apps/web/meter", | |
| 182 | G1T_METER_TOKEN: "t0ken", | |
| 183 | G1T_INSTANCE: "lite", | |
| 184 | APP: {}, | |
| 185 | DATABASE_URL: "postgres://x", | |
| 186 | }; | |
| 187 | const app = new G1tApp(ctx, env); | |
| 188 | const response = await app.fetch(new Request("https://web.g1t.page/")); | |
| 189 | assert.equal(await response.text(), "hello from 3000"); | |
| 190 | assert.deepEqual(container.started, { env: { PORT: "3000", DATABASE_URL: "postgres://x" }, enableInternet: true }); | |
| 191 | // The first minute is charged as it starts. | |
| 192 | assert.equal(reports.at(-1)?.awakeSeconds, 60); | |
| 193 | assert.equal(reports.at(-1)?.token, "t0ken"); | |
| 194 | assert.equal(stored.get("alarm"), now + 60_000); | |
| 195 | // A minute on, with a request in it: another minute. | |
| 196 | now += 60_000; | |
| 197 | await app.alarm(); | |
| 198 | assert.equal(reports.at(-1)?.awakeSeconds, 120); | |
| 199 | assert.equal(reports.at(-1)?.egressBytes, "hello from 3000".length); | |
| 200 | // Five minutes without a request: it is stopped, and nothing more is charged. | |
| 201 | now += 5 * 60_000; | |
| 202 | await app.alarm(); | |
| 203 | assert.equal(container.running, false); | |
| 204 | assert.equal(reports.at(-1)?.awakeSeconds, 120); | |
| 205 | // The next request starts it again, from the minute it starts. | |
| 206 | now += 60 * 60_000; | |
| 207 | await app.fetch(new Request("https://web.g1t.page/")); | |
| 208 | assert.equal(reports.at(-1)?.awakeSeconds, 180); | |
| 209 | // A new month starts again at zero, under the same counter. | |
| 210 | const epoch = reports.at(-1)?.epoch; | |
| 211 | now = Date.parse("2026-11-01T00:00:30Z"); | |
| 212 | await app.alarm(); | |
| 213 | assert.equal(reports.at(-1)?.month, "2026-11"); | |
| 214 | assert.equal(reports.at(-1)?.epoch, epoch); | |
| 215 | assert.equal(reports.at(-2)?.month, "2026-10", "the month that ended is reported first"); | |
| 216 | }); | |
| 217 | ||
| 218 | test("a paused app answers with a notice and its container stops", async () => { | |
| 219 | const { G1tApp } = await appWorker(); | |
| 220 | const { ctx, container } = runtime(); | |
| 221 | container.running = true; | |
| 222 | const app = new G1tApp(ctx, { G1T_MODE: "paused" }); | |
| 223 | const response = await app.fetch(new Request("https://web.g1t.page/")); | |
| 224 | assert.equal(response.status, 402); | |
| 225 | assert.equal(container.destroyed, 1); | |
| 226 | const moved = await new G1tApp(ctx, { G1T_MODE: "redirect", G1T_REDIRECT: "new-acme.g1t.page" }).fetch(new Request("https://web.g1t.page/a?b=1")); | |
| 227 | assert.equal(moved.headers.get("location"), "https://new-acme.g1t.page/a?b=1"); | |
| 228 | }); |
This file's history is long; its oldest lines are credited to the oldest commit read.