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.

362 linesCodeBlameRaw
1/**
2 * The whole service, on Node, opening builds for g1t Actions jobs
3 * (`start_from_workflow`): a D1 made of node:sqlite with the service's
4 * migrations, the other services as fakes that answer by method, and
5 * Cloudflare's API as a recorded fetch. See moves.flow.test.ts.
6 */
7
8import assert from "node:assert/strict";
9import { readdirSync, readFileSync } from "node:fs";
10import { registerHooks } from "node:module";
11import { DatabaseSync } from "node:sqlite";
12import { beforeEach, test } from "node:test";
13
14registerHooks({
15 resolve(specifier, context, next) {
16 try {
17 return next(specifier, context);
18 } catch (error) {
19 if (specifier.startsWith(".")) return next(`${specifier}.ts`, context);
20 throw error;
21 }
22 },
23});
24const { default: worker } = await import("./index.ts");
25
26function d1(db: DatabaseSync) {
27 const value = (v: unknown) => (v === undefined ? null : typeof v === "boolean" ? Number(v) : v);
28 const statement = (sql: string, params: unknown[] = []) => ({
29 sql,
30 params,
31 bind: (...args: unknown[]) => statement(sql, args.map(value)),
32 async first<T>() {
33 return (db.prepare(sql).get(...(params as never[])) as T) ?? null;
34 },
35 async all<T>() {
36 return { results: db.prepare(sql).all(...(params as never[])) as T[], success: true };
37 },
38 async run() {
39 const r = db.prepare(sql).run(...(params as never[]));
40 return { success: true, meta: { changes: Number(r.changes) } };
41 },
42 });
43 return {
44 prepare: (sql: string) => statement(sql),
45 async batch(statements: ReturnType<typeof statement>[]) {
46 db.exec("BEGIN");
47 try {
48 const out = statements.map((s) => ({ results: db.prepare(s.sql).all(...(s.params as never[])) }));
49 db.exec("COMMIT");
50 return out;
51 } catch (error) {
52 db.exec("ROLLBACK");
53 throw error;
54 }
55 },
56 };
57}
58
59const REPO = "rep_web";
60const PROJECT = "prj_web";
61const SHA = "4b8d0f2a6c1e3579bd02468ace13579bdf02468a";
62
63function world() {
64 const sqlite = new DatabaseSync(":memory:");
65 const dir = new URL("../migrations/", import.meta.url);
66 for (const file of readdirSync(dir).sort()) sqlite.exec(readFileSync(new URL(file, dir), "utf8"));
67 const state = {
68 sqlite,
69 run: { id: "run_1", event: "push", ref: "refs/heads/main", sha: SHA, pull: null as number | null, status: "in_progress", actor: "ada" },
70 jobStatus: "in_progress",
71 approval: null as null | { state: string },
72 pull: { status: "open", headCommit: SHA, branch: "feature" as string | null, fork: null as null | { namespace: string; name: string } },
73 collaborator: "write",
74 plan: true,
75 limit: "ok",
76 /** Billing does not take charges (it is down, say). */
77 chargeFails: false,
78 /** What other services were asked, as `SERVICE method`. */
79 calls: [] as string[],
80 /** What `resolve_settings` was asked for. */
81 resolved: [] as { environment: string; trusted: boolean }[],
82 };
83 const project = () => ({
84 id: PROJECT,
85 workspace: "acme",
86 slug: "web",
87 name: "web",
88 primary: true,
89 source: { kind: "hosted", repoId: REPO, repo: { namespace: "acme", name: "web" }, defaultBranch: "main", rootDir: "site" },
90 });
91 const ok = (value: unknown) => ({ ok: true, value });
92 const service = (name: string, answer: (method: string, args: any) => unknown) => ({
93 async fetch(input: string | Request, init?: RequestInit) {
94 const url = typeof input === "string" ? input : input.url;
95 const method = new URL(url).pathname.split("/").pop()!;
96 state.calls.push(`${name} ${method}`);
97 const args = init?.body ? JSON.parse(String(init.body)) : {};
98 return Response.json((await answer(method, args)) ?? ok(null));
99 },
100 });
101 const env = {
102 DB: d1(sqlite),
103 CLOUDFLARE_API_TOKEN: "test",
104 CLOUDFLARE_ACCOUNT_ID: "acct",
105 DISPATCH_NAMESPACE: "g1t-deployments",
106 SITE: "https://g1t.sh",
107 REPOS: service("REPOS", (method) => {
108 if (method === "get") return ok({ id: REPO, namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" });
109 return ok([]);
110 }),
111 PROJECTS: service("PROJECTS", (method) => (method === "by_repo" ? [project()] : ok(project()))),
112 IDENTITY: service("IDENTITY", (method, args) => {
113 if (method === "get_workspace") return { id: `wsp_${args.slug}`, slug: args.slug };
114 if (method === "collaborator_permission") return ok({ role: state.collaborator });
115 return ok(null);
116 }),
117 BILLING: service("BILLING", (method) => {
118 if (method === "has_feature")
119 return state.plan ? ok(true) : { ok: false, error: { code: "payment_required", message: "Deployments need the Team plan." } };
120 if (method === "check_limit") return ok({ state: state.limit, message: state.limit === "stopped" ? "The workspace reached its spend limit." : null });
121 if (method === "prices") return { prices: [] };
122 if (method === "charge_feature" && state.chargeFails) return { ok: false, error: { code: "unavailable", message: "billing is down" } };
123 return ok(null);
124 }),
125 WORK: service("WORK", (method) => {
126 if (method === "get_pull") return ok({ pull: { ...state.pull, number: 9, author: { username: "eve", kind: "user" } } });
127 return ok(null);
128 }),
129 ACTIONS: service("ACTIONS", (method, args) => {
130 if (method === "run") {
131 return ok({ run: state.run, jobs: [{ id: "job_1", status: state.jobStatus }], notes: [], approval: state.approval });
132 }
133 if (method === "resolve_settings") {
134 state.resolved.push({ environment: args.environment, trusted: args.trusted });
135 return { secrets: { API_KEY: "s3cret-value" }, variables: { PUBLIC_URL: "https://example.com" } };
136 }
137 return ok(null);
138 }),
139 RUNNER: service("RUNNER", () => ok(true)),
140 };
141 return { state, env };
142}
143
144let w: ReturnType<typeof world>;
145const realFetch = globalThis.fetch;
146
147beforeEach(() => {
148 w = world();
149 globalThis.fetch = (async (input: string | Request) => {
150 const url = new URL(typeof input === "string" ? input : input.url);
151 w.state.calls.push(`CLOUDFLARE ${url.pathname}`);
152 if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } });
153 return Response.json({ success: true, result: { jwt: "done", buckets: [] }, result_info: { total_pages: 1 } });
154 }) as typeof fetch;
155 w.state.sqlite
156 .prepare(
157 `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at)
158 VALUES (?, ?, 'acme', 'web', 1, 1, 1, 7, '2026-10-05T00:00:00Z')`,
159 )
160 .run(PROJECT, REPO);
161});
162
163process.on("exit", () => {
164 globalThis.fetch = realFetch;
165});
166
167const jobActor = (scopes: string[] = ["contents:read", "deployments:write"]) => ({
168 id: "wsp_acme",
169 username: "acme",
170 kind: "workspace",
171 verified: true,
172 workspaces: [{ slug: "acme", role: "member" }],
173 token: { token_id: "tok_job", repo: "acme/web", job: { run_id: "run_1", job_id: "job_1" }, scopes },
174});
175
176async function post(path: string, body: unknown): Promise<any> {
177 const response = await worker.fetch(new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }), w.env as never, {} as never);
178 return response.json();
179}
180
181const open = (body: Record<string, unknown> = {}) => post("/rpc/start_from_workflow", { actor: jobActor(), repo: { namespace: "acme", name: "web" }, ...body });
182
183const rows = (sql: string, ...params: unknown[]) => (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
184
185test("a push to the production branch opens a production build the job runs, with no sandbox and nothing reserved", async () => {
186 const answer = await open();
187 assert.equal(answer.ok, true, JSON.stringify(answer));
188 const build = answer.value;
189 assert.equal(build.kind, "production");
190 assert.equal(build.project, "web");
191 assert.equal(build.commit, SHA);
192 assert.equal(build.url, "https://web-acme.g1t.page");
193 assert.equal(build.trusted, true);
194 assert.equal(build.root_dir, "site");
195 assert.deepEqual(build.build_secrets, { API_KEY: "s3cret-value" });
196 assert.deepEqual(build.build_env, { PUBLIC_URL: "https://example.com" });
197 assert.match(build.token, /^[0-9a-f]{64}$/);
198 assert.deepEqual(rows("SELECT kind, status, run_id, job_id, created_by, trusted FROM deployments"), [
199 { kind: "production", status: "queued", run_id: "run_1", job_id: "job_1", created_by: "ada", trusted: 1 },
200 ]);
201 // The job runs it: no sandbox, and no compute reserved for it.
202 assert.ok(!w.state.calls.includes("RUNNER start_deploy"));
203 assert.ok(!w.state.calls.some((call) => call === "BILLING reserve" || call === "BILLING entitlements"));
204 // The commit's status says it is building.
205 assert.ok(w.state.calls.includes("WORK set_commit_status"));
206});
207
208test("the job reports with the build's token, and its seconds are not charged again", async () => {
209 const { value: build } = await open();
210 assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, true);
211 const finished = await post(`/jobs/${build.deploy_id}/finish`, {
212 token: build.token,
213 worker: { compatibilityDate: "2026-09-26", vars: {}, notFoundHandling: "single-page-application" },
214 completionJwt: "done",
215 buildSeconds: 42,
216 detected: "static",
217 });
218 assert.equal(finished.ok, true, JSON.stringify(finished));
219 assert.equal(rows("SELECT error FROM deployments")[0].error, null);
220 assert.deepEqual(rows("SELECT status, build_seconds, detected FROM deployments"), [{ status: "ready", build_seconds: 42, detected: "static" }]);
221 assert.deepEqual(rows("SELECT script, kind FROM apps"), [{ script: "web-acme", kind: "production" }]);
222 assert.ok(!w.state.calls.includes("BILLING charge_feature"));
223 assert.deepEqual(rows("SELECT build_seconds FROM meters WHERE build_seconds > 0"), []);
224 // The token was for that build alone, until it finished.
225 assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, false);
226});
227
228test("a sandbox's build is still charged by the second; a workflow job's never is", async () => {
229 // A classic build: what a push opens, built by g1t's sandbox.
230 const { createHash } = await import("node:crypto");
231 const token = "f".repeat(64);
232 w.state.sqlite
233 .prepare(
234 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
235 token_hash, trusted, created_by, created_at)
236 VALUES ('dpl_sandbox', ?, 'acme', 'web', ?, 'acme/web', 'preview', 'other', NULL, ?, 'web-git-other-acme', 'building', ?, 1, 'g1t', '2026-10-12T00:00:00Z')`,
237 )
238 .run(PROJECT, REPO, SHA, createHash("sha256").update(token).digest("hex"));
239 const finish = { worker: { compatibilityDate: "2026-09-26", vars: {} }, completionJwt: "done", buildSeconds: 30 };
240 assert.equal((await post("/jobs/dpl_sandbox/finish", { token, ...finish })).ok, true);
241 assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1);
242 assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]);
243 // A workflow job's build of the same project, finished and then failed: not charged.
244 const { value: done } = await open();
245 assert.equal((await post(`/jobs/${done.deploy_id}/finish`, { token: done.token, ...finish })).ok, true);
246 const { value: failed } = await open();
247 await post(`/jobs/${failed.deploy_id}/fail`, { token: failed.token, message: "npm run build failed", buildSeconds: 99 });
248 assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1);
249 assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]);
250 // What runs after the build is the same app as any: in apps, which the sweep meters.
251 assert.deepEqual(rows("SELECT script, kind FROM apps ORDER BY script"), [
252 { script: "web-acme", kind: "production" },
253 { script: "web-git-other-acme", kind: "preview" },
254 ]);
255});
256
257test("a build billing cannot charge still goes live and is announced; the sweep charges it once, later", async () => {
258 const { createHash } = await import("node:crypto");
259 const token = "e".repeat(64);
260 w.state.sqlite
261 .prepare(
262 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
263 token_hash, trusted, created_by, created_at)
264 VALUES ('dpl_owed', ?, 'acme', 'web', ?, 'acme/web', 'preview', 'other', NULL, ?, 'web-git-other-acme', 'building', ?, 1, 'g1t', '2026-10-12T00:00:00Z')`,
265 )
266 .run(PROJECT, REPO, SHA, createHash("sha256").update(token).digest("hex"));
267 w.state.chargeFails = true;
268 const finished = await post("/jobs/dpl_owed/finish", {
269 token,
270 worker: { compatibilityDate: "2026-09-26", vars: {} },
271 completionJwt: "done",
272 buildSeconds: 30,
273 });
274 assert.equal(finished.ok, true, JSON.stringify(finished));
275 // Live and said so, though billing refused the charge.
276 assert.deepEqual(rows("SELECT status, build_seconds, build_charged_at FROM deployments"), [{ status: "ready", build_seconds: 30, build_charged_at: null }]);
277 assert.ok(w.state.calls.includes("WORK set_commit_status"));
278 assert.deepEqual(rows("SELECT build_seconds FROM meters WHERE build_seconds > 0"), []);
279 // Still down: the sweep tries and keeps it.
280 await worker.scheduled({} as never, w.env as never);
281 assert.equal(rows("SELECT build_charged_at FROM deployments")[0].build_charged_at, null);
282 // Back: charged once, counted once, and never again.
283 w.state.chargeFails = false;
284 const before = w.state.calls.filter((call) => call === "BILLING charge_feature").length;
285 await worker.scheduled({} as never, w.env as never);
286 await worker.scheduled({} as never, w.env as never);
287 assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, before + 1);
288 assert.notEqual(rows("SELECT build_charged_at FROM deployments")[0].build_charged_at, null);
289 assert.deepEqual(rows("SELECT build_seconds FROM meters WHERE build_seconds > 0"), [{ build_seconds: 30 }]);
290});
291
292test("another branch builds its preview; a pull request's run builds the pull request's", async () => {
293 w.state.run = { ...w.state.run, ref: "refs/heads/feature/x" };
294 const branch = (await open()).value;
295 assert.deepEqual([branch.kind, branch.branch, branch.number], ["preview", "feature/x", null]);
296 w.state.run = { ...w.state.run, event: "pull_request", ref: "refs/pull/9/merge", pull: 9 };
297 const pull = (await open()).value;
298 assert.deepEqual([pull.kind, pull.branch, pull.number, pull.trusted], ["preview", "feature", 9, true]);
299 assert.deepEqual(
300 w.state.resolved.map((r) => r.environment),
301 ["preview", "preview"],
302 );
303});
304
305test("a pull request from a fork, or one that had to be approved, gets no build secrets", async () => {
306 w.state.run = { ...w.state.run, event: "pull_request", ref: "refs/pull/9/merge", pull: 9 };
307 w.state.pull = { ...w.state.pull, branch: null, fork: { namespace: "eve", name: "web" } };
308 const fork = (await open()).value;
309 assert.deepEqual([fork.branch, fork.trusted, fork.build_secrets], ["pr-9", false, {}]);
310 w.state.pull = { ...w.state.pull, branch: "feature", fork: null };
311 w.state.approval = { state: "approved" };
312 const approved = (await open()).value;
313 assert.deepEqual([approved.trusted, approved.build_secrets], [false, {}]);
314 assert.deepEqual(
315 w.state.resolved.map((r) => r.trusted),
316 [false, false],
317 );
318});
319
320test("it is refused without a job's write token, after the job, without the plan, or with previews off", async () => {
321 const refused = async (body: Record<string, unknown>) => (await open(body)).error?.code;
322 assert.equal(await refused({ actor: jobActor(["deployments:read"]) }), "forbidden");
323 assert.equal(await refused({ actor: { ...jobActor(), token: { token_id: "tok_person", scopes: null } } }), "forbidden");
324 assert.equal(await refused({ repo: { namespace: "acme", name: "api" } }), "forbidden");
325 assert.equal(await refused({ project: "docs" }), "not_found");
326 w.state.jobStatus = "completed";
327 assert.equal(await refused({}), "conflict");
328 w.state.jobStatus = "in_progress";
329 w.state.run = { ...w.state.run, ref: "refs/tags/v1.0.0" };
330 assert.equal(await refused({}), "invalid");
331 w.state.run = { ...w.state.run, ref: "refs/heads/main" };
332 w.state.plan = false;
333 const unpaid = await open();
334 assert.deepEqual([unpaid.ok, unpaid.error.code], [false, "payment_required"]);
335 assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "skipped" }]);
336 w.state.plan = true;
337 w.state.limit = "stopped";
338 const stopped = await open();
339 assert.deepEqual([stopped.ok, stopped.error.code, stopped.error.message], [false, "payment_required", "The workspace reached its spend limit."]);
340 w.state.limit = "ok";
341 w.state.sqlite.prepare("UPDATE settings SET previews = 0").run();
342 w.state.run = { ...w.state.run, ref: "refs/heads/feature" };
343 assert.equal(await refused({}), "conflict");
344 assert.ok(!w.state.calls.includes("RUNNER start_deploy"));
345});
346
347test("a job that ends without finishing its build fails the build at once, and leaves a finished one be", async () => {
348 const build = (await open()).value;
349 const ended = await post("/rpc/workflow_job_ended", { jobId: "job_1", conclusion: "cancelled" });
350 assert.deepEqual(ended, { ok: true, value: 1 });
351 assert.deepEqual(rows("SELECT id, status, error FROM deployments"), [
352 { id: build.deploy_id, status: "failed", error: "The workflow job was cancelled before the build finished." },
353 ]);
354 // Another job's build, and one that is no longer open, are left alone.
355 w.state.sqlite.prepare("UPDATE deployments SET job_id = 'job_2', status = 'building', error = NULL").run();
356 assert.deepEqual(await post("/rpc/workflow_job_ended", { jobId: "job_1", conclusion: "success" }), { ok: true, value: 0 });
357 assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "building" }]);
358 w.state.sqlite.prepare("UPDATE deployments SET status = 'ready'").run();
359 assert.deepEqual(await post("/rpc/workflow_job_ended", { jobId: "job_2", conclusion: "success" }), { ok: true, value: 0 });
360 assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "ready" }]);
361 assert.equal((await post("/rpc/workflow_job_ended", {})).error.code, "invalid");
362});