Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
813 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.

Tab bars on pull requests, search and profiles scroll without showing a scrollbar1// 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,
17 pushImage,
18 pushedDigest,
19 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";
Tab bars on pull requests, search and profiles scroll without showing a scrollbar41import { changedNames, parseCargoLock, parseNpmLock, reaches } from "./lockfiles.mjs";
Merge remote-tracking branch 'origin/main' into workspace-chat42import { decide, git, planJson, pool } from "./plan.mjs";
Tab bars on pull requests, search and profiles scroll without showing a scrollbar43import {
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,
Tab bars on pull requests, search and profiles scroll without showing a scrollbar57 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,
Tab bars on pull requests, search and profiles scroll without showing a scrollbar61} 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 });
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.106 const artifacts = u("artifacts", "g1t-artifacts", ["g1t-notify", "g1t-identity"]);
107 const notify = u("notify", "g1t-notify", ["g1t-artifacts", "g1t-identity"]);
108 const chat = u("chat", "g1t-chat", ["g1t-notify"]);
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.109 const ids = (list) => list.map((wave) => wave.map((x) => x.id));
110 // Nothing new: one wave, as before.
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.111 assert.deepEqual(ids(waves([artifacts, notify, chat], () => false)), [["artifacts", "notify", "chat"]]);
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.112 // A renamed Worker: it first, then what binds to it.
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.113 assert.deepEqual(ids(waves([notify, chat, artifacts], (x) => x.id === "artifacts")), [["chat", "artifacts"], ["notify"]]);
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.114 // New Workers bound to each other: no order helps, so together.
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.115 assert.deepEqual(ids(waves([artifacts, notify], () => true)), [["artifacts", "notify"]]);
116 // This repository's chat unit binds to the notify Worker, in the same stage.
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.117 const real = (id) => stack.units.find((x) => x.id === id);
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.118 assert.equal(real("chat").stage, real("notify").stage);
119 assert.deepEqual(ids(waves([real("chat"), real("notify")], (x) => x.id === "notify")), [["notify"], ["chat"]]);
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.120});
121
Tab bars on pull requests, search and profiles scroll without showing a scrollbar122test("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"));
Tab bars on pull requests, search and profiles scroll without showing a scrollbar136 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");
147 // The runner bumps lockfiles with the scanner's lockfile parser (security updates).
148 assert.deepEqual(runner.image.dirs, ["crates/actions", "crates/runner", "crates/scan"]);
149 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
255test("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
266test("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", () => {
338 assert.deepEqual(ids(stack.units, ["crates/scan/src/lib.rs"]), ["repos", "security", "runner"]);
339 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"));
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.415 assert.ok(!only("billing", "services/billing/src/compute.rs"));
416 assert.ok(!only("billing", "services/billing/src/limits.rs"));
Merge remote-tracking branch 'origin/main' into workspace-chat417 assert.ok(!only("api", "apps/api/src/toolkit.rs"));
418});
419
420test("decide: a change only to tests deploys nothing", () => {
421 const units = ["actions", "billing", "runner"].map(unit);
422 const live = { actions: { sha: OLD }, billing: { sha: OLD }, runner: { sha: OLD } };
423 const files = {
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.424 [`${HEAD}:services/billing/src/compute.rs`]: "#[cfg(test)]\n#[path = \"compute_tests.rs\"]\nmod tests;\n",
425 [`${HEAD}:services/billing/src/lib.rs`]: "mod compute;\n",
Merge remote-tracking branch 'origin/main' into workspace-chat426 };
427 const gitApi = {
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.428 ...fakeGit(["crates/actions/tests/repository_workflows.rs", "services/billing/src/compute_tests.rs"]),
Merge remote-tracking branch 'origin/main' into workspace-chat429 show: (sha, path) => files[`${sha}:${path}`] ?? "",
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.430 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/compute_tests.rs"] : []),
Merge remote-tracking branch 'origin/main' into workspace-chat431 };
432 const decisions = decide(units, { live, head: HEAD, gitApi });
433 assert.deepEqual(decisions.map((d) => [d.unit.id, d.deploy, d.image]), [["actions", false, false], ["billing", false, false], ["runner", false, false]]);
434 // With a source that ships beside them, only that is listed.
The agents, models and context services are deleted. Nothing calls them any more: workspace agents, the hosted model proxy at models.g1t.sh with its AI Gateway, and the context hub with its search index are gone with the features they served. The deploy stack no longer builds or deploys them, so it no longer needs CONNECTORS_KEY, AI_GATEWAY_TOKEN, ANTHROPIC_API_KEY or WORKERS_AI_TOKEN, and a self-hosted install no longer generates CONNECTORS_KEY or stubs them out. Their databases, queues, index, gateway and domain stay in Cloudflare until they are deleted by hand after this is deployed.435 const mixed = decide([unit("billing")], { live, head: HEAD, gitApi: { ...gitApi, changed: () => ["services/billing/src/compute_tests.rs", "services/billing/src/compute.rs"] } });
436 assert.deepEqual([mixed[0].deploy, mixed[0].files], [true, ["services/billing/src/compute.rs"]]);
Merge remote-tracking branch 'origin/main' into workspace-chat437});
438
Tab bars on pull requests, search and profiles scroll without showing a scrollbar439test("own folders, declared inputs and root files", () => {
440 assert.deepEqual(ids(stack.units, ["services/pages/src/index.ts"]), ["pages"]);
441 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.442 assert.deepEqual(ids(stack.units, ["LICENSE", "README.md", "deploy/stack.jsonc"]), []);
Tab bars on pull requests, search and profiles scroll without showing a scrollbar443 assert.deepEqual(ids(stack.units, ["scripts/build-rust-worker.mjs"]), stack.units.filter((u) => u.kind === "rust-worker").map((u) => u.id));
444 // services/events is not a prefix of services/eventsx.
445 assert.equal(touches(unit("events"), ["services/eventsx/a.rs"]), null);
446});
447
448// ── Lockfiles ─────────────────────────────────────────────────────────────
449
450const lock = (sha2) => `version = 4
451
452[[package]]
453name = "g1t-events"
454version = "0.1.0"
455dependencies = [
456 "g1t-kit",
457]
458
459[[package]]
460name = "g1t-kit"
461version = "0.1.0"
462dependencies = [
463 "serde",
464]
465
466[[package]]
467name = "g1t-webhooks"
468version = "0.1.0"
469dependencies = [
470 "g1t-kit",
471 "g1t-secrets",
472]
473
474[[package]]
475name = "g1t-secrets"
476version = "0.1.0"
477dependencies = [
478 "sha2 ${sha2}",
479]
480
481[[package]]
482name = "serde"
483version = "1.0.0"
484source = "registry+https://github.com/rust-lang/crates.io-index"
485
486[[package]]
487name = "sha2"
488version = "${sha2}"
489source = "registry+https://github.com/rust-lang/crates.io-index"
490`;
491
492test("a Cargo.lock change reaches only crates that use what changed", () => {
493 const before = parseCargoLock(lock("0.10.8"));
494 const after = parseCargoLock(lock("0.10.9"));
495 const names = changedNames(before, after);
496 assert.deepEqual([...names], ["sha2"]);
497 assert.ok(reaches(after, ["g1t-webhooks"], names));
498 assert.ok(!reaches(after, ["g1t-events"], names));
499});
500
501test("a package-lock change reaches through workspace links", () => {
502 const make = (version) =>
503 JSON.stringify({
504 lockfileVersion: 3,
505 packages: {
506 "": { devDependencies: { wrangler: "^4" } },
507 "apps/web": { dependencies: { "@g1t/theme": "*", react: "^19" } },
508 "services/pages": { dependencies: {} },
509 "packages/theme": { dependencies: { fontkit: "^1" } },
510 "node_modules/@g1t/theme": { resolved: "packages/theme", link: true },
511 "node_modules/react": { version: "19.0.0" },
512 "node_modules/fontkit": { version },
513 "node_modules/wrangler": { version: "4.1.0" },
514 },
515 });
516 const before = parseNpmLock(make("1.0.0"));
517 const after = parseNpmLock(make("1.0.1"));
518 const names = changedNames(before, after);
519 assert.deepEqual([...names], ["fontkit"]);
520 assert.ok(reaches(after, ["apps/web", ""], names));
521 assert.ok(!reaches(after, ["services/pages", ""], names));
522});
523
524// ── Deciding ──────────────────────────────────────────────────────────────
525
526const HEAD = "a".repeat(40);
527const OLD = "b".repeat(40);
528const fakeGit = (files, { has = true, locks = {} } = {}) => ({
529 has: () => has,
530 changed: () => files,
531 show: (sha, path) => locks[`${sha}:${path}`] ?? "",
532});
533
534test("decide: changed units deploy, others wait, unknown ones deploy", () => {
535 const units = ["events", "repos", "pages", "web"].map(unit);
536 const live = { events: { sha: OLD }, repos: { sha: OLD }, pages: { sha: HEAD }, web: { sha: null, why: "never deployed" } };
537 const decisions = decide(units, { live, head: HEAD, gitApi: fakeGit(["crates/scan/src/lib.rs"]) });
538 const by = Object.fromEntries(decisions.map((d) => [d.unit.id, d]));
539 assert.equal(by.events.deploy, false);
540 assert.equal(by.events.reason, "nothing it is built from changed");
541 assert.equal(by.repos.deploy, true);
542 assert.match(by.repos.reason, /crates\/scan\/src\/lib.rs via crates\/scan/);
543 assert.equal(by.pages.deploy, false);
544 assert.equal(by.pages.reason, "up to date");
545 assert.equal(by.web.deploy, true);
546 assert.match(by.web.reason, /never deployed/);
547});
548
549test("decide: --force deploys unchanged units; a commit missing from history deploys", () => {
550 const forced = decide([unit("events")], { live: { events: { sha: HEAD } }, head: HEAD, force: true, gitApi: fakeGit([]) });
551 assert.equal(forced[0].deploy, true);
552 const shallow = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, gitApi: fakeGit([], { has: false }) });
553 assert.equal(shallow[0].deploy, true);
554 assert.match(shallow[0].reason, /does not have/);
555});
556
557test("decide: Cargo.lock counts only where it reaches", () => {
558 const locks = { [`${OLD}:Cargo.lock`]: lock("0.10.8"), [`${HEAD}:Cargo.lock`]: lock("0.10.9") };
559 const units = [unit("events"), unit("webhooks")];
560 const live = { events: { sha: OLD }, webhooks: { sha: OLD } };
561 const decisions = decide(units, { live, head: HEAD, gitApi: fakeGit(["Cargo.lock"], { locks }) });
562 assert.deepEqual(decisions.map((d) => [d.unit.id, d.deploy]), [["events", false], ["webhooks", true]]);
563});
564
565test("decide: the runner's image rebuilds only when what it is built from changed", () => {
566 const live = { runner: { sha: OLD } };
567 const code = decide([unit("runner")], { live, head: HEAD, gitApi: fakeGit(["services/runner/src/index.ts"]) });
568 assert.deepEqual([code[0].deploy, code[0].image], [true, false]);
569 const image = decide([unit("runner")], { live, head: HEAD, gitApi: fakeGit(["crates/runner/src/main.rs"]) });
570 assert.deepEqual([image[0].deploy, image[0].image], [true, true]);
571});
572
573test("the plan as data: migrations, and each stage's units in jobs that share a build", () => {
574 const units = ["events", "repos", "projects", "api", "web", "docs"].map(unit);
575 const decisions = units.map((u) => ({ unit: u, deploy: true, reason: "x", since: null, image: false }));
576 const data = planJson(stack, decisions, { events: { pending: ["0003_x.sql"] }, repos: { pending: [] } }, HEAD);
577 assert.deepEqual(data.migrations, [{ unit: "events", database: "g1t-events", pending: ["0003_x.sql"] }]);
578 assert.deepEqual(data.stages.core.jobs, [
Merge branch 'main' into actions-toolkit-oidc-artifacts579 { group: "rust", units: "events,repos", rust: true, image: false },
580 { group: "ts", units: "projects", rust: false, image: false },
Tab bars on pull requests, search and profiles scroll without showing a scrollbar581 ]);
Merge branch 'main' into actions-toolkit-oidc-artifacts582 assert.deepEqual(data.stages.edge.jobs, [{ group: "rust", units: "api", rust: true, image: false }]);
Tab bars on pull requests, search and profiles scroll without showing a scrollbar583 assert.deepEqual(data.stages.front.jobs, [
Merge branch 'main' into actions-toolkit-oidc-artifacts584 { group: "web", units: "web", rust: false, image: false },
585 { group: "docs", units: "docs", rust: false, image: false },
Tab bars on pull requests, search and profiles scroll without showing a scrollbar586 ]);
587 assert.deepEqual(data.stage_order, ["core", "edge", "front"]);
588 assert.deepEqual(buildGroups([]), []);
589 // Eleven Rust workers go to three jobs; a unit whose image rebuilds gets its own.
590 const core = stack.units.filter((u) => u.stage === "core");
591 const jobs = buildGroups(core, ["runner"]);
592 assert.deepEqual(jobs.filter((j) => j.rust).map((j) => j.units.split(",").length), [4, 4, 3]);
Merge branch 'main' into actions-toolkit-oidc-artifacts593 assert.ok(jobs.some((j) => j.group === "runner-image" && j.units === "runner" && j.image && !j.rust));
Tab bars on pull requests, search and profiles scroll without showing a scrollbar594 assert.ok(!jobs.find((j) => j.group === "ts").units.includes("runner"));
595});
596
597test("units are picked by short name, folder or Worker name", () => {
598 assert.deepEqual(pick(stack, ["billing,services/web".replace("services/web", "apps/web"), "g1t-api"]).map((u) => u.id), ["billing", "web", "api"]);
599 assert.throws(() => pick(stack, ["nope"]), /No unit called nope/);
600});
601
602test("a CI job installs only what its units need", () => {
603 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.604 assert.deepEqual(npmCiArgs([unit("events"), unit("repos")], npm), ["ci", "--prefer-offline", "--no-audit", "--no-fund", "--workspaces=false"]);
Tab bars on pull requests, search and profiles scroll without showing a scrollbar605 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.606 "ci", "--prefer-offline", "--no-audit", "--no-fund", "--include-workspace-root",
Tab bars on pull requests, search and profiles scroll without showing a scrollbar607 "-w", "@g1t/contracts", "-w", "@g1t/status", "-w", "@g1t/theme",
608 ]);
609});
610
611// ── Cloudflare's answers ──────────────────────────────────────────────────
612
613const version = (id, number, message, triggered = "deployment") => ({
614 id,
615 number,
616 metadata: { created_on: "2026-10-05T00:00:00Z", author_email: "a@b" },
617 annotations: { "workers/triggered_by": triggered, ...(message ? { "workers/message": message } : {}) },
618});
619
620test("a deploy is annotated with its commit, and read back", () => {
621 const { message, tag } = annotation(HEAD, "A subject ".repeat(20));
622 assert.ok(message.length <= 100);
623 assert.equal(commitFrom(message), HEAD);
624 assert.equal(tag, `g1t-${HEAD.slice(0, 12)}`);
625 assert.equal(commitFrom(message.replace("g1t-deploy", "g1t-deploy-dirty")), null);
626 assert.equal(commitFrom("deployed by hand"), null);
627});
628
629test("the live commit: the live version's, looking through secret changes", () => {
630 const versions = [version("v1", 1, annotation(OLD).message), version("v2", 2, null, "secret"), version("v3", 3, null, "version_upload")];
631 const status = (id) => ({ versions: [{ version_id: id, percentage: 100 }] });
632 assert.equal(liveCommit(status("v1"), versions).sha, OLD);
633 assert.equal(liveCommit(status("v2"), versions).sha, OLD);
634 assert.equal(liveCommit(status("v3"), versions).sha, null);
635 assert.match(liveCommit(status("v9"), versions).why, /not among/);
636 const split = liveCommit({ versions: [{ version_id: "v1", percentage: 10 }, { version_id: "v2", percentage: 90 }] }, versions);
637 assert.equal(split.sha, OLD);
638 assert.equal(split.split, true);
639});
640
641test("Wrangler's output: pending migrations and the version a deploy made", () => {
642 const pending = `
643 ⛅️ wrangler 4.146.0
644Resource location: remote
645Migrations to be applied:
646┌──────────────────────┐
647│ Name │
648├──────────────────────┤
649│ 0021_confidence.sql │
650├──────────────────────┤
651│ 0022_more.sql │
652└──────────────────────┘`;
653 assert.deepEqual(pendingFrom(pending), ["0021_confidence.sql", "0022_more.sql"]);
654 assert.deepEqual(pendingFrom("Resource location: remote\n\n✅ No migrations to apply!"), []);
655 assert.equal(versionFrom("Deployed g1t-events triggers\nCurrent Version ID: 2c7fc93a-82d9-4850-8a77-ba887d157a4f\n"), "2c7fc93a-82d9-4850-8a77-ba887d157a4f");
656});
657
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check658// Cloudflare's REST answers, as the API sends them (Wrangler's --json
659// prints the first deployment, and the versions' items).
660const AUTH = { token: "t", account: "acct" };
661const ok = (result) => ({ success: true, errors: [], messages: [], result });
662const refused = (code, message) => ({ success: false, errors: [{ code, message }], messages: [], result: null });
663/** A fetch that answers by path: { "GET /path": [status, body] }. */
664function fakeFetch(answers, calls = []) {
665 return async (url, init) => {
666 const path = url.replace("https://api.cloudflare.com/client/v4", "");
667 calls.push({ path, init });
668 const found = answers[`${init.method} ${path}`];
669 if (!found) throw new Error(`unexpected ${init.method} ${path}`);
670 const [status, body] = found;
671 return { status, text: async () => (typeof body === "string" ? body : JSON.stringify(body)) };
672 };
673}
674const SCRIPT = "/accounts/acct/workers/scripts/g1t-events";
675const deploymentsAnswer = (id) =>
676 ok({
677 deployments: [
678 { id: "d2", source: "wrangler", strategy: "percentage", versions: [{ version_id: id, percentage: 100 }], annotations: { "workers/triggered_by": "deployment" } },
679 { id: "d1", source: "wrangler", strategy: "percentage", versions: [{ version_id: "v1", percentage: 100 }] },
680 ],
681 });
682const versionsAnswer = ok({ items: [version("v1", 1, annotation(OLD).message), version("v2", 2, null, "secret")] });
683
684test("the API's answers: the live commit as Wrangler's would give it", async () => {
685 const calls = [];
686 const fetchImpl = fakeFetch({ [`GET ${SCRIPT}/deployments`]: [200, deploymentsAnswer("v2")], [`GET ${SCRIPT}/versions?deployable=true`]: [200, versionsAnswer] }, calls);
687 const found = await readLive(unit("events"), { auth: AUTH, fetchImpl });
688 assert.equal(found.sha, OLD);
689 assert.equal(found.version, "v2");
690 assert.equal(found.at, "2026-10-05T00:00:00Z");
691 assert.equal(calls[0].init.headers.authorization, "Bearer t");
692 // The same as liveCommit over what Wrangler printed.
693 assert.deepEqual(liveFromApi("w", deploymentsAnswer("v2").result, versionsAnswer.result), liveCommit(deploymentsAnswer("v2").result.deployments[0], versionsAnswer.result.items));
694 // Versions that cannot be read: the live one is not among them.
695 assert.match(liveFromApi("w", deploymentsAnswer("v2").result, null).why, /not among/);
696 // No deployment at all is an error, as `deployments status` made it.
697 assert.match(liveFromApi("w", { deployments: [] }, versionsAnswer.result).error, /no deployments/);
698});
699
700test("the API's answers: a Worker never deployed, and a refusal, never throw", async () => {
701 const missing = fakeFetch({
702 [`GET ${SCRIPT}/deployments`]: [404, refused(10007, "workers.api.error.script_not_found")],
703 [`GET ${SCRIPT}/versions?deployable=true`]: [404, refused(10007, "workers.api.error.script_not_found")],
704 });
705 assert.deepEqual(await readLive(unit("events"), { auth: AUTH, fetchImpl: missing }), { sha: null, missing: true, why: "never deployed" });
706 const denied = fakeFetch({
707 [`GET ${SCRIPT}/deployments`]: [400, refused(10000, "Authentication error")],
708 [`GET ${SCRIPT}/versions?deployable=true`]: [400, refused(10000, "Authentication error")],
709 });
710 const found = await readLive(unit("events"), { auth: AUTH, fetchImpl: denied });
711 assert.equal(found.sha, null);
712 assert.match(found.error, /Authentication error \(10000\)/);
713 const broken = async () => {
714 throw new Error("connect ECONNREFUSED");
715 };
716 const offline = await readLive(unit("events"), { auth: AUTH, fetchImpl: broken });
717 assert.match(offline.error, /ECONNREFUSED/);
718 const garbled = fakeFetch({ [`GET ${SCRIPT}/deployments`]: [200, "<html>"], [`GET ${SCRIPT}/versions?deployable=true`]: [200, versionsAnswer] });
719 assert.match((await readLive(unit("events"), { auth: AUTH, fetchImpl: garbled })).error, /<html>/);
720});
721
722test("the API is used only with the token Wrangler would be given", () => {
723 assert.equal(apiAuth({ CLOUDFLARE_API_TOKEN: "shell" }), null);
724 assert.deepEqual(apiAuth({ CI: "true", CLOUDFLARE_API_TOKEN: "ci", CLOUDFLARE_ACCOUNT_ID: "a" }), { token: "ci", account: "a" });
725 assert.equal(apiAuth({ CLOUDFLARE_DEPLOY_TOKEN: "mine" }).token, "mine");
726 assert.equal(apiAuth({ CLOUDFLARE_DEPLOY_TOKEN: "mine" }).account, "1e6f2cffa3f445920836e8ebe446bb58");
727});
728
729test("pending migrations: the folder's files less the names in d1_migrations", async () => {
730 const events = unit("events");
731 const db = databaseOf(events);
732 assert.equal(db.id, events.config.d1_databases[0].database_id);
733 assert.equal(db.table, "d1_migrations");
734 const files = migrationFiles(join(ROOT, db.dir));
735 assert.ok(files.length > 2 && files.every((f) => f.endsWith(".sql")));
736 assert.deepEqual(files, [...files].sort());
737
738 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"]);
739 assert.deepEqual(pendingAgainst(["0001_a.sql"], [{ results: [], success: true }]), ["0001_a.sql"]);
740
741 const query = `POST /accounts/acct/d1/database/${db.id}/query`;
742 const calls = [];
743 const applied = files.slice(0, -1).map((name, i) => ({ name, id: i + 1 }));
744 const found = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [200, ok([{ results: applied, success: true, meta: {} }])] }, calls) });
745 assert.deepEqual(found, { pending: files.slice(-1) });
746 assert.match(JSON.parse(calls[0].init.body).sql, /^SELECT name FROM "d1_migrations"/);
747 const upToDate = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [200, ok([{ results: files.map((name) => ({ name })) }])] }) });
748 assert.deepEqual(upToDate, { pending: [] });
749 // A database never migrated has no table: everything is pending.
750 const fresh = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [400, refused(7500, "no such table: d1_migrations: SQLITE_ERROR")] }) });
751 assert.deepEqual(fresh, { pending: files });
752 const denied = await pendingMigrations(events, { auth: AUTH, fetchImpl: fakeFetch({ [query]: [400, refused(10000, "Authentication error")] }) });
753 assert.match(denied.error, /Authentication error/);
754});
755
Tab bars on pull requests, search and profiles scroll without showing a scrollbar756test("the pool runs everything, no more than its limit at once", async () => {
757 let running = 0;
758 let most = 0;
759 const out = await pool([1, 2, 3, 4, 5, 6, 7], 3, async (n) => {
760 running++;
761 most = Math.max(most, running);
762 await new Promise((resolve) => setTimeout(resolve, 5));
763 running--;
764 return n * 2;
765 });
766 assert.deepEqual(out, [2, 4, 6, 8, 10, 12, 14]);
767 assert.equal(most, 3);
768});
769
770// ── Everything else that reads the list ───────────────────────────────────
771
772test("self-hosting builds every Rust service the manifest says it runs", () => {
773 const dockerfile = readFileSync(join(ROOT, "deploy/self-host/Dockerfile"), "utf8");
774 const loops = [...dockerfile.matchAll(/for service in ([a-z ]+); do/g)].map((m) => m[1].trim().split(/\s+/).sort());
775 const wanted = stack.units.filter((u) => u.self_host === "run" && u.kind === "rust-worker").map((u) => u.path.split("/").pop()).sort();
776 assert.ok(loops.length >= 2);
777 for (const loop of loops) assert.deepEqual(loop, wanted);
778});
779
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.780test("the self-hosting architecture page names every unit", () => {
781 const page = "apps/docs/src/content/docs/guides/self-hosting-architecture.md";
782 const doc = readFileSync(join(ROOT, page), "utf8");
783 for (const u of stack.units) assert.ok(doc.includes(`| \`${u.path}\` |`), `${u.path} is missing from ${page}`);
Tab bars on pull requests, search and profiles scroll without showing a scrollbar784});
785
786test("resolveStack works on a manifest with no workspace crates or packages", () => {
787 const bare = resolveStack(loadStack(), { cargo: new Map(), npm: new Map() });
788 assert.deepEqual(bare.units.find((u) => u.id === "events").dependsOn, []);
789});
Deploying: never roll production back by accident790
791test("decide: a commit older than what runs is never deployed by accident, even forced; --rollback means it", () => {
792 const newer = { has: () => true, changed: () => ["services/events/src/lib.rs"], show: () => "", isAncestor: (older, newer) => older === HEAD && newer === OLD };
793 const refused = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, force: true, gitApi: newer });
794 assert.equal(refused[0].deploy, false);
795 assert.match(refused[0].reason, /newer than this commit/);
796 const meant = decide([unit("events")], { live: { events: { sha: OLD } }, head: HEAD, rollback: true, gitApi: newer });
797 assert.equal(meant[0].deploy, true);
798 assert.match(meant[0].reason, /rolling back/);
799});
Deploying: a deploy by hand shows on the repository's Deployments page800
801test("a deploy by hand reports to the repository on g1t, and a workflow's does not", async () => {
802 const { g1tRemote, inWorkflow, deployToken, reportDeployment } = await import("./report.mjs");
803 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" });
804 assert.equal(g1tRemote(["git@github.com:flagon-io/g1t.git"]), null);
805 assert.equal(inWorkflow({ GITHUB_ACTIONS: "true" }), true);
806 assert.equal(inWorkflow({}), false);
807 assert.equal(deployToken({ G1T_DEPLOY_TOKEN: " g1t_x \n" }, "/nowhere"), "g1t_x");
808 assert.equal(deployToken({}, "/nowhere"), null);
809 let called = false;
810 const settle = await reportDeployment({ head: "abc", subject: "s", units: ["web"], log: () => {}, env: { GITHUB_ACTIONS: "true", G1T_DEPLOY_TOKEN: "t" }, fetchImpl: () => { called = true; } });
811 assert.equal(settle, null);
812 assert.equal(called, false);
813});

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