Skip to content
228 linesCodeBlameRaw

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 */
5import { test } from "node:test";
6import assert from "node:assert/strict";
7import { registerHooks } from "node:module";
8
9registerHooks({
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
20const { DEPLOYMENT_COSTS } = await import("@g1t/contracts");
21const {
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
35const COSTS = DEPLOYMENT_COSTS.container;
36const DIGEST = "a".repeat(64);
37
38test("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
46test("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
58test("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
75test("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
90test("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
100test("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. */
119async 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
129function 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
159test("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
218test("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.