Skip to content
356 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") return state.plan ? ok(true) : { ok: false, error: { code: "payment_required", message: "Deployments need the Team plan." } };
119 if (method === "check_limit") return ok({ state: state.limit, message: state.limit === "stopped" ? "The workspace reached its spend limit." : null });
120 if (method === "prices") return { prices: [] };
121 if (method === "charge_feature" && state.chargeFails) return { ok: false, error: { code: "unavailable", message: "billing is down" } };
122 return ok(null);
123 }),
124 WORK: service("WORK", (method) => {
125 if (method === "get_pull") return ok({ pull: { ...state.pull, number: 9, author: { username: "eve", kind: "user" } } });
126 return ok(null);
127 }),
128 ACTIONS: service("ACTIONS", (method, args) => {
129 if (method === "run") {
130 return ok({ run: state.run, jobs: [{ id: "job_1", status: state.jobStatus }], notes: [], approval: state.approval });
131 }
132 if (method === "resolve_settings") {
133 state.resolved.push({ environment: args.environment, trusted: args.trusted });
134 return { secrets: { API_KEY: "s3cret-value" }, variables: { PUBLIC_URL: "https://example.com" } };
135 }
136 return ok(null);
137 }),
138 RUNNER: service("RUNNER", () => ok(true)),
139 };
140 return { state, env };
141}
142
143let w: ReturnType<typeof world>;
144const realFetch = globalThis.fetch;
145
146beforeEach(() => {
147 w = world();
148 globalThis.fetch = (async (input: string | Request) => {
149 const url = new URL(typeof input === "string" ? input : input.url);
150 w.state.calls.push(`CLOUDFLARE ${url.pathname}`);
151 if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } });
152 return Response.json({ success: true, result: { jwt: "done", buckets: [] }, result_info: { total_pages: 1 } });
153 }) as typeof fetch;
154 w.state.sqlite
155 .prepare(
156 `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at)
157 VALUES (?, ?, 'acme', 'web', 1, 1, 1, 7, '2026-10-05T00:00:00Z')`,
158 )
159 .run(PROJECT, REPO);
160});
161
162process.on("exit", () => {
163 globalThis.fetch = realFetch;
164});
165
166const jobActor = (scopes: string[] = ["contents:read", "deployments:write"]) => ({
167 id: "wsp_acme",
168 username: "acme",
169 kind: "workspace",
170 verified: true,
171 workspaces: [{ slug: "acme", role: "member" }],
172 token: { token_id: "tok_job", repo: "acme/web", job: { run_id: "run_1", job_id: "job_1" }, scopes },
173});
174
175async function post(path: string, body: unknown): Promise<any> {
176 const response = await worker.fetch(
177 new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }),
178 w.env as never,
179 {} as never,
180 );
181 return response.json();
182}
183
184const open = (body: Record<string, unknown> = {}) =>
185 post("/rpc/start_from_workflow", { actor: jobActor(), repo: { namespace: "acme", name: "web" }, ...body });
186
187const rows = (sql: string, ...params: unknown[]) =>
188 (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
189
190test("a push to the production branch opens a production build the job runs, with no sandbox and nothing reserved", async () => {
191 const answer = await open();
192 assert.equal(answer.ok, true, JSON.stringify(answer));
193 const build = answer.value;
194 assert.equal(build.kind, "production");
195 assert.equal(build.project, "web");
196 assert.equal(build.commit, SHA);
197 assert.equal(build.url, "https://web-acme.g1t.page");
198 assert.equal(build.trusted, true);
199 assert.equal(build.root_dir, "site");
200 assert.deepEqual(build.build_secrets, { API_KEY: "s3cret-value" });
201 assert.deepEqual(build.build_env, { PUBLIC_URL: "https://example.com" });
202 assert.match(build.token, /^[0-9a-f]{64}$/);
203 assert.deepEqual(rows("SELECT kind, status, run_id, job_id, created_by, trusted FROM deployments"), [
204 { kind: "production", status: "queued", run_id: "run_1", job_id: "job_1", created_by: "ada", trusted: 1 },
205 ]);
206 // The job runs it: no sandbox, and no compute reserved for it.
207 assert.ok(!w.state.calls.includes("RUNNER start_deploy"));
208 assert.ok(!w.state.calls.some((call) => call === "BILLING reserve" || call === "BILLING entitlements"));
209 // The commit's status says it is building.
210 assert.ok(w.state.calls.includes("WORK set_commit_status"));
211});
212
213test("the job reports with the build's token, and its seconds are not charged again", async () => {
214 const { value: build } = await open();
215 assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, true);
216 const finished = await post(`/jobs/${build.deploy_id}/finish`, {
217 token: build.token,
218 worker: { compatibilityDate: "2026-09-26", vars: {}, notFoundHandling: "single-page-application" },
219 completionJwt: "done",
220 buildSeconds: 42,
221 detected: "static",
222 });
223 assert.equal(finished.ok, true, JSON.stringify(finished));
224 assert.equal(rows("SELECT error FROM deployments")[0].error, null);
225 assert.deepEqual(rows("SELECT status, build_seconds, detected FROM deployments"), [{ status: "ready", build_seconds: 42, detected: "static" }]);
226 assert.deepEqual(rows("SELECT script, kind FROM apps"), [{ script: "web-acme", kind: "production" }]);
227 assert.ok(!w.state.calls.includes("BILLING charge_feature"));
228 assert.deepEqual(rows("SELECT build_seconds FROM meters WHERE build_seconds > 0"), []);
229 // The token was for that build alone, until it finished.
230 assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, false);
231});
232
233test("a sandbox's build is still charged by the second; a workflow job's never is", async () => {
234 // A classic build: what a push opens, built by g1t's sandbox.
235 const { createHash } = await import("node:crypto");
236 const token = "f".repeat(64);
237 w.state.sqlite
238 .prepare(
239 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
240 token_hash, trusted, created_by, created_at)
241 VALUES ('dpl_sandbox', ?, 'acme', 'web', ?, 'acme/web', 'preview', 'other', NULL, ?, 'web-git-other-acme', 'building', ?, 1, 'g1t', '2026-10-12T00:00:00Z')`,
242 )
243 .run(PROJECT, REPO, SHA, createHash("sha256").update(token).digest("hex"));
244 const finish = { worker: { compatibilityDate: "2026-09-26", vars: {} }, completionJwt: "done", buildSeconds: 30 };
245 assert.equal((await post("/jobs/dpl_sandbox/finish", { token, ...finish })).ok, true);
246 assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1);
247 assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]);
248 // A workflow job's build of the same project, finished and then failed: not charged.
249 const { value: done } = await open();
250 assert.equal((await post(`/jobs/${done.deploy_id}/finish`, { token: done.token, ...finish })).ok, true);
251 const { value: failed } = await open();
252 await post(`/jobs/${failed.deploy_id}/fail`, { token: failed.token, message: "npm run build failed", buildSeconds: 99 });
253 assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1);
254 assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]);
255 // What runs after the build is the same app as any: in apps, which the sweep meters.
256 assert.deepEqual(rows("SELECT script, kind FROM apps ORDER BY script"), [
257 { script: "web-acme", kind: "production" },
258 { script: "web-git-other-acme", kind: "preview" },
259 ]);
260});
261
262test("a build billing cannot charge still goes live and is announced; the sweep charges it once, later", async () => {
263 const { createHash } = await import("node:crypto");
264 const token = "e".repeat(64);
265 w.state.sqlite
266 .prepare(
267 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
268 token_hash, trusted, created_by, created_at)
269 VALUES ('dpl_owed', ?, 'acme', 'web', ?, 'acme/web', 'preview', 'other', NULL, ?, 'web-git-other-acme', 'building', ?, 1, 'g1t', '2026-10-12T00:00:00Z')`,
270 )
271 .run(PROJECT, REPO, SHA, createHash("sha256").update(token).digest("hex"));
272 w.state.chargeFails = true;
273 const finished = await post("/jobs/dpl_owed/finish", { token, worker: { compatibilityDate: "2026-09-26", vars: {} }, completionJwt: "done", buildSeconds: 30 });
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(w.state.resolved.map((r) => r.environment), ["preview", "preview"]);
300});
301
302test("a pull request from a fork, or one that had to be approved, gets no build secrets", async () => {
303 w.state.run = { ...w.state.run, event: "pull_request", ref: "refs/pull/9/merge", pull: 9 };
304 w.state.pull = { ...w.state.pull, branch: null, fork: { namespace: "eve", name: "web" } };
305 const fork = (await open()).value;
306 assert.deepEqual([fork.branch, fork.trusted, fork.build_secrets], ["pr-9", false, {}]);
307 w.state.pull = { ...w.state.pull, branch: "feature", fork: null };
308 w.state.approval = { state: "approved" };
309 const approved = (await open()).value;
310 assert.deepEqual([approved.trusted, approved.build_secrets], [false, {}]);
311 assert.deepEqual(w.state.resolved.map((r) => r.trusted), [false, false]);
312});
313
314test("it is refused without a job's write token, after the job, without the plan, or with previews off", async () => {
315 const refused = async (body: Record<string, unknown>) => (await open(body)).error?.code;
316 assert.equal(await refused({ actor: jobActor(["deployments:read"]) }), "forbidden");
317 assert.equal(await refused({ actor: { ...jobActor(), token: { token_id: "tok_person", scopes: null } } }), "forbidden");
318 assert.equal(await refused({ repo: { namespace: "acme", name: "api" } }), "forbidden");
319 assert.equal(await refused({ project: "docs" }), "not_found");
320 w.state.jobStatus = "completed";
321 assert.equal(await refused({}), "conflict");
322 w.state.jobStatus = "in_progress";
323 w.state.run = { ...w.state.run, ref: "refs/tags/v1.0.0" };
324 assert.equal(await refused({}), "invalid");
325 w.state.run = { ...w.state.run, ref: "refs/heads/main" };
326 w.state.plan = false;
327 const unpaid = await open();
328 assert.deepEqual([unpaid.ok, unpaid.error.code], [false, "payment_required"]);
329 assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "skipped" }]);
330 w.state.plan = true;
331 w.state.limit = "stopped";
332 const stopped = await open();
333 assert.deepEqual([stopped.ok, stopped.error.code, stopped.error.message], [false, "payment_required", "The workspace reached its spend limit."]);
334 w.state.limit = "ok";
335 w.state.sqlite.prepare("UPDATE settings SET previews = 0").run();
336 w.state.run = { ...w.state.run, ref: "refs/heads/feature" };
337 assert.equal(await refused({}), "conflict");
338 assert.ok(!w.state.calls.includes("RUNNER start_deploy"));
339});
340
341test("a job that ends without finishing its build fails the build at once, and leaves a finished one be", async () => {
342 const build = (await open()).value;
343 const ended = await post("/rpc/workflow_job_ended", { jobId: "job_1", conclusion: "cancelled" });
344 assert.deepEqual(ended, { ok: true, value: 1 });
345 assert.deepEqual(rows("SELECT id, status, error FROM deployments"), [
346 { id: build.deploy_id, status: "failed", error: "The workflow job was cancelled before the build finished." },
347 ]);
348 // Another job's build, and one that is no longer open, are left alone.
349 w.state.sqlite.prepare("UPDATE deployments SET job_id = 'job_2', status = 'building', error = NULL").run();
350 assert.deepEqual(await post("/rpc/workflow_job_ended", { jobId: "job_1", conclusion: "success" }), { ok: true, value: 0 });
351 assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "building" }]);
352 w.state.sqlite.prepare("UPDATE deployments SET status = 'ready'").run();
353 assert.deepEqual(await post("/rpc/workflow_job_ended", { jobId: "job_2", conclusion: "success" }), { ok: true, value: 0 });
354 assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "ready" }]);
355 assert.equal((await post("/rpc/workflow_job_ended", {})).error.code, "invalid");
356});