Skip to content
814 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Fast pages, required checks on the branch, self-hosted runners, honest incidents1// node --test "scripts/deploy/*.test.mjs" (npm run test:deploy)
2import assert from "node:assert/strict";
3import { execFileSync } from "node:child_process";
4import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5import { tmpdir } from "node:os";
6import { join } from "node:path";
7import { test } from "node:test";
8
9import { isStaticElf } from "../build-runner.mjs";
10import {
11 DEPLOY_CONFIG,
12 baseInputs,
13 baseState,
14 baseTag,
15 contentHash,
16 lockFor,
The runner's base image, pushed as Wrangler builds images, with pushes that fail loudly17 pushImage,
18 pushedDigest,
Fast pages, required checks on the branch, self-hosted runners, honest incidents19 readBaseLock,
20 registryHas,
21 removeDeployConfig,
22 runnerRef,
23 runnerTag,
24 writeDeployConfig,
25} from "./image.mjs";
26
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check27import {
28 annotation,
29 apiAuth,
30 commitFrom,
31 databaseOf,
32 liveCommit,
33 liveFromApi,
34 migrationFiles,
35 pendingAgainst,
36 pendingFrom,
37 pendingMigrations,
38 readLive,
39 versionFrom,
40} from "./cloudflare.mjs";
Fast pages, required checks on the branch, self-hosted runners, honest incidents41import { changedNames, parseCargoLock, parseNpmLock, reaches } from "./lockfiles.mjs";
Merge remote-tracking branch 'origin/main' into workspace-chat42import { decide, git, planJson, pool } from "./plan.mjs";
Fast pages, required checks on the branch, self-hosted runners, honest incidents43import {
44 ROOT,
45 buildGroups,
46 cargoWorkspace,
47 findWranglerConfigs,
48 loadStack,
49 npmCiArgs,
50 npmWorkspace,
51 outsideImports,
52 pick,
53 problems,
54 resolveStack,
55 resolvedStack,
Merge remote-tracking branch 'origin/main' into workspace-chat56 testOnlySource,
Fast pages, required checks on the branch, self-hosted runners, honest incidents57 touches,
58 touchesBase,
59 touchesImage,
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.60 waves,
Fast pages, required checks on the branch, self-hosted runners, honest incidents61} from "./stack.mjs";
62
63const stack = resolvedStack();
64const unit = (id) => stack.units.find((u) => u.id === id);
65
66// ── The manifest ──────────────────────────────────────────────────────────
67
68test("the manifest matches every wrangler config in the repository", () => {
69 assert.deepEqual(problems(stack, findWranglerConfigs()), []);
70});
71
72test("every wrangler.jsonc is found, and only checked-in ones", () => {
73 const configs = findWranglerConfigs();
74 assert.ok(configs.includes("services/events/wrangler.jsonc"));
75 assert.ok(configs.includes("apps/web/wrangler.jsonc"));
76 assert.ok(!configs.some((c) => c.includes("/build/")), "build output is not a unit");
77 assert.equal(configs.length, stack.units.length);
78});
79
80test("problems are found: an unlisted config, a later-stage binding, a wrong database, an unnamed KV", () => {
81 const broken = loadStack();
82 const events = broken.units.find((u) => u.id === "events");
83 events.stage = "front";
84 const identity = broken.units.find((u) => u.id === "identity");
85 identity.d1 = { database: "g1t-identity", migrations: "migrations" };
86 broken.resources = { kv: {} };
87 const found = problems(broken, [...findWranglerConfigs(), "services/new/wrangler.jsonc"]);
88 assert.ok(found.some((p) => p.includes("services/new/wrangler.jsonc is not in")));
89 assert.ok(found.some((p) => p.includes("binds to events (front), which ships after it")));
90 assert.ok(found.some((p) => p.startsWith("Unit identity: d1 is")));
91 assert.ok(found.some((p) => p.includes("has no name under resources.kv")));
92});
93
94test("stages follow bindings: nothing binds to a unit that ships after it", () => {
95 const order = stack.stages;
96 for (const u of stack.units) {
97 for (const target of u.bindsTo) {
98 const other = stack.units.find((o) => o.worker === target);
99 assert.ok(order.indexOf(other.stage) <= order.indexOf(u.stage), `${u.id} -> ${other.id}`);
100 }
101 }
102});
103
The artifacts service is services/artifacts, the Worker g1t-artifacts, bound as ARTIFACTS by the API, the site and the agents; its live rooms move to it with a Durable Object transfer from g1t-docs-service, and its database, bucket, indexes and queue keep their names. The git store's binding and settings are GITSTORE, its ops scripts gitstore-*, and workflow run artifacts keep their compatible API under run_artifacts modules. The deploy tool puts a Worker that has never deployed before the Workers in its stage that bind to it, and the deploy guide gives the cutover runbook.104test("inside a stage, a Worker that does not exist yet goes before those bound to it", () => {
105 const u = (id, worker, bindsTo = []) => ({ id, worker, bindsTo });
106 const artifacts = u("artifacts", "g1t-artifacts", ["g1t-agents", "g1t-identity"]);
107 const agents = u("agents", "g1t-agents", ["g1t-artifacts", "g1t-identity"]);
108 const chat = u("chat", "g1t-chat", ["g1t-agents"]);
109 const ids = (list) => list.map((wave) => wave.map((x) => x.id));
110 // Nothing new: one wave, as before.
111 assert.deepEqual(ids(waves([artifacts, agents, chat], () => false)), [["artifacts", "agents", "chat"]]);
112 // A renamed Worker: it first, then what binds to it.
113 assert.deepEqual(ids(waves([agents, chat, artifacts], (x) => x.id === "artifacts")), [["chat", "artifacts"], ["agents"]]);
114 // New Workers bound to each other: no order helps, so together.
115 assert.deepEqual(ids(waves([artifacts, agents], () => true)), [["artifacts", "agents"]]);
116 // This repository's agents unit binds to the artifacts Worker, in the same stage.
117 const real = (id) => stack.units.find((x) => x.id === id);
118 assert.equal(real("artifacts").stage, real("agents").stage);
119 assert.deepEqual(ids(waves([real("agents"), real("artifacts")], (x) => x.id === "artifacts")), [["artifacts"], ["agents"]]);
120});
121
Fast pages, required checks on the branch, self-hosted runners, honest incidents122test("Rust workers build with the shared script and the same wasm-opt level", () => {
123 for (const u of stack.units.filter((u) => u.kind === "rust-worker")) {
124 assert.equal(u.config.build.command, "node ../../scripts/build-rust-worker.mjs", u.id);
125 const toml = readFileSync(join(ROOT, u.path, "Cargo.toml"), "utf8");
126 assert.match(toml, /\[package\.metadata\.wasm-pack\.profile\.release\]\s*\nwasm-opt = \["-O1"\]/, u.id);
127 }
128});
129
130// ── What each unit is built from ──────────────────────────────────────────
131
132test("shared crates and packages are read from workspace metadata", () => {
133 assert.deepEqual(unit("events").dependsOn, ["crates/contracts", "crates/kit"]);
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge134 assert.deepEqual(unit("repos").dependsOn, ["crates/blobstore", "crates/contracts", "crates/kit", "crates/rules", "crates/scan", "crates/secrets"]);
135 assert.ok(unit("work").dependsOn.includes("crates/rules"));
Fast pages, required checks on the branch, self-hosted runners, honest incidents136 assert.ok(unit("actions").dependsOn.includes("crates/actions"));
137 assert.ok(!unit("events").dependsOn.includes("crates/scan"));
138 assert.deepEqual(unit("web").dependsOn, ["packages/contracts", "packages/theme"]);
139 assert.deepEqual(unit("projects").dependsOn, ["packages/contracts"]);
140 assert.deepEqual(unit("pages").dependsOn, []);
141 assert.equal(unit("events").crate, "g1t-events");
142 assert.equal(unit("web").pkg, "@g1t/web");
143});
144
145test("the runner's image is built from the runner crate and what it uses, on the base its lock names", () => {
146 const runner = unit("runner");
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily147 // The runner bumps lockfiles with the scanner's lockfile parser (security updates).
148 assert.deepEqual(runner.image.dirs, ["crates/actions", "crates/runner", "crates/scan"]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents149 assert.deepEqual(runner.image.baseDirs, ["services/runner/base"]);
150 assert.ok(runner.dependsOn.includes("crates/runner"));
151 assert.ok(touchesImage(runner, ["crates/actions/src/expr.rs"]));
152 assert.ok(touchesImage(runner, ["services/runner/Dockerfile"]));
153 assert.ok(touchesImage(runner, ["services/runner/base.json"]));
154 assert.ok(touchesImage(runner, ["scripts/build-runner.mjs"]));
155 assert.ok(!touchesImage(runner, ["services/runner/src/index.ts"]));
156 // The base's Dockerfile reaches the image only through a new lock.
157 assert.ok(!touchesImage(runner, ["services/runner/base/Dockerfile"]));
158 assert.ok(touchesBase(runner, ["services/runner/base/Dockerfile"]));
159 assert.ok(!touchesBase(unit("events"), ["services/runner/base/Dockerfile"]));
160});
161
162// ── The runner's images ───────────────────────────────────────────────────
163
164test("services/runner/base.json records the base its folder builds", () => {
165 const runner = unit("runner");
166 const lock = readBaseLock(runner);
167 assert.ok(lock, "services/runner/base.json is missing: node scripts/deploy.mjs build-base");
168 assert.equal(
169 lock.inputs,
170 baseInputs(runner),
171 "services/runner/base changed since base.json was written: node scripts/deploy.mjs build-base, and commit base.json",
172 );
173 assert.match(lock.image, /^registry\.cloudflare\.com\/[0-9a-f]{32}\/g1t-runner:base-\d{8}-[0-9a-f]{12}$/);
174 assert.ok(lock.image.endsWith(lock.inputs.slice(0, 12)));
175 assert.equal(baseState(runner).current, true);
176});
177
178test("an image's hash ignores line endings, and its tags say what they are", () => {
179 const files = { "a.txt": "one\r\ntwo\r\n", "b.txt": "x" };
180 const lf = { "a.txt": "one\ntwo\n", "b.txt": "x" };
181 const hash = (set) => contentHash(Object.keys(set), "/", (file) => Buffer.from(set[file]));
182 assert.equal(hash(files), hash(lf));
183 assert.notEqual(hash(lf), hash({ ...lf, "b.txt": "y" }));
184 assert.equal(baseTag("0123456789abcdef".repeat(4), new Date("2026-10-06T12:00:00Z")), "base-20261006-0123456789ab");
185 const runner = unit("runner");
186 assert.match(runnerTag(runner), /^[0-9a-f]{16}$/);
187 assert.equal(runnerTag(runner), runnerTag(runner));
188 assert.equal(runnerRef(runner, ROOT, "a".repeat(32)), `registry.cloudflare.com/${"a".repeat(32)}/g1t-runner:${runnerTag(runner)}`);
189});
190
191test("only the lockfile entries the runner is built from name its image", () => {
192 const lock = (wasm) => `[[package]]
193name = "g1t-runner"
194version = "0.1.0"
195dependencies = [
196 "ureq",
197]
198
199[[package]]
200name = "ureq"
201version = "2.12.1"
202
203[[package]]
204name = "wasm-bindgen"
205version = "${wasm}"
206`;
207 assert.equal(lockFor(lock("0.2.100"), "g1t-runner"), lockFor(lock("0.2.101"), "g1t-runner"));
208 assert.ok(lockFor(lock("0.2.100"), "g1t-runner").includes("ureq"));
209 assert.notEqual(lockFor(lock("0.2.100"), "g1t-runner"), lockFor(lock("0.2.100").replace("2.12.1", "2.12.2"), "g1t-runner"));
210});
211
212test("the runner's deploy config names its image by reference, beside its wrangler.jsonc", () => {
213 const runner = unit("runner");
214 const ref = "registry.cloudflare.com/acct/g1t-runner:0123456789abcdef";
215 const name = writeDeployConfig(runner, ref);
216 try {
217 assert.equal(name, DEPLOY_CONFIG);
218 const config = JSON.parse(readFileSync(join(ROOT, runner.path, DEPLOY_CONFIG), "utf8"));
219 assert.equal(config.name, "g1t-runner");
220 assert.equal(config.main, "./src/index.ts");
221 assert.ok(config.containers.length >= 1);
222 for (const container of config.containers) {
223 assert.equal(container.image, ref);
224 assert.equal(container.image_build_context, undefined);
225 }
226 // Every sandbox class shares the image; each has a Durable Object binding.
227 const classes = config.containers.map((c) => c.class_name).sort();
228 assert.deepEqual(classes, config.durable_objects.bindings.map((b) => b.class_name).sort());
229 // Generated, never committed, and not taken for a unit of its own.
230 assert.ok(readFileSync(join(ROOT, ".gitignore"), "utf8").includes("wrangler.deploy.json"));
231 assert.ok(!findWranglerConfigs().some((c) => c.endsWith(DEPLOY_CONFIG)));
232 } finally {
233 removeDeployConfig(runner);
234 }
235 assert.ok(!existsSync(join(ROOT, runner.path, DEPLOY_CONFIG)));
236});
237
238test("the registry is asked for an image's manifest, without Docker", async () => {
239 const asked = [];
240 const fetchImpl = async (url, init) => {
241 asked.push([url, init.method, init.headers.authorization]);
242 return { status: url.endsWith(":nope") || url.endsWith("/nope") ? 404 : 200, ok: !url.endsWith("/nope") };
243 };
244 const credentials = async () => ({ username: "v1", password: "secret" });
245 assert.equal(await registryHas("registry.cloudflare.com/acct/g1t-runner:abc", { fetchImpl, credentials }), true);
246 assert.equal(await registryHas("registry.cloudflare.com/acct/g1t-runner:nope", { fetchImpl, credentials }), false);
247 assert.deepEqual(asked[0], [
248 "https://registry.cloudflare.com/v2/acct/g1t-runner/manifests/abc",
249 "HEAD",
250 `Basic ${Buffer.from("v1:secret").toString("base64")}`,
251 ]);
252 await assert.rejects(registryHas("not-a-reference", { fetchImpl, credentials }));
253});
254
The runner's base image, pushed as Wrangler builds images, with pushes that fail loudly255test("a push counts only when it ends with a digest, and is tried again", async () => {
256 const digest = "sha256:" + "a".repeat(64);
257 assert.equal(pushedDigest({ code: 0, out: `base: digest: ${digest} size: 856` }), digest);
258 assert.equal(pushedDigest({ code: 0, out: "error from registry: blob unknown to registry" }), null);
259 assert.equal(pushedDigest({ code: 1, out: `digest: ${digest}` }), null);
260 assert.equal(pushedDigest({ code: 0, out: "Pushed" }), null);
261 const answers = [{ code: 1, out: "error from registry: blob unknown to registry" }, { code: 0, out: `x: digest: ${digest} size: 1` }];
262 assert.equal(await pushImage("r/x:y", { run: async () => answers.shift() }), digest);
263 await assert.rejects(pushImage("r/x:y", { attempts: 2, run: async () => ({ code: 0, out: "blob unknown" }) }), /docker push r\/x:y failed/);
264});
265
Fast pages, required checks on the branch, self-hosted runners, honest incidents266test("a static Linux binary is told from a dynamic one", () => {
267 const dir = mkdtempSync(join(tmpdir(), "g1t-elf-"));
268 try {
269 const elf = (interp) => {
270 // 64-bit little-endian x86-64 ELF with one program header.
271 const bytes = Buffer.alloc(64 + 56);
272 bytes.writeUInt32BE(0x7f454c46, 0);
273 bytes[4] = 2;
274 bytes[5] = 1;
275 bytes.writeUInt16LE(0x3e, 18);
276 bytes.writeBigUInt64LE(64n, 32);
277 bytes.writeUInt16LE(56, 54);
278 bytes.writeUInt16LE(1, 56);
279 bytes.writeUInt32LE(interp ? 3 : 1, 64);
280 return bytes;
281 };
282 writeFileSync(join(dir, "static"), elf(false));
283 writeFileSync(join(dir, "dynamic"), elf(true));
284 writeFileSync(join(dir, "text"), "#!/bin/sh\n");
285 assert.equal(isStaticElf(join(dir, "static")), true);
286 assert.equal(isStaticElf(join(dir, "dynamic")), false);
287 assert.equal(isStaticElf(join(dir, "text")), false);
288 } finally {
289 rmSync(dir, { recursive: true, force: true });
290 }
291});
292
293test("path dependencies agree with Cargo's own resolved graph", (t) => {
294 let full;
295 try {
296 full = JSON.parse(execFileSync("cargo", ["metadata", "--format-version", "1", "--offline"], { cwd: ROOT, encoding: "utf8", maxBuffer: 256 << 20 }));
297 } catch {
298 t.skip("cargo metadata could not resolve offline");
299 return;
300 }
301 const cargo = cargoWorkspace(ROOT);
302 const dirOf = new Map(full.packages.filter((p) => p.source === null).map((p) => [p.id, p.manifest_path]));
303 const nodes = new Map(full.resolve.nodes.map((n) => [n.id, n]));
304 for (const u of stack.units.filter((u) => u.crate)) {
305 const root = full.packages.find((p) => p.name === u.crate).id;
306 const seen = new Set();
307 const stackIds = [root];
308 while (stackIds.length) {
309 const id = stackIds.pop();
310 for (const dep of nodes.get(id).deps) {
311 if (!dep.dep_kinds.some((k) => k.kind !== "dev")) continue;
312 if (dirOf.has(dep.pkg) && !seen.has(dep.pkg)) {
313 seen.add(dep.pkg);
314 stackIds.push(dep.pkg);
315 }
316 }
317 }
318 const names = [...seen].map((id) => full.packages.find((p) => p.id === id).name);
319 const dirs = names.map((name) => cargo.get(name).dir).sort();
320 assert.deepEqual(dirs, u.dependsOn.filter((d) => d.startsWith("crates/")).sort(), u.id);
321 }
322});
323
324test("every import that leaves a unit's folder is covered by its dependencies or inputs", () => {
325 for (const u of stack.units) {
326 for (const { file, target } of outsideImports(ROOT, u)) {
327 const covered = u.dependsOn.some((dir) => target.startsWith(`${dir}/`)) || u.inputs.some((input) => target === input || target.startsWith(input.replace(/\.ts$/, "")));
328 assert.ok(covered, `${file} imports ${target}, which ${u.id}'s manifest entry does not name`);
329 }
330 }
331});
332
333// ── What a change touches ─────────────────────────────────────────────────
334
335const ids = (units, files) => units.filter((u) => touches(u, files)).map((u) => u.id);
336
337test("a shared crate's change reaches every unit built from it, and no other", () => {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily338 assert.deepEqual(ids(stack.units, ["crates/scan/src/lib.rs"]), ["repos", "security", "runner"]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents339 assert.deepEqual(ids(stack.units, ["crates/secrets/src/lib.rs"]), ["identity", "repos", "integrations", "webhooks", "actions"]);
340 const kit = ids(stack.units, ["crates/kit/src/lib.rs"]);
341 assert.deepEqual(kit, stack.units.filter((u) => u.kind === "rust-worker").map((u) => u.id));
342 assert.deepEqual(ids(stack.units, ["crates/runner/src/main.rs"]), ["runner"]);
343});
344
345test("a shared package's change reaches what imports it", () => {
346 assert.deepEqual(ids(stack.units, ["packages/theme/tokens.css"]), ["status", "web", "sudo", "docs"]);
347 assert.ok(ids(stack.units, ["packages/contracts/src/index.ts"]).includes("og"));
348 assert.ok(!ids(stack.units, ["packages/contracts/src/index.ts"]).includes("events"));
349});
350
Merge remote-tracking branch 'origin/main' into workspace-chat351test("a crate's tests, benches and examples deploy nothing", () => {
352 // 0cdd221 changed only this file, and the plan sent three Rust Workers.
353 assert.deepEqual(ids(stack.units, ["crates/actions/tests/repository_workflows.rs"]), []);
354 assert.ok(!touchesImage(unit("runner"), ["crates/actions/tests/repository_workflows.rs"]));
355 assert.deepEqual(ids(stack.units, ["crates/kit/benches/wire.rs", "crates/scan/examples/scan.rs", "services/repos/tests/git.rs"]), []);
356 // Their sources still do, and a folder merely named like one.
357 assert.deepEqual(ids(stack.units, ["crates/actions/src/expr.rs"]), ["actions", "security", "runner"]);
358 assert.ok(touchesImage(unit("runner"), ["crates/actions/src/expr.rs"]));
359 assert.deepEqual(ids(stack.units, ["services/repos/src/tests/helpers.rs"]), ["repos"]);
360 assert.deepEqual(ids(stack.units, ["crates/kit/testsuite/a.rs"]), ids(stack.units, ["crates/kit/src/lib.rs"]));
361 // A unit that is not a crate keeps its whole folder.
362 assert.deepEqual(unit("web").crateDirs, []);
363 assert.deepEqual(unit("events").crateDirs, ["crates/contracts", "crates/kit", "services/events"]);
364 assert.ok(unit("runner").crateDirs.includes("crates/runner"));
365});
366
367/** Rust sources by path, as `testOnlySource` reads them. */
368const sources = (files) => ({
369 read: (path) => files[path] ?? "",
370 list: (dir) => Object.keys(files).filter((path) => path.slice(0, path.lastIndexOf("/")) === dir),
371});
372
373test("a Rust file compiled only for tests is found from what declares it", () => {
374 const crate = ["crates/x"];
375 const tree = sources({
376 "crates/x/src/lib.rs": "pub mod api;\nmod store;\n#[cfg(test)]\nmod tests;\n#[cfg(test)] pub(crate) mod fixtures;\n#[cfg(any(test, feature = \"x\"))]\nmod both;\n",
377 "crates/x/src/api.rs": "mod wire;\n#[cfg(test)]\n#[path = \"api_tests.rs\"]\nmod tests;\n",
378 "crates/x/src/api/wire.rs": "",
379 "crates/x/src/api_tests.rs": "use super::*;",
380 "crates/x/src/store/mod.rs": "#[cfg(test)]\nmod memory;\n",
381 "crates/x/src/store/memory.rs": "mod deep;",
382 "crates/x/src/store/memory/deep.rs": "",
383 "crates/x/src/tests.rs": "",
384 "crates/x/src/fixtures/mod.rs": "",
385 "crates/x/src/both.rs": "",
386 "crates/x/src/bin/tool.rs": "",
387 });
388 const only = (file) => testOnlySource(file, crate, tree);
389 for (const file of ["crates/x/src/tests.rs", "crates/x/src/fixtures/mod.rs", "crates/x/src/api_tests.rs", "crates/x/src/store/memory.rs", "crates/x/src/store/memory/deep.rs"]) {
390 assert.ok(only(file), file);
391 }
392 // Built: roots, ordinary modules, a cfg that is not only test, a file
393 // nothing declares (new, or deleted), and files outside src or the crate.
394 for (const file of [
395 "crates/x/src/lib.rs",
396 "crates/x/src/api.rs",
397 "crates/x/src/api/wire.rs",
398 "crates/x/src/store/mod.rs",
399 "crates/x/src/both.rs",
400 "crates/x/src/bin/tool.rs",
401 "crates/x/src/new.rs",
402 "crates/x/build.rs",
403 "crates/y/src/tests.rs",
404 "crates/x/src/notes.md",
405 ]) {
406 assert.ok(!only(file), file);
407 }
408});
409
410test("this repository's test-only sources are read from git", () => {
411 const head = git.head();
412 const atHead = { read: (path) => git.show(head, path), list: (dir) => git.list(head, dir) };
413 const only = (u, file) => testOnlySource(file, unit(u).crateDirs, atHead);
414 assert.ok(only("api", "apps/api/src/responses.rs"));
415 assert.ok(only("billing", "services/billing/src/catalogue_tests.rs"));
416 assert.ok(only("billing", "services/billing/src/gateway_tests.rs"));
417 assert.ok(!only("billing", "services/billing/src/catalogue.rs"));
418 assert.ok(!only("api", "apps/api/src/toolkit.rs"));
419});
420
421test("decide: a change only to tests deploys nothing", () => {
422 const units = ["actions", "billing", "runner"].map(unit);
423 const live = { actions: { sha: OLD }, billing: { sha: OLD }, runner: { sha: OLD } };
424 const files = {
425 [`${HEAD}:services/billing/src/gateway.rs`]: "#[cfg(test)]\n#[path = \"gateway_tests.rs\"]\nmod tests;\n",
426 [`${HEAD}:services/billing/src/lib.rs`]: "mod gateway;\n",
427 };
428 const gitApi = {
429 ...fakeGit(["crates/actions/tests/repository_workflows.rs", "services/billing/src/gateway_tests.rs"]),
430 show: (sha, path) => files[`${sha}:${path}`] ?? "",
431 list: (sha, dir) => Object.keys(files).map((k) => k.slice(sha.length + 1)).filter((p) => p.startsWith(`${dir}/`)).concat(dir === "services/billing/src" ? ["services/billing/src/gateway_tests.rs"] : []),
432 };
433 const decisions = decide(units, { live, head: HEAD, gitApi });
434 assert.deepEqual(decisions.map((d) => [d.unit.id, d.deploy, d.image]), [["actions", false, false], ["billing", false, false], ["runner", false, false]]);
435 // With a source that ships beside them, only that is listed.
436 const mixed = decide([unit("billing")], { live, head: HEAD, gitApi: { ...gitApi, changed: () => ["services/billing/src/gateway_tests.rs", "services/billing/src/gateway.rs"] } });
437 assert.deepEqual([mixed[0].deploy, mixed[0].files], [true, ["services/billing/src/gateway.rs"]]);
438});
439
Fast pages, required checks on the branch, self-hosted runners, honest incidents440test("own folders, declared inputs and root files", () => {
441 assert.deepEqual(ids(stack.units, ["services/pages/src/index.ts"]), ["pages"]);
442 assert.deepEqual(ids(stack.units, ["apps/web/app/lib/roadmap.ts"]), ["og", "web"]);
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.443 assert.deepEqual(ids(stack.units, ["LICENSE", "README.md", "deploy/stack.jsonc"]), []);
Fast pages, required checks on the branch, self-hosted runners, honest incidents444 assert.deepEqual(ids(stack.units, ["scripts/build-rust-worker.mjs"]), stack.units.filter((u) => u.kind === "rust-worker").map((u) => u.id));
445 // services/events is not a prefix of services/eventsx.
446 assert.equal(touches(unit("events"), ["services/eventsx/a.rs"]), null);
447});
448
449// ── Lockfiles ─────────────────────────────────────────────────────────────
450
451const lock = (sha2) => `version = 4
452
453[[package]]
454name = "g1t-events"
455version = "0.1.0"
456dependencies = [
457 "g1t-kit",
458]
459
460[[package]]
461name = "g1t-kit"
462version = "0.1.0"
463dependencies = [
464 "serde",
465]
466
467[[package]]
468name = "g1t-webhooks"
469version = "0.1.0"
470dependencies = [
471 "g1t-kit",
472 "g1t-secrets",
473]
474
475[[package]]
476name = "g1t-secrets"
477version = "0.1.0"
478dependencies = [
479 "sha2 ${sha2}",
480]
481
482[[package]]
483name = "serde"
484version = "1.0.0"
485source = "registry+https://github.com/rust-lang/crates.io-index"
486
487[[package]]
488name = "sha2"
489version = "${sha2}"
490source = "registry+https://github.com/rust-lang/crates.io-index"
491`;
492
493test("a Cargo.lock change reaches only crates that use what changed", () => {
494 const before = parseCargoLock(lock("0.10.8"));
495 const after = parseCargoLock(lock("0.10.9"));
496 const names = changedNames(before, after);
497 assert.deepEqual([...names], ["sha2"]);
498 assert.ok(reaches(after, ["g1t-webhooks"], names));
499 assert.ok(!reaches(after, ["g1t-events"], names));
500});
501
502test("a package-lock change reaches through workspace links", () => {
503 const make = (version) =>
504 JSON.stringify({
505 lockfileVersion: 3,
506 packages: {
507 "": { devDependencies: { wrangler: "^4" } },
508 "apps/web": { dependencies: { "@g1t/theme": "*", react: "^19" } },
509 "services/pages": { dependencies: {} },
510 "packages/theme": { dependencies: { fontkit: "^1" } },
511 "node_modules/@g1t/theme": { resolved: "packages/theme", link: true },
512 "node_modules/react": { version: "19.0.0" },
513 "node_modules/fontkit": { version },
514 "node_modules/wrangler": { version: "4.1.0" },
515 },
516 });
517 const before = parseNpmLock(make("1.0.0"));
518 const after = parseNpmLock(make("1.0.1"));
519 const names = changedNames(before, after);
520 assert.deepEqual([...names], ["fontkit"]);
521 assert.ok(reaches(after, ["apps/web", ""], names));
522 assert.ok(!reaches(after, ["services/pages", ""], names));
523});
524
525// ── Deciding ──────────────────────────────────────────────────────────────
526
527const HEAD = "a".repeat(40);
528const OLD = "b".repeat(40);
529const fakeGit = (files, { has = true, locks = {} } = {}) => ({
530 has: () => has,
531 changed: () => files,
532 show: (sha, path) => locks[`${sha}:${path}`] ?? "",
533});
534
535test("decide: changed units deploy, others wait, unknown ones deploy", () => {
536 const units = ["events", "repos", "pages", "web"].map(unit);
537 const live = { events: { sha: OLD }, repos: { sha: OLD }, pages: { sha: HEAD }, web: { sha: null, why: "never deployed" } };
538 const decisions = decide(units, { live, head: HEAD, gitApi: fakeGit(["crates/scan/src/lib.rs"]) });
539 const by = Object.fromEntries(decisions.map((d) => [d.unit.id, d]));
540 assert.equal(by.events.deploy, false);
541 assert.equal(by.events.reason, "nothing it is built from changed");
542 assert.equal(by.repos.deploy, true);
543 assert.match(by.repos.reason, /crates\/scan\/src\/lib.rs via crates\/scan/);
544 assert.equal(by.pages.deploy, false);
545 assert.equal(by.pages.reason, "up to date");
546 assert.equal(by.web.deploy, true);
547 assert.match(by.web.reason, /never deployed/);
548});
549
550test("decide: --force deploys unchanged units; a commit missing from history deploys", () => {
551 const forced = decide([unit("events")], { live: { events: { sha: HEAD } }, head: HEAD, force: true, gitApi: fakeGit([]) });
552 assert.equal(forced[0].deploy, true);
553 const shallow = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, gitApi: fakeGit([], { has: false }) });
554 assert.equal(shallow[0].deploy, true);
555 assert.match(shallow[0].reason, /does not have/);
556});
557
558test("decide: Cargo.lock counts only where it reaches", () => {
559 const locks = { [`${OLD}:Cargo.lock`]: lock("0.10.8"), [`${HEAD}:Cargo.lock`]: lock("0.10.9") };
560 const units = [unit("events"), unit("webhooks")];
561 const live = { events: { sha: OLD }, webhooks: { sha: OLD } };
562 const decisions = decide(units, { live, head: HEAD, gitApi: fakeGit(["Cargo.lock"], { locks }) });
563 assert.deepEqual(decisions.map((d) => [d.unit.id, d.deploy]), [["events", false], ["webhooks", true]]);
564});
565
566test("decide: the runner's image rebuilds only when what it is built from changed", () => {
567 const live = { runner: { sha: OLD } };
568 const code = decide([unit("runner")], { live, head: HEAD, gitApi: fakeGit(["services/runner/src/index.ts"]) });
569 assert.deepEqual([code[0].deploy, code[0].image], [true, false]);
570 const image = decide([unit("runner")], { live, head: HEAD, gitApi: fakeGit(["crates/runner/src/main.rs"]) });
571 assert.deepEqual([image[0].deploy, image[0].image], [true, true]);
572});
573
574test("the plan as data: migrations, and each stage's units in jobs that share a build", () => {
575 const units = ["events", "repos", "projects", "api", "web", "docs"].map(unit);
576 const decisions = units.map((u) => ({ unit: u, deploy: true, reason: "x", since: null, image: false }));
577 const data = planJson(stack, decisions, { events: { pending: ["0003_x.sql"] }, repos: { pending: [] } }, HEAD);
578 assert.deepEqual(data.migrations, [{ unit: "events", database: "g1t-events", pending: ["0003_x.sql"] }]);
579 assert.deepEqual(data.stages.core.jobs, [
Merge branch 'main' into actions-toolkit-oidc-artifacts580 { group: "rust", units: "events,repos", rust: true, image: false },
581 { group: "ts", units: "projects", rust: false, image: false },
Fast pages, required checks on the branch, self-hosted runners, honest incidents582 ]);
Merge branch 'main' into actions-toolkit-oidc-artifacts583 assert.deepEqual(data.stages.edge.jobs, [{ group: "rust", units: "api", rust: true, image: false }]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents584 assert.deepEqual(data.stages.front.jobs, [
Merge branch 'main' into actions-toolkit-oidc-artifacts585 { group: "web", units: "web", rust: false, image: false },
586 { group: "docs", units: "docs", rust: false, image: false },
Fast pages, required checks on the branch, self-hosted runners, honest incidents587 ]);
588 assert.deepEqual(data.stage_order, ["core", "edge", "front"]);
589 assert.deepEqual(buildGroups([]), []);
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member590 // Eleven Rust workers go to three jobs; a unit whose image rebuilds gets its own.
Fast pages, required checks on the branch, self-hosted runners, honest incidents591 const core = stack.units.filter((u) => u.stage === "core");
592 const jobs = buildGroups(core, ["runner"]);
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member593 assert.deepEqual(jobs.filter((j) => j.rust).map((j) => j.units.split(",").length), [4, 4, 3]);
Merge branch 'main' into actions-toolkit-oidc-artifacts594 assert.ok(jobs.some((j) => j.group === "runner-image" && j.units === "runner" && j.image && !j.rust));
Fast pages, required checks on the branch, self-hosted runners, honest incidents595 assert.ok(!jobs.find((j) => j.group === "ts").units.includes("runner"));
596});
597
598test("units are picked by short name, folder or Worker name", () => {
599 assert.deepEqual(pick(stack, ["billing,services/web".replace("services/web", "apps/web"), "g1t-api"]).map((u) => u.id), ["billing", "web", "api"]);
600 assert.throws(() => pick(stack, ["nope"]), /No unit called nope/);
601});
602
603test("a CI job installs only what its units need", () => {
604 const npm = npmWorkspace();
The deploy's stages ride out a dropped npm connection as CI does. Each core, edge and front job keeps npm's downloads between runs (actions/cache on ~/.npm, keyed on the lockfile), and the deploy tool's install reads them first (npm ci --prefer-offline), so a registry connection reset mid-install no longer fails a stage, as it failed the web stage of Deploy 201.605 assert.deepEqual(npmCiArgs([unit("events"), unit("repos")], npm), ["ci", "--prefer-offline", "--no-audit", "--no-fund", "--workspaces=false"]);
Fast pages, required checks on the branch, self-hosted runners, honest incidents606 assert.deepEqual(npmCiArgs([unit("events"), unit("status")], npm), [
The deploy's stages ride out a dropped npm connection as CI does. Each core, edge and front job keeps npm's downloads between runs (actions/cache on ~/.npm, keyed on the lockfile), and the deploy tool's install reads them first (npm ci --prefer-offline), so a registry connection reset mid-install no longer fails a stage, as it failed the web stage of Deploy 201.607 "ci", "--prefer-offline", "--no-audit", "--no-fund", "--include-workspace-root",
Fast pages, required checks on the branch, self-hosted runners, honest incidents608 "-w", "@g1t/contracts", "-w", "@g1t/status", "-w", "@g1t/theme",
609 ]);
610});
611
612// ── Cloudflare's answers ──────────────────────────────────────────────────
613
614const version = (id, number, message, triggered = "deployment") => ({
615 id,
616 number,
617 metadata: { created_on: "2026-10-05T00:00:00Z", author_email: "a@b" },
618 annotations: { "workers/triggered_by": triggered, ...(message ? { "workers/message": message } : {}) },
619});
620
621test("a deploy is annotated with its commit, and read back", () => {
622 const { message, tag } = annotation(HEAD, "A subject ".repeat(20));
623 assert.ok(message.length <= 100);
624 assert.equal(commitFrom(message), HEAD);
625 assert.equal(tag, `g1t-${HEAD.slice(0, 12)}`);
626 assert.equal(commitFrom(message.replace("g1t-deploy", "g1t-deploy-dirty")), null);
627 assert.equal(commitFrom("deployed by hand"), null);
628});
629
630test("the live commit: the live version's, looking through secret changes", () => {
631 const versions = [version("v1", 1, annotation(OLD).message), version("v2", 2, null, "secret"), version("v3", 3, null, "version_upload")];
632 const status = (id) => ({ versions: [{ version_id: id, percentage: 100 }] });
633 assert.equal(liveCommit(status("v1"), versions).sha, OLD);
634 assert.equal(liveCommit(status("v2"), versions).sha, OLD);
635 assert.equal(liveCommit(status("v3"), versions).sha, null);
636 assert.match(liveCommit(status("v9"), versions).why, /not among/);
637 const split = liveCommit({ versions: [{ version_id: "v1", percentage: 10 }, { version_id: "v2", percentage: 90 }] }, versions);
638 assert.equal(split.sha, OLD);
639 assert.equal(split.split, true);
640});
641
642test("Wrangler's output: pending migrations and the version a deploy made", () => {
643 const pending = `
644 ⛅️ wrangler 4.146.0
645Resource location: remote
646Migrations to be applied:
647┌──────────────────────┐
648│ Name │
649├──────────────────────┤
650│ 0021_confidence.sql │
651├──────────────────────┤
652│ 0022_more.sql │
653└──────────────────────┘`;
654 assert.deepEqual(pendingFrom(pending), ["0021_confidence.sql", "0022_more.sql"]);
655 assert.deepEqual(pendingFrom("Resource location: remote\n\n✅ No migrations to apply!"), []);
656 assert.equal(versionFrom("Deployed g1t-events triggers\nCurrent Version ID: 2c7fc93a-82d9-4850-8a77-ba887d157a4f\n"), "2c7fc93a-82d9-4850-8a77-ba887d157a4f");
657});
658
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check659// Cloudflare's REST answers, as the API sends them (Wrangler's --json
660// prints the first deployment, and the versions' items).
661const AUTH = { token: "t", account: "acct" };
662const ok = (result) => ({ success: true, errors: [], messages: [], result });
663const refused = (code, message) => ({ success: false, errors: [{ code, message }], messages: [], result: null });
664/** A fetch that answers by path: { "GET /path": [status, body] }. */
665function fakeFetch(answers, calls = []) {
666 return async (url, init) => {
667 const path = url.replace("https://api.cloudflare.com/client/v4", "");
668 calls.push({ path, init });
669 const found = answers[`${init.method} ${path}`];
670 if (!found) throw new Error(`unexpected ${init.method} ${path}`);
671 const [status, body] = found;
672 return { status, text: async () => (typeof body === "string" ? body : JSON.stringify(body)) };
673 };
674}
675const SCRIPT = "/accounts/acct/workers/scripts/g1t-events";
676const deploymentsAnswer = (id) =>
677 ok({
678 deployments: [
679 { id: "d2", source: "wrangler", strategy: "percentage", versions: [{ version_id: id, percentage: 100 }], annotations: { "workers/triggered_by": "deployment" } },
680 { id: "d1", source: "wrangler", strategy: "percentage", versions: [{ version_id: "v1", percentage: 100 }] },
681 ],
682 });
683const versionsAnswer = ok({ items: [version("v1", 1, annotation(OLD).message), version("v2", 2, null, "secret")] });
684
685test("the API's answers: the live commit as Wrangler's would give it", async () => {
686 const calls = [];
687 const fetchImpl = fakeFetch({ [`GET ${SCRIPT}/deployments`]: [200, deploymentsAnswer("v2")], [`GET ${SCRIPT}/versions?deployable=true`]: [200, versionsAnswer] }, calls);
688 const found = await readLive(unit("events"), { auth: AUTH, fetchImpl });
689 assert.equal(found.sha, OLD);
690 assert.equal(found.version, "v2");
691 assert.equal(found.at, "2026-10-05T00:00:00Z");
692 assert.equal(calls[0].init.headers.authorization, "Bearer t");
693 // The same as liveCommit over what Wrangler printed.
694 assert.deepEqual(liveFromApi("w", deploymentsAnswer("v2").result, versionsAnswer.result), liveCommit(deploymentsAnswer("v2").result.deployments[0], versionsAnswer.result.items));
695 // Versions that cannot be read: the live one is not among them.
696 assert.match(liveFromApi("w", deploymentsAnswer("v2").result, null).why, /not among/);
697 // No deployment at all is an error, as `deployments status` made it.
698 assert.match(liveFromApi("w", { deployments: [] }, versionsAnswer.result).error, /no deployments/);
699});
700
701test("the API's answers: a Worker never deployed, and a refusal, never throw", async () => {
702 const missing = fakeFetch({
703 [`GET ${SCRIPT}/deployments`]: [404, refused(10007, "workers.api.error.script_not_found")],
704 [`GET ${SCRIPT}/versions?deployable=true`]: [404, refused(10007, "workers.api.error.script_not_found")],
705 });
706 assert.deepEqual(await readLive(unit("events"), { auth: AUTH, fetchImpl: missing }), { sha: null, missing: true, why: "never deployed" });
707 const denied = fakeFetch({
708 [`GET ${SCRIPT}/deployments`]: [400, refused(10000, "Authentication error")],
709 [`GET ${SCRIPT}/versions?deployable=true`]: [400, refused(10000, "Authentication error")],
710 });
711 const found = await readLive(unit("events"), { auth: AUTH, fetchImpl: denied });
712 assert.equal(found.sha, null);
713 assert.match(found.error, /Authentication error \(10000\)/);
714 const broken = async () => {
715 throw new Error("connect ECONNREFUSED");
716 };
717 const offline = await readLive(unit("events"), { auth: AUTH, fetchImpl: broken });
718 assert.match(offline.error, /ECONNREFUSED/);
719 const garbled = fakeFetch({ [`GET ${SCRIPT}/deployments`]: [200, "<html>"], [`GET ${SCRIPT}/versions?deployable=true`]: [200, versionsAnswer] });
720 assert.match((await readLive(unit("events"), { auth: AUTH, fetchImpl: garbled })).error, /<html>/);
721});
722
723test("the API is used only with the token Wrangler would be given", () => {
724 assert.equal(apiAuth({ CLOUDFLARE_API_TOKEN: "shell" }), null);
725 assert.deepEqual(apiAuth({ CI: "true", CLOUDFLARE_API_TOKEN: "ci", CLOUDFLARE_ACCOUNT_ID: "a" }), { token: "ci", account: "a" });
726 assert.equal(apiAuth({ CLOUDFLARE_DEPLOY_TOKEN: "mine" }).token, "mine");
727 assert.equal(apiAuth({ CLOUDFLARE_DEPLOY_TOKEN: "mine" }).account, "1e6f2cffa3f445920836e8ebe446bb58");
728});
729
730test("pending migrations: the folder's files less the names in d1_migrations", async () => {
731 const events = unit("events");
732 const db = databaseOf(events);
733 assert.equal(db.id, events.config.d1_databases[0].database_id);
734 assert.equal(db.table, "d1_migrations");
735 const files = migrationFiles(join(ROOT, db.dir));
736 assert.ok(files.length > 2 && files.every((f) => f.endsWith(".sql")));
737 assert.deepEqual(files, [...files].sort());
738
739 assert.deepEqual(pendingAgainst(["0001_a.sql", "0002_b.sql", "0003_c.sql"], [{ results: [{ name: "0001_a.sql" }, { name: "0002_b.sql" }], success: true, meta: {} }]), ["0003_c.sql"]);
740 assert.deepEqual(pendingAgainst(["0001_a.sql"], [{ results: [], success: true }]), ["0001_a.sql"]);
741
742 const query = `POST /accounts/acct/d1/database/${db.id}/query`;
743 const calls = [];
744 const applied = files.slice(0, -1).map((name, i) => ({ name, id: i + 1 }));
745 const found = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [200, ok([{ results: applied, success: true, meta: {} }])] }, calls) });
746 assert.deepEqual(found, { pending: files.slice(-1) });
747 assert.match(JSON.parse(calls[0].init.body).sql, /^SELECT name FROM "d1_migrations"/);
748 const upToDate = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [200, ok([{ results: files.map((name) => ({ name })) }])] }) });
749 assert.deepEqual(upToDate, { pending: [] });
750 // A database never migrated has no table: everything is pending.
751 const fresh = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [400, refused(7500, "no such table: d1_migrations: SQLITE_ERROR")] }) });
752 assert.deepEqual(fresh, { pending: files });
753 const denied = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [400, refused(10000, "Authentication error")] }) });
754 assert.match(denied.error, /Authentication error/);
755});
756
Fast pages, required checks on the branch, self-hosted runners, honest incidents757test("the pool runs everything, no more than its limit at once", async () => {
758 let running = 0;
759 let most = 0;
760 const out = await pool([1, 2, 3, 4, 5, 6, 7], 3, async (n) => {
761 running++;
762 most = Math.max(most, running);
763 await new Promise((resolve) => setTimeout(resolve, 5));
764 running--;
765 return n * 2;
766 });
767 assert.deepEqual(out, [2, 4, 6, 8, 10, 12, 14]);
768 assert.equal(most, 3);
769});
770
771// ── Everything else that reads the list ───────────────────────────────────
772
773test("self-hosting builds every Rust service the manifest says it runs", () => {
774 const dockerfile = readFileSync(join(ROOT, "deploy/self-host/Dockerfile"), "utf8");
775 const loops = [...dockerfile.matchAll(/for service in ([a-z ]+); do/g)].map((m) => m[1].trim().split(/\s+/).sort());
776 const wanted = stack.units.filter((u) => u.self_host === "run" && u.kind === "rust-worker").map((u) => u.path.split("/").pop()).sort();
777 assert.ok(loops.length >= 2);
778 for (const loop of loops) assert.deepEqual(loop, wanted);
779});
780
The docs folder is gone, and what it held lives where people read it: how a self-hosted g1t runs and how to deploy g1t to Cloudflare are pages on docs.g1t.sh under Run g1t yourself, and speed, rate limits and operating g1t.sh are sections of CONTRIBUTING.md; code that cited a file in docs/ now points to the page or section that covers it, or says what it means itself, and applied migrations and the runner images are left as they were.781test("the self-hosting architecture page names every unit", () => {
782 const page = "apps/docs/src/content/docs/guides/self-hosting-architecture.md";
783 const doc = readFileSync(join(ROOT, page), "utf8");
784 for (const u of stack.units) assert.ok(doc.includes(`| \`${u.path}\` |`), `${u.path} is missing from ${page}`);
Fast pages, required checks on the branch, self-hosted runners, honest incidents785});
786
787test("resolveStack works on a manifest with no workspace crates or packages", () => {
788 const bare = resolveStack(loadStack(), { cargo: new Map(), npm: new Map() });
789 assert.deepEqual(bare.units.find((u) => u.id === "events").dependsOn, []);
790});
Deploying: never roll production back by accident791
792test("decide: a commit older than what runs is never deployed by accident, even forced; --rollback means it", () => {
793 const newer = { has: () => true, changed: () => ["services/events/src/lib.rs"], show: () => "", isAncestor: (older, newer) => older === HEAD && newer === OLD };
794 const refused = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, force: true, gitApi: newer });
795 assert.equal(refused[0].deploy, false);
796 assert.match(refused[0].reason, /newer than this commit/);
797 const meant = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, rollback: true, gitApi: newer });
798 assert.equal(meant[0].deploy, true);
799 assert.match(meant[0].reason, /rolling back/);
800});
Deploying: a deploy by hand shows on the repository's Deployments page801
802test("a deploy by hand reports to the repository on g1t, and a workflow's does not", async () => {
803 const { g1tRemote, inWorkflow, deployToken, reportDeployment } = await import("./report.mjs");
804 assert.deepEqual(g1tRemote(["git@github.com:flagon-io/g1t.git", "https://g1t.sh/flagon-io/g1t.git"]), { api: "https://api.g1t.sh", repo: "flagon-io/g1t" });
805 assert.equal(g1tRemote(["git@github.com:flagon-io/g1t.git"]), null);
806 assert.equal(inWorkflow({ GITHUB_ACTIONS: "true" }), true);
807 assert.equal(inWorkflow({}), false);
808 assert.equal(deployToken({ G1T_DEPLOY_TOKEN: " g1t_x \n" }, "/nowhere"), "g1t_x");
809 assert.equal(deployToken({}, "/nowhere"), null);
810 let called = false;
811 const settle = await reportDeployment({ head: "abc", subject: "s", units: ["web"], log: () => {}, env: { GITHUB_ACTIONS: "true", G1T_DEPLOY_TOKEN: "t" }, fetchImpl: () => { called = true; } });
812 assert.equal(settle, null);
813 assert.equal(called, false);
814});

This file's history is long; its oldest lines are credited to the oldest commit read.