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.

g1t.sh
327 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(
187 new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }),
188 w.env as never,
189 {} as never,
190 );
191 return { status: response.status, ...(await response.json()) };
192}
193
194const rows = (sql: string, ...params: unknown[]) =>
195 (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
196
197const finish = (container: unknown) =>
198 post("/jobs/dpl_1/finish", { token: TOKEN, worker: {}, detected: "container", framework: "node", container, buildSeconds: 20 });
199
200test("while containers are off, the registry step says so in its own words", async () => {
201 setUp("off");
202 const answer = await post("/jobs/dpl_1/registry", { token: TOKEN });
203 assert.equal(answer.status, 409);
204 assert.equal(answer.error.code, "not_enabled");
205 assert.ok(!calls.some((call) => call.includes("credentials")), "no login is minted");
206});
207
208test("a build gets a push-only login for its app's repository, held to its size", async () => {
209 const answer = await post("/jobs/dpl_1/registry", { token: TOKEN });
210 assert.deepEqual(answer.value, {
211 registry: "registry.cloudflare.com",
212 repository: `acct/g1t-${SCRIPT}`,
213 tag: "dpl_1",
214 username: "v1",
215 password: "push-secret",
216 maxImageBytes: 4_000_000_000,
217 instanceType: "basic",
218 });
219 // Another build's token gets nothing.
220 assert.equal((await post("/jobs/dpl_1/registry", { token: "nope" })).status, 404);
221});
222
223test("an image outside the app's repository, or not in the registry, is never run", async () => {
224 const other = await finish({ image: `registry.cloudflare.com/acct/g1t-other@sha256:${DIGEST}`, port: 3000 });
225 assert.deepEqual(other.value, false);
226 assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_1'")[0].error, /did not report an image by digest/);
227 assert.equal(uploads.length, 0);
228 open("dpl_2");
229 registryHas = false;
230 const missing = await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE } });
231 assert.deepEqual(missing.value, false);
232 assert.match(rows("SELECT error FROM deployments WHERE id = 'dpl_2'")[0].error, /is not in the registry/);
233});
234
235test("a container app runs behind g1t's Worker, is metered by its own token, and comes down whole", async () => {
236 const done = await finish({ image: IMAGE, port: 3000, sizeBytes: 123 });
237 assert.deepEqual(done.value, true);
238 // g1t's Worker, its class, and the project's entries bound to it.
239 const [metadata] = uploads;
240 assert.deepEqual(metadata.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } });
241 const binding = (name: string) => metadata.bindings.find((b: any) => b.name === name);
242 assert.equal(binding("G1T_PORT").text, "3000");
243 assert.equal(binding("G1T_INSTANCE").text, "basic");
244 assert.equal(binding("G1T_METER_URL").text, `https://api.g1t.sh/deployments/apps/${SCRIPT}/meter`);
245 assert.deepEqual(binding("DATABASE_URL"), { type: "secret_text", name: "DATABASE_URL", text: "postgres://db" });
246 assert.ok(calls.includes("POST /containers/applications"));
247 assert.deepEqual(rows("SELECT runtime, container_app_id, instance_type FROM apps"), [{ runtime: "container", container_app_id: "app1", instance_type: "basic" }]);
248 assert.deepEqual(rows("SELECT detected, framework, image, status FROM deployments WHERE id = 'dpl_1'"), [
249 { detected: "container", framework: "node", image: IMAGE, status: "ready" },
250 ]);
251 // The next deploy rolls the application out to the new image.
252 open("dpl_2");
253 await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE, port: 3000 } });
254 assert.ok(calls.includes("POST /containers/applications/app1/rollouts"));
255
256 // Its reports: only with the token it was deployed with, each counter only raised.
257 const token = uploads.at(-1)!.bindings.find((b: any) => b.name === "G1T_METER_TOKEN").text;
258 const month = new Date().toISOString().slice(0, 7);
259 const report = { token, epoch: "epoch-0001", month, awakeSeconds: 3600, egressBytes: 1_000_000_000, instanceType: "basic" };
260 assert.equal((await post(`/apps/${SCRIPT}/meter`, { ...report, token: "wrong" })).status, 404);
261 assert.equal((await post(`/apps/${SCRIPT}/meter`, report)).status, 200);
262 await post(`/apps/${SCRIPT}/meter`, { ...report, awakeSeconds: 60 });
263 assert.deepEqual(rows("SELECT awake_seconds, egress_bytes FROM container_meters"), [{ awake_seconds: 3600, egress_bytes: 1_000_000_000 }]);
264
265 // It is noted for the spend limit as it runs: basic for an hour, and 1 GB at $0.05.
266 w.state.sqlite.exec("INSERT OR IGNORE INTO meters (namespace, month) VALUES ('acme', strftime('%Y-%m', 'now'))");
267 await worker.scheduled({} as never, w.env as never);
268 const pending = w.state.pending.find((p) => p.workspace === "acme");
269 const second = 0.25 * 20 + 1 * 2.5 + 4 * 0.07 + 1.5625;
270 assert.equal(pending.costMicros, Math.ceil(second * 3600 + 50_000));
271 assert.match(pending.detail, /1 container hours and 1 GB sent/);
272
273 // Taken down: its container first, then its Worker.
274 calls = [];
275 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 });
276 const deleteApp = calls.indexOf("DELETE /containers/applications/app1");
277 const deleteScript = calls.findIndex((call) => call.startsWith("DELETE /workers/dispatch/namespaces/g1t-deployments/scripts/web-acme"));
278 assert.ok(deleteApp >= 0 && deleteScript > deleteApp, calls.join("\n"));
279});
280
281test("a paused container app keeps its class and stops serving", async () => {
282 await finish({ image: IMAGE, port: 3000 });
283 uploads = [];
284 // Its workspace reached its limit.
285 w.env.BILLING = {
286 async fetch(input: string | Request, init?: RequestInit) {
287 const method = new URL(typeof input === "string" ? input : input.url).pathname.split("/").pop();
288 if (method === "check_limit") return Response.json({ ok: true, value: { state: "stopped" } });
289 if (method === "prices") return Response.json({ prices: [] });
290 return Response.json({ ok: true, value: true, init: !!init });
291 },
292 } as never;
293 await worker.scheduled({} as never, w.env as never);
294 const paused = uploads.find((u) => u.bindings.some((b: any) => b.name === "G1T_MODE" && b.text === "paused"));
295 assert.ok(paused, "the Worker was put back paused");
296 assert.deepEqual(paused.exports, { G1tApp: { type: "durable-object", storage: "sqlite" } });
297 assert.ok(!paused.bindings.some((b: any) => b.name === "DATABASE_URL"), "a paused app holds no secrets");
298 assert.ok(rows("SELECT paused_at FROM apps")[0].paused_at);
299});
300
301test("the app's Container application is attached to the namespace its Worker's binding names", async () => {
302 boundNamespace = "ns-bound";
303 await finish({ image: IMAGE, port: 3000 });
304 assert.equal(applicationBodies[0].durable_objects.namespace_id, "ns-bound");
305 assert.ok(!calls.some((call) => call.includes("/durable_objects/namespaces")), "no listing is needed");
306 // Without it, the account's namespaces are searched, by class and script.
307 setUp("on");
308 await finish({ image: IMAGE, port: 3000 });
309 assert.equal(applicationBodies[0].durable_objects.namespace_id, "ns1");
310});
311
312test("an app keeps the image it runs, the one before it and those still being built, and none once it is down", async () => {
313 registryTags = ["dpl_0", "dpl_1"];
314 await finish({ image: IMAGE, port: 3000 });
315 // Nothing replaced yet: only the older image goes.
316 assert.deepEqual(registryTags, ["dpl_1"]);
317 assert.ok(calls.includes("PUT registry.cloudflare.com/v2/gc/layers"), "unused layers are collected");
318 // The next build replaces it, while another is being built.
319 open("dpl_2");
320 open("dpl_3");
321 registryTags = ["dpl_0", "dpl_1", "dpl_2", "dpl_3"];
322 await post("/jobs/dpl_2/finish", { token: TOKEN, detected: "container", container: { image: IMAGE, port: 3000 } });
323 assert.deepEqual(registryTags, ["dpl_1", "dpl_2", "dpl_3"]);
324 // Taken down, it takes its images with it, but a build still under way keeps its own.
325 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 });
326 assert.deepEqual(registryTags, ["dpl_3"]);
327});