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
278 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.

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
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;
108const realFetch = globalThis.fetch;
109
110function 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. */
148function 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
158beforeEach(() => setUp("on"));
159
160process.on("exit", () => {
161 globalThis.fetch = realFetch;
162});
163
164async 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
173const rows = (sql: string, ...params: unknown[]) =>
174 (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
175
176const finish = (container: unknown) =>
177 post("/jobs/dpl_1/finish", { token: TOKEN, worker: {}, detected: "container", framework: "node", container, buildSeconds: 20 });
178
179test("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
187test("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
202test("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
214test("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
260test("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});