Skip to content
306 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 /** What other services were asked, as `SERVICE method`. */
77 calls: [] as string[],
78 /** What `resolve_settings` was asked for. */
79 resolved: [] as { environment: string; trusted: boolean }[],
80 };
81 const project = () => ({
82 id: PROJECT,
83 workspace: "acme",
84 slug: "web",
85 name: "web",
86 primary: true,
87 source: { kind: "hosted", repoId: REPO, repo: { namespace: "acme", name: "web" }, defaultBranch: "main", rootDir: "site" },
88 });
89 const ok = (value: unknown) => ({ ok: true, value });
90 const service = (name: string, answer: (method: string, args: any) => unknown) => ({
91 async fetch(input: string | Request, init?: RequestInit) {
92 const url = typeof input === "string" ? input : input.url;
93 const method = new URL(url).pathname.split("/").pop()!;
94 state.calls.push(`${name} ${method}`);
95 const args = init?.body ? JSON.parse(String(init.body)) : {};
96 return Response.json((await answer(method, args)) ?? ok(null));
97 },
98 });
99 const env = {
100 DB: d1(sqlite),
101 CLOUDFLARE_API_TOKEN: "test",
102 CLOUDFLARE_ACCOUNT_ID: "acct",
103 DISPATCH_NAMESPACE: "g1t-deployments",
104 SITE: "https://g1t.sh",
105 REPOS: service("REPOS", (method) => {
106 if (method === "get") return ok({ id: REPO, namespace: "acme", name: "web", isPrivate: true, defaultBranch: "main" });
107 return ok([]);
108 }),
109 PROJECTS: service("PROJECTS", (method) => (method === "by_repo" ? [project()] : ok(project()))),
110 IDENTITY: service("IDENTITY", (method, args) => {
111 if (method === "get_workspace") return { id: `wsp_${args.slug}`, slug: args.slug };
112 if (method === "collaborator_permission") return ok({ role: state.collaborator });
113 return ok(null);
114 }),
115 BILLING: service("BILLING", (method) => {
116 if (method === "has_feature") return state.plan ? ok(true) : { ok: false, error: { code: "payment_required", message: "Deployments need the Team plan." } };
117 if (method === "check_limit") return ok({ state: state.limit, message: state.limit === "stopped" ? "The workspace reached its spend limit." : null });
118 if (method === "prices") return { prices: [] };
119 return ok(null);
120 }),
121 WORK: service("WORK", (method) => {
122 if (method === "get_pull") return ok({ pull: { ...state.pull, number: 9, author: { username: "eve", kind: "user" } } });
123 return ok(null);
124 }),
125 ACTIONS: service("ACTIONS", (method, args) => {
126 if (method === "run") {
127 return ok({ run: state.run, jobs: [{ id: "job_1", status: state.jobStatus }], notes: [], approval: state.approval });
128 }
129 if (method === "resolve_settings") {
130 state.resolved.push({ environment: args.environment, trusted: args.trusted });
131 return { secrets: { API_KEY: "s3cret-value" }, variables: { PUBLIC_URL: "https://example.com" } };
132 }
133 return ok(null);
134 }),
135 RUNNER: service("RUNNER", () => ok(true)),
136 };
137 return { state, env };
138}
139
140let w: ReturnType<typeof world>;
141const realFetch = globalThis.fetch;
142
143beforeEach(() => {
144 w = world();
145 globalThis.fetch = (async (input: string | Request) => {
146 const url = new URL(typeof input === "string" ? input : input.url);
147 w.state.calls.push(`CLOUDFLARE ${url.pathname}`);
148 if (url.pathname.endsWith("/graphql")) return Response.json({ data: { viewer: { accounts: [{}] } } });
149 return Response.json({ success: true, result: { jwt: "done", buckets: [] }, result_info: { total_pages: 1 } });
150 }) as typeof fetch;
151 w.state.sqlite
152 .prepare(
153 `INSERT INTO settings (project_id, repo_id, workspace, slug, enabled, previews, production, idle_days, updated_at)
154 VALUES (?, ?, 'acme', 'web', 1, 1, 1, 7, '2026-10-05T00:00:00Z')`,
155 )
156 .run(PROJECT, REPO);
157});
158
159process.on("exit", () => {
160 globalThis.fetch = realFetch;
161});
162
163const jobActor = (scopes: string[] = ["contents:read", "deployments:write"]) => ({
164 id: "wsp_acme",
165 username: "acme",
166 kind: "workspace",
167 verified: true,
168 workspaces: [{ slug: "acme", role: "member" }],
169 token: { token_id: "tok_job", repo: "acme/web", job: { run_id: "run_1", job_id: "job_1" }, scopes },
170});
171
172async function post(path: string, body: unknown): Promise<any> {
173 const response = await worker.fetch(
174 new Request(`https://deployments${path}`, { method: "POST", body: JSON.stringify(body) }),
175 w.env as never,
176 {} as never,
177 );
178 return response.json();
179}
180
181const open = (body: Record<string, unknown> = {}) =>
182 post("/rpc/start_from_workflow", { actor: jobActor(), repo: { namespace: "acme", name: "web" }, ...body });
183
184const rows = (sql: string, ...params: unknown[]) =>
185 (w.state.sqlite.prepare(sql).all(...(params as never[])) as any[]).map((row) => ({ ...row }));
186
187test("a push to the production branch opens a production build the job runs, with no sandbox and nothing reserved", async () => {
188 const answer = await open();
189 assert.equal(answer.ok, true, JSON.stringify(answer));
190 const build = answer.value;
191 assert.equal(build.kind, "production");
192 assert.equal(build.project, "web");
193 assert.equal(build.commit, SHA);
194 assert.equal(build.url, "https://web-acme.g1t.page");
195 assert.equal(build.trusted, true);
196 assert.equal(build.root_dir, "site");
197 assert.deepEqual(build.build_secrets, { API_KEY: "s3cret-value" });
198 assert.deepEqual(build.build_env, { PUBLIC_URL: "https://example.com" });
199 assert.match(build.token, /^[0-9a-f]{64}$/);
200 assert.deepEqual(rows("SELECT kind, status, run_id, job_id, created_by, trusted FROM deployments"), [
201 { kind: "production", status: "queued", run_id: "run_1", job_id: "job_1", created_by: "ada", trusted: 1 },
202 ]);
203 // The job runs it: no sandbox, and no compute reserved for it.
204 assert.ok(!w.state.calls.includes("RUNNER start_deploy"));
205 assert.ok(!w.state.calls.some((call) => call === "BILLING reserve" || call === "BILLING entitlements"));
206 // The commit's status says it is building.
207 assert.ok(w.state.calls.includes("WORK set_commit_status"));
208});
209
210test("the job reports with the build's token, and its seconds are not charged again", async () => {
211 const { value: build } = await open();
212 assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, true);
213 const finished = await post(`/jobs/${build.deploy_id}/finish`, {
214 token: build.token,
215 worker: { compatibilityDate: "2026-09-26", vars: {}, notFoundHandling: "single-page-application" },
216 completionJwt: "done",
217 buildSeconds: 42,
218 detected: "static",
219 });
220 assert.equal(finished.ok, true, JSON.stringify(finished));
221 assert.equal(rows("SELECT error FROM deployments")[0].error, null);
222 assert.deepEqual(rows("SELECT status, build_seconds, detected FROM deployments"), [{ status: "ready", build_seconds: 42, detected: "static" }]);
223 assert.deepEqual(rows("SELECT script, kind FROM apps"), [{ script: "web-acme", kind: "production" }]);
224 assert.ok(!w.state.calls.includes("BILLING charge_feature"));
225 assert.deepEqual(rows("SELECT build_seconds FROM meters WHERE build_seconds > 0"), []);
226 // The token was for that build alone, until it finished.
227 assert.equal((await post(`/jobs/${build.deploy_id}/started`, { token: build.token })).ok, false);
228});
229
230test("a sandbox's build is still charged by the second; a workflow job's never is", async () => {
231 // A classic build: what a push opens, built by g1t's sandbox.
232 const { createHash } = await import("node:crypto");
233 const token = "f".repeat(64);
234 w.state.sqlite
235 .prepare(
236 `INSERT INTO deployments (id, project_id, workspace, slug, repo_id, repo, kind, branch, number, commit_sha, script, status,
237 token_hash, trusted, created_by, created_at)
238 VALUES ('dpl_sandbox', ?, 'acme', 'web', ?, 'acme/web', 'preview', 'other', NULL, ?, 'web-git-other-acme', 'building', ?, 1, 'g1t', '2026-10-12T00:00:00Z')`,
239 )
240 .run(PROJECT, REPO, SHA, createHash("sha256").update(token).digest("hex"));
241 const finish = { worker: { compatibilityDate: "2026-09-26", vars: {} }, completionJwt: "done", buildSeconds: 30 };
242 assert.equal((await post("/jobs/dpl_sandbox/finish", { token, ...finish })).ok, true);
243 assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1);
244 assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]);
245 // A workflow job's build of the same project, finished and then failed: not charged.
246 const { value: done } = await open();
247 assert.equal((await post(`/jobs/${done.deploy_id}/finish`, { token: done.token, ...finish })).ok, true);
248 const { value: failed } = await open();
249 await post(`/jobs/${failed.deploy_id}/fail`, { token: failed.token, message: "npm run build failed", buildSeconds: 99 });
250 assert.equal(w.state.calls.filter((call) => call === "BILLING charge_feature").length, 1);
251 assert.deepEqual(rows("SELECT build_seconds FROM meters"), [{ build_seconds: 30 }]);
252 // What runs after the build is the same app as any: in apps, which the sweep meters.
253 assert.deepEqual(rows("SELECT script, kind FROM apps ORDER BY script"), [
254 { script: "web-acme", kind: "production" },
255 { script: "web-git-other-acme", kind: "preview" },
256 ]);
257});
258
259test("another branch builds its preview; a pull request's run builds the pull request's", async () => {
260 w.state.run = { ...w.state.run, ref: "refs/heads/feature/x" };
261 const branch = (await open()).value;
262 assert.deepEqual([branch.kind, branch.branch, branch.number], ["preview", "feature/x", null]);
263 w.state.run = { ...w.state.run, event: "pull_request", ref: "refs/pull/9/merge", pull: 9 };
264 const pull = (await open()).value;
265 assert.deepEqual([pull.kind, pull.branch, pull.number, pull.trusted], ["preview", "feature", 9, true]);
266 assert.deepEqual(w.state.resolved.map((r) => r.environment), ["preview", "preview"]);
267});
268
269test("a pull request from a fork, or one that had to be approved, gets no build secrets", async () => {
270 w.state.run = { ...w.state.run, event: "pull_request", ref: "refs/pull/9/merge", pull: 9 };
271 w.state.pull = { ...w.state.pull, branch: null, fork: { namespace: "eve", name: "web" } };
272 const fork = (await open()).value;
273 assert.deepEqual([fork.branch, fork.trusted, fork.build_secrets], ["pr-9", false, {}]);
274 w.state.pull = { ...w.state.pull, branch: "feature", fork: null };
275 w.state.approval = { state: "approved" };
276 const approved = (await open()).value;
277 assert.deepEqual([approved.trusted, approved.build_secrets], [false, {}]);
278 assert.deepEqual(w.state.resolved.map((r) => r.trusted), [false, false]);
279});
280
281test("it is refused without a job's write token, after the job, without the plan, or with previews off", async () => {
282 const refused = async (body: Record<string, unknown>) => (await open(body)).error?.code;
283 assert.equal(await refused({ actor: jobActor(["deployments:read"]) }), "forbidden");
284 assert.equal(await refused({ actor: { ...jobActor(), token: { token_id: "tok_person", scopes: null } } }), "forbidden");
285 assert.equal(await refused({ repo: { namespace: "acme", name: "api" } }), "forbidden");
286 assert.equal(await refused({ project: "docs" }), "not_found");
287 w.state.jobStatus = "completed";
288 assert.equal(await refused({}), "conflict");
289 w.state.jobStatus = "in_progress";
290 w.state.run = { ...w.state.run, ref: "refs/tags/v1.0.0" };
291 assert.equal(await refused({}), "invalid");
292 w.state.run = { ...w.state.run, ref: "refs/heads/main" };
293 w.state.plan = false;
294 const unpaid = await open();
295 assert.deepEqual([unpaid.ok, unpaid.error.code], [false, "payment_required"]);
296 assert.deepEqual(rows("SELECT status FROM deployments"), [{ status: "skipped" }]);
297 w.state.plan = true;
298 w.state.limit = "stopped";
299 const stopped = await open();
300 assert.deepEqual([stopped.ok, stopped.error.code, stopped.error.message], [false, "payment_required", "The workspace reached its spend limit."]);
301 w.state.limit = "ok";
302 w.state.sqlite.prepare("UPDATE settings SET previews = 0").run();
303 w.state.run = { ...w.state.run, ref: "refs/heads/feature" };
304 assert.equal(await refused({}), "conflict");
305 assert.ok(!w.state.calls.includes("RUNNER start_deploy"));
306});