Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

332 linesCodeBlameRaw
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
8import assert from "node:assert/strict";
9import { createHash } from "node:crypto";
10import { readdirSync, readFileSync } from "node:fs";
11import { registerHooks } from "node:module";
12import { DatabaseSync } from "node:sqlite";
13import { beforeEach, test } from "node:test";
14
15registerHooks({
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});
25const { default: worker } = await import("./index.ts");
26
27function 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
52const PROJECT = "prj_web";
53const SCRIPT = "web-acme";
54const TOKEN = "build-token";
55const DIGEST = "b".repeat(64);
56const IMAGE = `registry.cloudflare.com/acct/g1t-${SCRIPT}@sha256:${DIGEST}`;
57
58function 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
103let w: ReturnType<typeof world>;
104let calls: string[] = [];
105let uploads: Record<string, any>[] = [];
106let applications: { id: string; name: string }[] = [];
107let registryHas = true;
108/** The tags the registry holds in the app's repository. */
109let registryTags: string[] = [];
110/** The namespace the app's Worker's `APP` binding names, if Cloudflare says. */
111let boundNamespace: string | null = null;
112let applicationBodies: any[] = [];
113const realFetch = globalThis.fetch;
114
115function setUp(containers: string) {
116 w = world(containers);
117 calls = [];
118 uploads = [];
119 applications = [];
120 registryHas = true;
121 registryTags = [];
122 boundNamespace = null;
123 applicationBodies = [];
124 globalThis.fetch = (async (input: string | Request, init?: RequestInit) => {
125 const url = new URL(typeof input === "string" ? input : input.url);
126 const method = init?.method ?? "GET";
127 calls.push(`${method} ${url.host === "api.cloudflare.com" ? url.pathname.replace("/client/v4/accounts/acct", "") : url.host + url.pathname}`);
128 const result = (value: unknown) => Response.json({ success: true, result: value });
129 if (url.host === "registry.cloudflare.com") {
130 if (url.pathname.endsWith("/tags/list")) return Response.json({ name: `acct/g1t-${SCRIPT}`, tags: registryTags });
131 if (method === "DELETE") {
132 const tag = url.pathname.split("/").pop()!;
133 registryTags = registryTags.filter((t) => t !== tag);
134 return new Response(null, { status: 202 });
135 }
136 if (url.pathname === "/v2/gc/layers") return new Response(null, { status: 200 });
137 return new Response(null, { status: registryHas ? 200 : 404 });
138 }
139 if (url.pathname.endsWith("/credentials")) return result({ username: "v1", password: "push-secret", registry_host: "registry.cloudflare.com" });
140 if (method === "PUT" && url.pathname.includes("/scripts/")) {
141 const form = init?.body as FormData;
142 uploads.push(JSON.parse(String(form.get("metadata"))));
143 return result({});
144 }
145 if (url.pathname.endsWith(`/scripts/${SCRIPT}/bindings`)) {
146 return result(boundNamespace ? [{ type: "durable_object_namespace", name: "APP", class_name: "G1tApp", namespace_id: boundNamespace }] : []);
147 }
148 if (url.pathname.endsWith("/durable_objects/namespaces")) {
149 return result([{ id: "ns1", class: "G1tApp", script: SCRIPT, dispatch_namespace: "g1t-deployments" }]);
150 }
151 if (url.pathname.endsWith("/containers/applications") && method === "GET") return result(applications);
152 if (url.pathname.endsWith("/containers/applications") && method === "POST") {
153 applicationBodies.push(JSON.parse(String(init?.body)));
154 applications.push({ id: "app1", name: JSON.parse(String(init?.body)).name });
155 return result({ id: "app1" });
156 }
157 if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } });
158 return result([]);
159 }) as typeof fetch;
160 const db = w.state.sqlite;
161 db.prepare(
162 `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at, build_as, instance_type)
163 VALUES (?, 'rep_web', 'acme', 'web', 1, 1, 1, 7, '2026-10-12T00:00:00Z', 'docker', 'basic')`,
164 ).run(PROJECT);
165 open("dpl_1");
166}
167
168/** A build under way, as `open` leaves it. */
169function open(id: string) {
170 w.state.sqlite
171 .prepare(
172 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, commit_sha, script, status, token_hash,
173 trusted, created_by, created_at)
174 VALUES (?, ?, 'acme', 'web', 'rep_web', 'acme/web', 'production', 'abc1234', ?, 'building', ?, 1, 'g1t', ?)`,
175 )
176 .run(id, PROJECT, SCRIPT, createHash("sha256").update(TOKEN).digest("hex"), new Date().toISOString());
177}
178
179beforeEach(() => setUp("on"));
180
181process.on("exit", () => {
182 globalThis.fetch = realFetch;
183});
184
185async function post(path: string, body: unknown): Promise<any> {
186 const response = await worker.fetch(new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }), w.env as never, {} as never);
187 return { status: response.status, ...(await response.json()) };
188}
189
190const rows = (sql: string, ...params: unknown[]) => (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
191
192const finish = (container: unknown) =>
193 post("/jobs/dpl_1/finish", { token: TOKEN, worker: {}, detected: "container", framework: "node", container, buildSeconds: 20 });
194
195test("while containers are off, the registry step says so in its own words", async () => {
196 setUp("off");
197 const answer = await post("/jobs/dpl_1/registry", { token: TOKEN });
198 assert.equal(answer.status, 409);
199 assert.equal(answer.error.code, "not_enabled");
200 assert.ok(!calls.some((call) => call.includes("credentials")), "no login is minted");
201});
202
203test("a build gets a push-only login for its app's repository, held to its size", async () => {
204 const answer = await post("/jobs/dpl_1/registry", { token: TOKEN });
205 assert.deepEqual(answer.value, {
206 registry: "registry.cloudflare.com",
207 repository: `acct/g1t-${SCRIPT}`,
208 tag: "dpl_1",
209 username: "v1",
210 password: "push-secret",
211 maxImageBytes: 4_000_000_000,
212 instanceType: "basic",
213 });
214 // Another build's token gets nothing.
215 assert.equal((await post("/jobs/dpl_1/registry", { token: "nope" })).status, 404);
216});
217
218test("an image outside the app's repository, or not in the registry, is never run", async () => {
219 const other = await finish({ image: `registry.cloudflare.com/acct/g1t-other@sha256:${DIGEST}`, port: 3000 });
220 assert.deepEqual(other.value, false);
221 assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_1'")[0].error, /did not report an image by digest/);
222 assert.equal(uploads.length, 0);
223 open("dpl_2");
224 registryHas = false;
225 const missing = await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE } });
226 assert.deepEqual(missing.value, false);
227 assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_2'")[0].error, /is not in the registry/);
228});
229
230test("a container app runs behind g1t's Worker, is metered by its own token, and comes down whole", async () => {
231 const done = await finish({ image: IMAGE, port: 3000, sizeBytes: 123 });
232 assert.deepEqual(done.value, true);
233 // g1t's Worker, its class, and the project's entries bound to it.
234 const [metadata] = uploads;
235 assert.deepEqual(metadata.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } });
236 const binding = (name: string) => metadata.bindings.find((b: any) => b.name === name);
237 assert.equal(binding("G1T_PORT").text, "3000");
238 assert.equal(binding("G1T_INSTANCE").text, "basic");
239 assert.equal(binding("G1T_METER_URL").text, `https://api.g1t.sh/deployments/apps/${SCRIPT}/meter`);
240 assert.deepEqual(binding("DATABASE_URL"), { type: "secret_text", name: "DATABASE_URL", text: "postgres://db" });
241 assert.ok(calls.includes("POST /containers/applications"));
242 assert.deepEqual(rows("SELECT runtime, container_app_id, instance_type FROM apps"), [
243 { runtime: "container", container_app_id: "app1", instance_type: "basic" },
244 ]);
245 assert.deepEqual(rows("SELECT detected, framework, image, status FROM deployments WHERE id = 'dpl_1'"), [
246 { detected: "container", framework: "node", image: IMAGE, status: "ready" },
247 ]);
248 // The next deploy rolls the application out to the new image.
249 open("dpl_2");
250 await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE, port: 3000 } });
251 assert.ok(calls.includes("POST /containers/applications/app1/rollouts"));
252
253 // Its reports: only with the token it was deployed with, each counter only raised.
254 const token = uploads.at(-1)!.bindings.find((b: any) => b.name === "G1T_METER_TOKEN").text;
255 const month = new Date().toISOString().slice(0, 7);
256 const report = { token, epoch: "epoch-0001", month, awakeSeconds: 3600, egressBytes: 1_000_000_000, instanceType: "basic" };
257 assert.equal((await post(`/apps/${SCRIPT}/meter`, { ...report, token: "wrong" })).status, 404);
258 assert.equal((await post(`/apps/${SCRIPT}/meter`, report)).status, 200);
259 await post(`/apps/${SCRIPT}/meter`, { ...report, awakeSeconds: 60 });
260 assert.deepEqual(rows("SELECT awake_seconds, egress_bytes FROM container_meters"), [{ awake_seconds: 3600, egress_bytes: 1_000_000_000 }]);
261
262 // It is noted for the spend limit as it runs: basic for an hour, and 1 GB at $0.05.
263 w.state.sqlite.exec("INSERT OR IGNORE INTO meters (namespace, month) VALUES ('acme', strftime('%Y-%m', 'now'))");
264 await worker.scheduled({} as never, w.env as never);
265 const pending = w.state.pending.find((p) => p.workspace === "acme");
266 const second = 0.25 * 20 + 1 * 2.5 + 4 * 0.07 + 1.5625;
267 assert.equal(pending.costMicros, Math.ceil(second * 3600 + 50_000));
268 assert.match(pending.detail, /1 container hours and 1 GB sent/);
269
270 // Taken down: its container first, then its Worker.
271 calls = [];
272 await post("/rpc/take_down", {
273 actor: { id: "usr_1", username: "a", kind: "user", workspaces: [{ slug: "acme", role: "owner" }] },
274 project: { workspace: "acme", slug: "web" },
275 branch: null,
276 });
277 const deleteApp = calls.indexOf("DELETE /containers/applications/app1");
278 const deleteScript = calls.findIndex((call) => call.startsWith("DELETE /workers/dispatch/namespaces/g1t-deployments/scripts/web-acme"));
279 assert.ok(deleteApp >= 0 && deleteScript > deleteApp, calls.join("\n"));
280});
281
282test("a paused container app keeps its class and stops serving", async () => {
283 await finish({ image: IMAGE, port: 3000 });
284 uploads = [];
285 // Its workspace reached its limit.
286 w.env.BILLING = {
287 async fetch(input: string | Request, init?: RequestInit) {
288 const method = new URL(typeof input === "string" ? input : input.url).pathname.split("/").pop();
289 if (method === "check_limit") return Response.json({ ok: true, value: { state: "stopped" } });
290 if (method === "prices") return Response.json({ prices: [] });
291 return Response.json({ ok: true, value: true, init: !!init });
292 },
293 } as never;
294 await worker.scheduled({} as never, w.env as never);
295 const paused = uploads.find((u) => u.bindings.some((b: any) => b.name === "G1T_MODE" && b.text === "paused"));
296 assert.ok(paused, "the Worker was put back paused");
297 assert.deepEqual(paused.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } });
298 assert.ok(!paused.bindings.some((b: any) => b.name === "DATABASE_URL"), "a paused app holds no secrets");
299 assert.ok(rows("SELECT paused_at FROM apps")[0].paused_at);
300});
301
302test("the app's Container application is attached to the namespace its Worker's binding names", async () => {
303 boundNamespace = "ns-bound";
304 await finish({ image: IMAGE, port: 3000 });
305 assert.equal(applicationBodies[0].durable_objects.namespace_id, "ns-bound");
306 assert.ok(!calls.some((call) => call.includes("/durable_objects/namespaces")), "no listing is needed");
307 // Without it, the account's namespaces are searched, by class and script.
308 setUp("on");
309 await finish({ image: IMAGE, port: 3000 });
310 assert.equal(applicationBodies[0].durable_objects.namespace_id, "ns1");
311});
312
313test("an app keeps the image it runs, the one before it and those still being built, and none once it is down", async () => {
314 registryTags = ["dpl_0", "dpl_1"];
315 await finish({ image: IMAGE, port: 3000 });
316 // Nothing replaced yet: only the older image goes.
317 assert.deepEqual(registryTags, ["dpl_1"]);
318 assert.ok(calls.includes("PUT registry.cloudflare.com/v2/gc/layers"), "unused layers are collected");
319 // The next build replaces it, while another is being built.
320 open("dpl_2");
321 open("dpl_3");
322 registryTags = ["dpl_0", "dpl_1", "dpl_2", "dpl_3"];
323 await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE, port: 3000 } });
324 assert.deepEqual(registryTags, ["dpl_1", "dpl_2", "dpl_3"]);
325 // Taken down, it takes its images with it, but a build still under way keeps its own.
326 await post("/rpc/take_down", {
327 actor: { id: "usr_1", username: "a", kind: "user", workspaces: [{ slug: "acme", role: "owner" }] },
328 project: { workspace: "acme", slug: "web" },
329 branch: null,
330 });
331 assert.deepEqual(registryTags, ["dpl_3"]);
332});