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
355 linesCodeBlameRaw
1/**
2 * The whole service, on Node, following a repository transfer: a D1 made
3 * of node:sqlite with the service's migrations, the other services as
4 * fakes that answer by method, and Cloudflare's API as a recorded fetch.
5 *
6 * Run with `--experimental-transform-types` (see package.json): the
7 * service's own modules import without extensions, so a resolve hook adds
8 * them.
9 */
10
11import assert from "node:assert/strict";
12import { readdirSync, readFileSync } from "node:fs";
13import { registerHooks } from "node:module";
14import { DatabaseSync } from "node:sqlite";
15import { beforeEach, test } from "node:test";
16
17registerHooks({
18 resolve(specifier, context, next) {
19 try {
20 return next(specifier, context);
21 } catch (error) {
22 if (specifier.startsWith(".")) return next(`${specifier}.ts`, context);
23 throw error;
24 }
25 },
26});
27const { default: worker } = await import("./index.ts");
28
29// ---- D1, on node:sqlite ------------------------------------------------
30
31function d1(db: DatabaseSync) {
32 const value = (v: unknown) => (v === undefined ? null : typeof v === "boolean" ? Number(v) : v);
33 const statement = (sql: string, params: unknown[] = []) => ({
34 sql,
35 params,
36 bind: (...args: unknown[]) => statement(sql, args.map(value)),
37 async first<T>() {
38 return (db.prepare(sql).get(...(params as never[])) as T) ?? null;
39 },
40 async all<T>() {
41 return { results: db.prepare(sql).all(...(params as never[])) as T[], success: true };
42 },
43 async run() {
44 const r = db.prepare(sql).run(...(params as never[]));
45 return { success: true, meta: { changes: Number(r.changes) } };
46 },
47 });
48 return {
49 prepare: (sql: string) => statement(sql),
50 async batch(statements: ReturnType<typeof statement>[]) {
51 db.exec("BEGIN");
52 try {
53 const out = statements.map((s) => ({ results: db.prepare(s.sql).all(...(s.params as never[])) }));
54 db.exec("COMMIT");
55 return out;
56 } catch (error) {
57 db.exec("ROLLBACK");
58 throw error;
59 }
60 },
61 };
62}
63
64// ---- The world ------------------------------------------------------------
65
66const API = "rep_api";
67const PROJECT = "prj_api";
68
69type World = ReturnType<typeof world>;
70
71function world() {
72 const sqlite = new DatabaseSync(":memory:");
73 const dir = new URL("../migrations/", import.meta.url);
74 for (const file of readdirSync(dir).sort()) sqlite.exec(readFileSync(new URL(file, dir), "utf8"));
75 const state = {
76 sqlite,
77 /** Where the repository is now. */
78 path: { namespace: "flagon-io", name: "lab-api" },
79 /** Where the projects service has the project now. */
80 projectWorkspace: "flagon-io",
81 /** Each workspace's usage limit state. */
82 limits: new Map<string, string>([["syntaqx", "stopped"]]),
83 runnerUp: true,
84 /** Builds the runner was asked for: deploy id and token. */
85 builds: [] as { deployId: string; token: string; workspace: string }[],
86 /** Cloudflare API calls, as `METHOD path`. */
87 cloudflare: [] as string[],
88 kv: new Map<string, { value: string; expiration?: number }>(),
89 /** The repository's branches. */
90 branches: ["main", "v2"],
91 };
92 const project = () => ({
93 id: PROJECT,
94 workspace: state.projectWorkspace,
95 slug: "lab-api",
96 name: "lab-api",
97 primary: true,
98 source: { kind: "hosted", repoId: API, repo: { ...state.path }, defaultBranch: "main", rootDir: null },
99 });
100 const ok = (value: unknown) => ({ ok: true, value });
101 const service = (answer: (method: string, args: any) => unknown) => ({
102 async fetch(input: string | Request, init?: RequestInit) {
103 const url = typeof input === "string" ? input : input.url;
104 const method = new URL(url).pathname.split("/").pop()!;
105 const args = init?.body ? JSON.parse(String(init.body)) : {};
106 const answered = await answer(method, args);
107 if (answered instanceof Response) return answered;
108 return Response.json(answered ?? ok(null));
109 },
110 });
111 const env = {
112 DB: d1(sqlite),
113 CLOUDFLARE_API_TOKEN: "test",
114 CLOUDFLARE_ACCOUNT_ID: "acct",
115 DISPATCH_NAMESPACE: "g1t-deployments",
116 SITE: "https://g1t.sh",
117 DOMAINS: {
118 async get(key: string) {
119 const found = state.kv.get(key);
120 return found ? JSON.parse(found.value) : null;
121 },
122 async put(key: string, value: string, options?: { expiration?: number }) {
123 state.kv.set(key, { value, expiration: options?.expiration });
124 },
125 async delete(key: string) {
126 state.kv.delete(key);
127 },
128 },
129 REPOS: service((method) => {
130 if (method === "path_by_id") return state.path;
131 if (method === "get") return ok({ isPrivate: true });
132 if (method === "branches") return ok(state.branches.map((name) => ({ name, hash: `${name}head` })));
133 return ok([]);
134 }),
135 PROJECTS: service((method) => {
136 if (method === "by_repo") return [project()];
137 return ok(project());
138 }),
139 IDENTITY: service((method, args) => {
140 if (method === "get_workspace") return { id: `wsp_${args.slug}`, slug: args.slug };
141 if (method === "collaborator_permission") return ok({ role: "write" });
142 return ok(null);
143 }),
144 BILLING: service((method, args) => {
145 if (method === "has_feature") return ok(true);
146 if (method === "check_limit") return ok({ state: state.limits.get(args.workspace) ?? "ok" });
147 if (method === "entitlements") return ok({ plan: "paid" });
148 if (method === "reserve") return ok({ id: "res_1", paidBy: "credit" });
149 if (method === "prices") return { prices: [] };
150 return ok(null);
151 }),
152 WORK: service((method) => {
153 if (method === "get_pull") {
154 return ok({ pull: { status: "open", headCommit: "pr1head", branch: "v2", author: { username: "syntaqx", kind: "user" } } });
155 }
156 return ok(null);
157 }),
158 ACTIONS: service(() => ({ secrets: {}, variables: {} })),
159 RUNNER: service((method, args) => {
160 if (!state.runnerUp) return new Response("unavailable", { status: 503 });
161 if (method === "start_deploy") state.builds.push({ deployId: args.deployId, token: args.token, workspace: args.workspace });
162 return ok(true);
163 }),
164 };
165 return { state, env };
166}
167
168let w: World;
169const realFetch = globalThis.fetch;
170
171beforeEach(() => {
172 w = world();
173 // Cloudflare's API: every call succeeds, and none lists anything.
174 globalThis.fetch = (async (input: string | Request, init?: RequestInit) => {
175 const url = new URL(typeof input === "string" ? input : input.url);
176 w.state.cloudflare.push(`${init?.method ?? "GET"} ${url.pathname}`);
177 if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } });
178 return Response.json({ success: true, result: [], result_info: { total_pages: 1 } });
179 }) as typeof fetch;
180 // As production was on 2026-10-06: the project's rows still in syntaqx,
181 // its apps paused there by syntaqx's limit, minutes before the transfer.
182 // Previews are kept ten years idle, so the sweep never takes one down
183 // as idle whatever today's date is.
184 const db = w.state.sqlite;
185 db.prepare(
186 `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at)
187 VALUES (?, ?, 'syntaqx', 'lab-api', 1, 1, 1, 3650, '2026-10-05T00:00:00Z')`,
188 ).run(PROJECT, API);
189 const app = db.prepare(
190 `INSERT INTO apps (script, project_id, workspace, slug, kind, branch, number, commit_sha, deployed_at, created_at, paused_at)
191 VALUES (?, ?, 'syntaqx', 'lab-api', ?, ?, ?, ?, '2026-10-05T00:26:29Z', '2026-10-05T00:26:29Z', '2026-10-06T01:20:54Z')`,
192 );
193 app.run("lab-api-syntaqx", PROJECT, "production", null, null, "8e500b1");
194 app.run("lab-api-git-v2-syntaqx", PROJECT, "preview", "v2", 1, "d2ca224");
195 const deployment = db.prepare(
196 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
197 trusted, created_by, created_at, finished_at)
198 VALUES (?, ?, 'syntaqx', 'lab-api', ?, 'syntaqx/lab-api', ?, ?, ?, ?, ?, 'ready', 1, 'g1t', '2026-10-05T00:26:00Z', '2026-10-05T00:26:29Z')`,
199 );
200 deployment.run("dpl_prod", PROJECT, API, "production", null, null, "8e500b1", "lab-api-syntaqx");
201 deployment.run("dpl_prev", PROJECT, API, "preview", "v2", 1, "d2ca224", "lab-api-git-v2-syntaqx");
202});
203
204process.on("exit", () => {
205 globalThis.fetch = realFetch;
206});
207
208const transferred = {
209 id: "evt_1",
210 type: "repo.transferred",
211 source: "repos",
212 time: "2026-10-06T01:23:38Z",
213 repoId: API,
214 actor: "usr_1",
215 data: { from: "syntaqx", name: "lab-api", repoId: API, to: "flagon-io" },
216};
217
218/** Delivers the event as the queue would; whether it was acked. */
219async function deliver(event: unknown, attempts = 1): Promise<boolean> {
220 let acked = false;
221 let retried = false;
222 const message = { body: event, attempts, ack: () => (acked = true), retry: () => (retried = true) };
223 await worker.queue({ messages: [message] } as never, w.env as never);
224 assert.notEqual(acked, retried);
225 return acked;
226}
227
228const rows = (sql: string, ...params: unknown[]) =>
229 (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
230
231test("a transfer rebuilds every app under the new workspace, paused ones too", async () => {
232 assert.equal(await deliver(transferred), true);
233 assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]);
234 const queued = rows("SELECT script, kind, commit_sha FROM deployments WHERE status = 'queued' ORDER BY script");
235 assert.deepEqual(queued, [
236 { script: "lab-api-flagonio", kind: "production", commit_sha: "8e500b1" },
237 { script: "lab-api-git-v2-flagonio", kind: "preview", commit_sha: "pr1head" },
238 ]);
239 assert.equal(w.state.builds.length, 2);
240 assert.ok(w.state.builds.every((b) => b.workspace === "flagon-io"));
241});
242
243test("a second delivery builds nothing more", async () => {
244 await deliver(transferred);
245 assert.equal(await deliver(transferred, 2), true);
246 assert.equal(w.state.builds.length, 2);
247 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2);
248});
249
250test("a rebuild that could not be queued is not acked, and the next delivery queues it", async () => {
251 w.state.runnerUp = false;
252 assert.equal(await deliver(transferred), false);
253 // The rows moved, but nothing is building.
254 assert.deepEqual(rows("SELECT workspace FROM settings"), [{ workspace: "flagon-io" }]);
255 assert.equal(rows("SELECT id FROM deployments WHERE status IN ('queued', 'building')").length, 0);
256 w.state.runnerUp = true;
257 assert.equal(await deliver(transferred, 2), true);
258 assert.deepEqual(
259 rows("SELECT script FROM deployments WHERE status = 'queued' ORDER BY script").map((r) => r.script),
260 ["lab-api-flagonio", "lab-api-git-v2-flagonio"],
261 );
262});
263
264test("the sweep picks up a move whose deliveries ran out, keyed on the new workspace", async () => {
265 w.state.runnerUp = false;
266 await deliver(transferred);
267 w.state.runnerUp = true;
268 // A failed attempt was just made: the sweep waits before trying again.
269 await worker.scheduled({} as never, w.env as never);
270 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 0);
271 w.state.sqlite.exec("UPDATE deployments SET created_at = '2026-10-06T00:00:00Z' WHERE status = 'failed'");
272 await worker.scheduled({} as never, w.env as never);
273 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2);
274 // syntaqx is over its limit, flagon-io is not: nothing of the project's is
275 // paused for syntaqx's sake, and nothing new was paused at all.
276 assert.ok(!w.state.cloudflare.some((call) => call.startsWith("PUT") && /scripts\/lab-api-flagonio/.test(call)));
277});
278
279test("a move waits, without refused deployments, while the new workspace is over its limit", async () => {
280 w.state.limits.set("flagon-io", "stopped");
281 assert.equal(await deliver(transferred), true);
282 assert.equal(rows("SELECT id FROM deployments WHERE created_at > '2026-10-06'").length, 0);
283 w.state.limits.delete("flagon-io");
284 await worker.scheduled({} as never, w.env as never);
285 assert.equal(rows("SELECT id FROM deployments WHERE status = 'queued'").length, 2);
286});
287
288test("once the new app is live, the old address redirects to it, paused or not", async () => {
289 await deliver(transferred);
290 const build = w.state.builds.find((b) => rows("SELECT script FROM deployments WHERE id = ?", b.deployId)[0].script === "lab-api-flagonio")!;
291 const finish = await worker.fetch(
292 new Request(`https://deployments/jobs/${build.deployId}/finish`, {
293 method: "POST",
294 body: JSON.stringify({ token: build.token, worker: { mainModule: "index.js", modules: [{ name: "index.js", contentBase64: btoa("export default {}"), contentType: "application/javascript+module" }] }, buildSeconds: 30 }),
295 }),
296 w.env as never,
297 {} as never,
298 );
299 assert.deepEqual(await finish.json(), { ok: true, value: true });
300 assert.deepEqual(
301 rows("SELECT script, workspace FROM apps WHERE kind = 'production'"),
302 [{ script: "lab-api-flagonio", workspace: "flagon-io" }],
303 );
304 const [redirect] = rows("SELECT script, target, workspace FROM redirects");
305 assert.deepEqual({ ...redirect }, { script: "lab-api-syntaqx", target: "lab-api-flagonio.g1t.page", workspace: "flagon-io" });
306 // The dispatcher's entry, followed before the old (paused) script runs.
307 const entry = w.state.kv.get("lab-api-syntaqx.g1t.page")!;
308 assert.deepEqual(JSON.parse(entry.value), { script: "lab-api-syntaqx", redirect: "lab-api-flagonio.g1t.page" });
309 assert.ok(entry.expiration! > Date.now() / 1000 + 89 * 24 * 3600);
310 // And the old script itself is the redirect too.
311 assert.ok(w.state.cloudflare.some((call) => call === "PUT /client/v4/accounts/acct/workers/dispatch/namespaces/g1t-deployments/scripts/lab-api-syntaqx"));
312 // The preview is still to come; the old one stays until it is live.
313 assert.equal(rows("SELECT script FROM apps WHERE kind = 'preview'")[0].script, "lab-api-git-v2-syntaqx");
314});
315
316test("the sweep never pauses a moved app for its old workspace's limit", async () => {
317 // Rows moved (as the event does) but no rebuild yet, and the apps not paused.
318 w.state.sqlite.exec("UPDATE settings SET workspace = 'flagon-io'; UPDATE apps SET paused_at = NULL");
319 w.state.runnerUp = false;
320 await worker.scheduled({} as never, w.env as never);
321 assert.deepEqual(rows("SELECT paused_at FROM apps").map((r) => r.paused_at), [null, null]);
322 // When the project's own workspace is over its limit, they are paused.
323 w.state.limits.set("flagon-io", "stopped");
324 await worker.scheduled({} as never, w.env as never);
325 assert.ok(rows("SELECT paused_at FROM apps").every((r) => r.paused_at));
326});
327
328test("a branch's preview comes down once its branch is gone, and a pull request's stays", async () => {
329 // Branch previews have no pull request: one the deploy workflow built for
330 // a branch that is still there, and one a preview stack built (before
331 // projects stopped depending on each other) for a branch since deleted.
332 const app = w.state.sqlite.prepare(
333 `INSERT INTO apps (script, project_id, workspace, slug, kind, branch, number, commit_sha, deployed_at, created_at)
334 VALUES (?, ?, 'syntaqx', 'lab-api', 'preview', ?, NULL, 'abc1234', '2026-10-06T22:01:00Z', '2026-10-06T22:01:00Z')`,
335 );
336 app.run("lab-api-git-feature-syntaqx", PROJECT, "feature");
337 app.run("lab-api-git-stack-syntaqx", PROJECT, "stack");
338 w.state.sqlite
339 .prepare(
340 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
341 trusted, created_by, created_at, finished_at)
342 VALUES ('dpl_stack', ?, 'syntaqx', 'lab-api', ?, 'syntaqx/lab-api', 'preview', 'stack', NULL, 'abc1234', 'lab-api-git-stack-syntaqx', 'ready', 1, 'g1t', '2026-10-06T22:00:00Z', '2026-10-06T22:01:00Z')`,
343 )
344 .run(PROJECT, API);
345 w.state.branches.push("feature");
346 await worker.scheduled({} as never, w.env as never);
347 const previews = () => rows("SELECT script FROM apps WHERE kind = 'preview' ORDER BY script").map((r) => r.script);
348 assert.deepEqual(previews(), ["lab-api-git-feature-syntaqx", "lab-api-git-v2-syntaqx"]);
349 assert.equal(rows("SELECT status FROM deployments WHERE id = 'dpl_stack'")[0].status, "down");
350 // Deleting the branch takes its preview down at once.
351 w.state.branches = w.state.branches.filter((name) => name !== "feature");
352 const deleted = { ...transferred, id: "evt_2", type: "git.deleted", data: { repoId: API, ref: "refs/heads/feature", before: "abc1234" } };
353 assert.equal(await deliver(deleted), true);
354 assert.deepEqual(previews(), ["lab-api-git-v2-syntaqx"]);
355});