flagon-io/g1t

public

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/scripts/deploy/deploy.test.mjs

409 lines19,287 bytesCodeBlame
1// node --test "scripts/deploy/*.test.mjs" (npm run test:deploy)
2import assert from "node:assert/strict";
3import { execFileSync } from "node:child_process";
4import { readFileSync } from "node:fs";
5import { join } from "node:path";
6import { test } from "node:test";
7
8import { annotation, commitFrom, liveCommit, pendingFrom, versionFrom } from "./cloudflare.mjs";
9import { changedNames, parseCargoLock, parseNpmLock, reaches } from "./lockfiles.mjs";
10import { decide, planJson, pool } from "./plan.mjs";
11import {
12 ROOT,
13 buildGroups,
14 cargoWorkspace,
15 findWranglerConfigs,
16 loadStack,
17 npmCiArgs,
18 npmWorkspace,
19 outsideImports,
20 pick,
21 problems,
22 resolveStack,
23 resolvedStack,
24 touches,
25 touchesImage,
26} from "./stack.mjs";
27
28const stack = resolvedStack();
29const unit = (id) => stack.units.find((u) => u.id === id);
30
31// ── The manifest ──────────────────────────────────────────────────────────
32
33test("the manifest matches every wrangler config in the repository", () => {
34 assert.deepEqual(problems(stack, findWranglerConfigs()), []);
35});
36
37test("every wrangler.jsonc is found, and only checked-in ones", () => {
38 const configs = findWranglerConfigs();
39 assert.ok(configs.includes("services/events/wrangler.jsonc"));
40 assert.ok(configs.includes("apps/web/wrangler.jsonc"));
41 assert.ok(!configs.some((c) => c.includes("/build/")), "build output is not a unit");
42 assert.equal(configs.length, stack.units.length);
43});
44
45test("problems are found: an unlisted config, a later-stage binding, a wrong database, an unnamed KV", () => {
46 const broken = loadStack();
47 const events = broken.units.find((u) => u.id === "events");
48 events.stage = "front";
49 const identity = broken.units.find((u) => u.id === "identity");
50 identity.d1 = { database: "g1t-identity", migrations: "migrations" };
51 broken.resources = { kv: {} };
52 const found = problems(broken, [...findWranglerConfigs(), "services/new/wrangler.jsonc"]);
53 assert.ok(found.some((p) => p.includes("services/new/wrangler.jsonc is not in")));
54 assert.ok(found.some((p) => p.includes("binds to events (front), which ships after it")));
55 assert.ok(found.some((p) => p.startsWith("Unit identity: d1 is")));
56 assert.ok(found.some((p) => p.includes("has no name under resources.kv")));
57});
58
59test("stages follow bindings: nothing binds to a unit that ships after it", () => {
60 const order = stack.stages;
61 for (const u of stack.units) {
62 for (const target of u.bindsTo) {
63 const other = stack.units.find((o) => o.worker === target);
64 assert.ok(order.indexOf(other.stage) <= order.indexOf(u.stage), `${u.id} -> ${other.id}`);
65 }
66 }
67});
68
69test("Rust workers build with the shared script and the same wasm-opt level", () => {
70 for (const u of stack.units.filter((u) => u.kind === "rust-worker")) {
71 assert.equal(u.config.build.command, "node ../../scripts/build-rust-worker.mjs", u.id);
72 const toml = readFileSync(join(ROOT, u.path, "Cargo.toml"), "utf8");
73 assert.match(toml, /\[package\.metadata\.wasm-pack\.profile\.release\]\s*\nwasm-opt = \["-O1"\]/, u.id);
74 }
75});
76
77// ── What each unit is built from ──────────────────────────────────────────
78
79test("shared crates and packages are read from workspace metadata", () => {
80 assert.deepEqual(unit("events").dependsOn, ["crates/contracts", "crates/kit"]);
81 assert.deepEqual(unit("repos").dependsOn, ["crates/contracts", "crates/kit", "crates/scan", "crates/secrets"]);
82 assert.ok(unit("actions").dependsOn.includes("crates/actions"));
83 assert.ok(!unit("events").dependsOn.includes("crates/scan"));
84 assert.deepEqual(unit("web").dependsOn, ["packages/contracts", "packages/theme"]);
85 assert.deepEqual(unit("projects").dependsOn, ["packages/contracts"]);
86 assert.deepEqual(unit("pages").dependsOn, []);
87 assert.equal(unit("events").crate, "g1t-events");
88 assert.equal(unit("web").pkg, "@g1t/web");
89});
90
91test("the runner's image is built from the runner crate and what it uses", () => {
92 const runner = unit("runner");
93 assert.deepEqual(runner.image.dirs, ["crates/actions", "crates/runner"]);
94 assert.ok(runner.dependsOn.includes("crates/runner"));
95 assert.ok(touchesImage(runner, ["crates/actions/src/expr.rs"]));
96 assert.ok(touchesImage(runner, ["services/runner/Dockerfile"]));
97 assert.ok(!touchesImage(runner, ["services/runner/src/index.ts"]));
98});
99
100test("path dependencies agree with Cargo's own resolved graph", (t) => {
101 let full;
102 try {
103 full = JSON.parse(execFileSync("cargo", ["metadata", "--format-version", "1", "--offline"], { cwd: ROOT, encoding: "utf8", maxBuffer: 256 << 20 }));
104 } catch {
105 t.skip("cargo metadata could not resolve offline");
106 return;
107 }
108 const cargo = cargoWorkspace(ROOT);
109 const dirOf = new Map(full.packages.filter((p) => p.source === null).map((p) => [p.id, p.manifest_path]));
110 const nodes = new Map(full.resolve.nodes.map((n) => [n.id, n]));
111 for (const u of stack.units.filter((u) => u.crate)) {
112 const root = full.packages.find((p) => p.name === u.crate).id;
113 const seen = new Set();
114 const stackIds = [root];
115 while (stackIds.length) {
116 const id = stackIds.pop();
117 for (const dep of nodes.get(id).deps) {
118 if (!dep.dep_kinds.some((k) => k.kind !== "dev")) continue;
119 if (dirOf.has(dep.pkg) && !seen.has(dep.pkg)) {
120 seen.add(dep.pkg);
121 stackIds.push(dep.pkg);
122 }
123 }
124 }
125 const names = [...seen].map((id) => full.packages.find((p) => p.id === id).name);
126 const dirs = names.map((name) => cargo.get(name).dir).sort();
127 assert.deepEqual(dirs, u.dependsOn.filter((d) => d.startsWith("crates/")).sort(), u.id);
128 }
129});
130
131test("every import that leaves a unit's folder is covered by its dependencies or inputs", () => {
132 for (const u of stack.units) {
133 for (const { file, target } of outsideImports(ROOT, u)) {
134 const covered = u.dependsOn.some((dir) => target.startsWith(`${dir}/`)) || u.inputs.some((input) => target === input || target.startsWith(input.replace(/\.ts$/, "")));
135 assert.ok(covered, `${file} imports ${target}, which ${u.id}'s manifest entry does not name`);
136 }
137 }
138});
139
140// ── What a change touches ─────────────────────────────────────────────────
141
142const ids = (units, files) => units.filter((u) => touches(u, files)).map((u) => u.id);
143
144test("a shared crate's change reaches every unit built from it, and no other", () => {
145 assert.deepEqual(ids(stack.units, ["crates/scan/src/lib.rs"]), ["repos", "security"]);
146 assert.deepEqual(ids(stack.units, ["crates/secrets/src/lib.rs"]), ["identity", "repos", "integrations", "webhooks", "actions"]);
147 const kit = ids(stack.units, ["crates/kit/src/lib.rs"]);
148 assert.deepEqual(kit, stack.units.filter((u) => u.kind === "rust-worker").map((u) => u.id));
149 assert.deepEqual(ids(stack.units, ["crates/runner/src/main.rs"]), ["runner"]);
150});
151
152test("a shared package's change reaches what imports it", () => {
153 assert.deepEqual(ids(stack.units, ["packages/theme/tokens.css"]), ["status", "web", "sudo", "docs"]);
154 assert.ok(ids(stack.units, ["packages/contracts/src/index.ts"]).includes("og"));
155 assert.ok(!ids(stack.units, ["packages/contracts/src/index.ts"]).includes("events"));
156});
157
158test("own folders, declared inputs and root files", () => {
159 assert.deepEqual(ids(stack.units, ["services/pages/src/index.ts"]), ["pages"]);
160 assert.deepEqual(ids(stack.units, ["apps/web/app/lib/roadmap.ts"]), ["og", "web"]);
161 assert.deepEqual(ids(stack.units, ["docs/PLAN.md", "README.md", "deploy/stack.jsonc"]), []);
162 assert.deepEqual(ids(stack.units, ["scripts/build-rust-worker.mjs"]), stack.units.filter((u) => u.kind === "rust-worker").map((u) => u.id));
163 // services/events is not a prefix of services/eventsx.
164 assert.equal(touches(unit("events"), ["services/eventsx/a.rs"]), null);
165});
166
167// ── Lockfiles ─────────────────────────────────────────────────────────────
168
169const lock = (sha2) => `version = 4
170
171[[package]]
172name = "g1t-events"
173version = "0.1.0"
174dependencies = [
175 "g1t-kit",
176]
177
178[[package]]
179name = "g1t-kit"
180version = "0.1.0"
181dependencies = [
182 "serde",
183]
184
185[[package]]
186name = "g1t-webhooks"
187version = "0.1.0"
188dependencies = [
189 "g1t-kit",
190 "g1t-secrets",
191]
192
193[[package]]
194name = "g1t-secrets"
195version = "0.1.0"
196dependencies = [
197 "sha2 ${sha2}",
198]
199
200[[package]]
201name = "serde"
202version = "1.0.0"
203source = "registry+https://github.com/rust-lang/crates.io-index"
204
205[[package]]
206name = "sha2"
207version = "${sha2}"
208source = "registry+https://github.com/rust-lang/crates.io-index"
209`;
210
211test("a Cargo.lock change reaches only crates that use what changed", () => {
212 const before = parseCargoLock(lock("0.10.8"));
213 const after = parseCargoLock(lock("0.10.9"));
214 const names = changedNames(before, after);
215 assert.deepEqual([...names], ["sha2"]);
216 assert.ok(reaches(after, ["g1t-webhooks"], names));
217 assert.ok(!reaches(after, ["g1t-events"], names));
218});
219
220test("a package-lock change reaches through workspace links", () => {
221 const make = (version) =>
222 JSON.stringify({
223 lockfileVersion: 3,
224 packages: {
225 "": { devDependencies: { wrangler: "^4" } },
226 "apps/web": { dependencies: { "@g1t/theme": "*", react: "^19" } },
227 "services/pages": { dependencies: {} },
228 "packages/theme": { dependencies: { fontkit: "^1" } },
229 "node_modules/@g1t/theme": { resolved: "packages/theme", link: true },
230 "node_modules/react": { version: "19.0.0" },
231 "node_modules/fontkit": { version },
232 "node_modules/wrangler": { version: "4.1.0" },
233 },
234 });
235 const before = parseNpmLock(make("1.0.0"));
236 const after = parseNpmLock(make("1.0.1"));
237 const names = changedNames(before, after);
238 assert.deepEqual([...names], ["fontkit"]);
239 assert.ok(reaches(after, ["apps/web", ""], names));
240 assert.ok(!reaches(after, ["services/pages", ""], names));
241});
242
243// ── Deciding ──────────────────────────────────────────────────────────────
244
245const HEAD = "a".repeat(40);
246const OLD = "b".repeat(40);
247const fakeGit = (files, { has = true, locks = {} } = {}) => ({
248 has: () => has,
249 changed: () => files,
250 show: (sha, path) => locks[`${sha}:${path}`] ?? "",
251});
252
253test("decide: changed units deploy, others wait, unknown ones deploy", () => {
254 const units = ["events", "repos", "pages", "web"].map(unit);
255 const live = { events: { sha: OLD }, repos: { sha: OLD }, pages: { sha: HEAD }, web: { sha: null, why: "never deployed" } };
256 const decisions = decide(units, { live, head: HEAD, gitApi: fakeGit(["crates/scan/src/lib.rs"]) });
257 const by = Object.fromEntries(decisions.map((d) => [d.unit.id, d]));
258 assert.equal(by.events.deploy, false);
259 assert.equal(by.events.reason, "nothing it is built from changed");
260 assert.equal(by.repos.deploy, true);
261 assert.match(by.repos.reason, /crates\/scan\/src\/lib.rs via crates\/scan/);
262 assert.equal(by.pages.deploy, false);
263 assert.equal(by.pages.reason, "up to date");
264 assert.equal(by.web.deploy, true);
265 assert.match(by.web.reason, /never deployed/);
266});
267
268test("decide: --force deploys unchanged units; a commit missing from history deploys", () => {
269 const forced = decide([unit("events")], { live: { events: { sha: HEAD } }, head: HEAD, force: true, gitApi: fakeGit([]) });
270 assert.equal(forced[0].deploy, true);
271 const shallow = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, gitApi: fakeGit([], { has: false }) });
272 assert.equal(shallow[0].deploy, true);
273 assert.match(shallow[0].reason, /does not have/);
274});
275
276test("decide: Cargo.lock counts only where it reaches", () => {
277 const locks = { [`${OLD}:Cargo.lock`]: lock("0.10.8"), [`${HEAD}:Cargo.lock`]: lock("0.10.9") };
278 const units = [unit("events"), unit("webhooks")];
279 const live = { events: { sha: OLD }, webhooks: { sha: OLD } };
280 const decisions = decide(units, { live, head: HEAD, gitApi: fakeGit(["Cargo.lock"], { locks }) });
281 assert.deepEqual(decisions.map((d) => [d.unit.id, d.deploy]), [["events", false], ["webhooks", true]]);
282});
283
284test("decide: the runner's image rebuilds only when what it is built from changed", () => {
285 const live = { runner: { sha: OLD } };
286 const code = decide([unit("runner")], { live, head: HEAD, gitApi: fakeGit(["services/runner/src/index.ts"]) });
287 assert.deepEqual([code[0].deploy, code[0].image], [true, false]);
288 const image = decide([unit("runner")], { live, head: HEAD, gitApi: fakeGit(["crates/runner/src/main.rs"]) });
289 assert.deepEqual([image[0].deploy, image[0].image], [true, true]);
290});
291
292test("the plan as data: migrations, and each stage's units in jobs that share a build", () => {
293 const units = ["events", "repos", "projects", "api", "web", "docs"].map(unit);
294 const decisions = units.map((u) => ({ unit: u, deploy: true, reason: "x", since: null, image: false }));
295 const data = planJson(stack, decisions, { events: { pending: ["0003_x.sql"] }, repos: { pending: [] } }, HEAD);
296 assert.deepEqual(data.migrations, [{ unit: "events", database: "g1t-events", pending: ["0003_x.sql"] }]);
297 assert.deepEqual(data.stages.core.jobs, [
298 { group: "rust", units: "events,repos", rust: true },
299 { group: "ts", units: "projects", rust: false },
300 ]);
301 assert.deepEqual(data.stages.edge.jobs, [{ group: "rust", units: "api", rust: true }]);
302 assert.deepEqual(data.stages.front.jobs, [
303 { group: "web", units: "web", rust: false },
304 { group: "docs", units: "docs", rust: false },
305 ]);
306 assert.deepEqual(data.stage_order, ["core", "edge", "front"]);
307 assert.deepEqual(buildGroups([]), []);
308 // Ten Rust workers go to three jobs; a unit whose image rebuilds gets its own.
309 const core = stack.units.filter((u) => u.stage === "core");
310 const jobs = buildGroups(core, ["runner"]);
311 assert.deepEqual(jobs.filter((j) => j.rust).map((j) => j.units.split(",").length), [4, 3, 3]);
312 assert.ok(jobs.some((j) => j.group === "runner-image" && j.units === "runner"));
313 assert.ok(!jobs.find((j) => j.group === "ts").units.includes("runner"));
314});
315
316test("units are picked by short name, folder or Worker name", () => {
317 assert.deepEqual(pick(stack, ["billing,services/web".replace("services/web", "apps/web"), "g1t-api"]).map((u) => u.id), ["billing", "web", "api"]);
318 assert.throws(() => pick(stack, ["nope"]), /No unit called nope/);
319});
320
321test("a CI job installs only what its units need", () => {
322 const npm = npmWorkspace();
323 assert.deepEqual(npmCiArgs([unit("events"), unit("repos")], npm), ["ci", "--no-audit", "--no-fund", "--workspaces=false"]);
324 assert.deepEqual(npmCiArgs([unit("events"), unit("status")], npm), [
325 "ci", "--no-audit", "--no-fund", "--include-workspace-root",
326 "-w", "@g1t/contracts", "-w", "@g1t/status", "-w", "@g1t/theme",
327 ]);
328});
329
330// ── Cloudflare's answers ──────────────────────────────────────────────────
331
332const version = (id, number, message, triggered = "deployment") => ({
333 id,
334 number,
335 metadata: { created_on: "2026-10-05T00:00:00Z", author_email: "a@b" },
336 annotations: { "workers/triggered_by": triggered, ...(message ? { "workers/message": message } : {}) },
337});
338
339test("a deploy is annotated with its commit, and read back", () => {
340 const { message, tag } = annotation(HEAD, "A subject ".repeat(20));
341 assert.ok(message.length <= 100);
342 assert.equal(commitFrom(message), HEAD);
343 assert.equal(tag, `g1t-${HEAD.slice(0, 12)}`);
344 assert.equal(commitFrom(message.replace("g1t-deploy", "g1t-deploy-dirty")), null);
345 assert.equal(commitFrom("deployed by hand"), null);
346});
347
348test("the live commit: the live version's, looking through secret changes", () => {
349 const versions = [version("v1", 1, annotation(OLD).message), version("v2", 2, null, "secret"), version("v3", 3, null, "version_upload")];
350 const status = (id) => ({ versions: [{ version_id: id, percentage: 100 }] });
351 assert.equal(liveCommit(status("v1"), versions).sha, OLD);
352 assert.equal(liveCommit(status("v2"), versions).sha, OLD);
353 assert.equal(liveCommit(status("v3"), versions).sha, null);
354 assert.match(liveCommit(status("v9"), versions).why, /not among/);
355 const split = liveCommit({ versions: [{ version_id: "v1", percentage: 10 }, { version_id: "v2", percentage: 90 }] }, versions);
356 assert.equal(split.sha, OLD);
357 assert.equal(split.split, true);
358});
359
360test("Wrangler's output: pending migrations and the version a deploy made", () => {
361 const pending = `
362 ⛅️ wrangler 4.146.0
363Resource location: remote
364Migrations to be applied:
365┌──────────────────────┐
366│ Name │
367├──────────────────────┤
368│ 0021_confidence.sql │
369├──────────────────────┤
370│ 0022_more.sql │
371└──────────────────────┘`;
372 assert.deepEqual(pendingFrom(pending), ["0021_confidence.sql", "0022_more.sql"]);
373 assert.deepEqual(pendingFrom("Resource location: remote\n\n✅ No migrations to apply!"), []);
374 assert.equal(versionFrom("Deployed g1t-events triggers\nCurrent Version ID: 2c7fc93a-82d9-4850-8a77-ba887d157a4f\n"), "2c7fc93a-82d9-4850-8a77-ba887d157a4f");
375});
376
377test("the pool runs everything, no more than its limit at once", async () => {
378 let running = 0;
379 let most = 0;
380 const out = await pool([1, 2, 3, 4, 5, 6, 7], 3, async (n) => {
381 running++;
382 most = Math.max(most, running);
383 await new Promise((resolve) => setTimeout(resolve, 5));
384 running--;
385 return n * 2;
386 });
387 assert.deepEqual(out, [2, 4, 6, 8, 10, 12, 14]);
388 assert.equal(most, 3);
389});
390
391// ── Everything else that reads the list ───────────────────────────────────
392
393test("self-hosting builds every Rust service the manifest says it runs", () => {
394 const dockerfile = readFileSync(join(ROOT, "deploy/self-host/Dockerfile"), "utf8");
395 const loops = [...dockerfile.matchAll(/for service in ([a-z ]+); do/g)].map((m) => m[1].trim().split(/\s+/).sort());
396 const wanted = stack.units.filter((u) => u.self_host === "run" && u.kind === "rust-worker").map((u) => u.path.split("/").pop()).sort();
397 assert.ok(loops.length >= 2);
398 for (const loop of loops) assert.deepEqual(loop, wanted);
399});
400
401test("docs/SELF_HOSTING.md's table names every unit", () => {
402 const doc = readFileSync(join(ROOT, "docs/SELF_HOSTING.md"), "utf8");
403 for (const u of stack.units) assert.ok(doc.includes(`| \`${u.path}\` |`), `${u.path} is missing from docs/SELF_HOSTING.md`);
404});
405
406test("resolveStack works on a manifest with no workspace crates or packages", () => {
407 const bare = resolveStack(loadStack(), { cargo: new Map(), npm: new Map() });
408 assert.deepEqual(bare.units.find((u) => u.id === "events").dependsOn, []);
409});