Skip to content
221 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.

Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow1// What a deploy would do: for each unit, the commit it runs, what changed
2// since, and its pending migrations. Git and Wrangler are passed in, so the
3// tests can give their own.
4
5import { execFileSync } from "node:child_process";
6
7import { changedNames, lockRoots, parseCargoLock, parseNpmLock, reaches } from "./lockfiles.mjs";
Merge remote-tracking branch 'origin/main' into workspace-chat8import { ROOT, byStage, buildGroups, codeStages, testOnlySource, touches, touchesImage } from "./stack.mjs";
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow9
10/** Git, read-only. */
11export const git = {
12 head: () => run(["rev-parse", "HEAD"]).trim(),
13 resolve: (rev) => run(["rev-parse", "--verify", `${rev}^{commit}`]).trim(),
14 subject: () => run(["log", "-1", "--format=%s"]).trim(),
15 /** Whether a commit is in this checkout's history. */
16 has: (sha) => {
17 try {
18 run(["cat-file", "-e", `${sha}^{commit}`]);
19 return true;
20 } catch {
21 return false;
22 }
23 },
24 /** A file's text at a commit, or "" if it is not there. */
25 show: (sha, path) => {
26 try {
27 return run(["show", `${sha}:${path}`]);
28 } catch {
29 return "";
30 }
31 },
Merge remote-tracking branch 'origin/main' into workspace-chat32 /** The files directly in a folder at a commit (repository-relative). */
33 list: (sha, dir) => {
34 try {
35 return run(["ls-tree", "--name-only", sha, "--", `${dir}/`]).split("\n").filter(Boolean);
36 } catch {
37 return [];
38 }
39 },
40 /** Files changed between two commits. */
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow41 changed: (from, to) => run(["diff", "--name-only", "--no-renames", from, to]).split("\n").filter(Boolean),
Deploying: never roll production back by accident42 /** Whether `older` is in `newer`'s history (and not the same commit). */
43 isAncestor: (older, newer) => {
44 if (older === newer) return false;
45 try {
46 run(["merge-base", "--is-ancestor", older, newer]);
47 return true;
48 } catch {
49 return false;
50 }
51 },
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow52 /** Uncommitted and untracked files (not ignored ones). */
53 dirty: () =>
54 run(["status", "--porcelain", "--untracked-files=all"])
55 .split("\n")
56 .filter(Boolean)
57 .map((line) => line.slice(3).replace(/^"|"$/g, "").split(" -> ").pop()),
58};
59
60function run(args) {
61 return execFileSync("git", args, { cwd: ROOT, encoding: "utf8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
62}
63
64/**
65 * Decides, for each unit in `units`, whether it deploys and why.
66 *
67 * live: unit id -> { sha, why? } (what readLive found)
68 * head: the commit being deployed
69 * force: deploy even what has not changed
70 * gitApi: { has, changed }
71 *
72 * Each unit gets { deploy, reason, since, files, image }: `files` are the
73 * changed files that touch it; `image` says whether its Containers image
74 * must be built.
75 */
Deploying: never roll production back by accident76export function decide(units, { live, head, force = false, rollback = false, gitApi = git }) {
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow77 const diffs = new Map();
78 const changedSince = (sha) => {
79 if (!diffs.has(sha)) diffs.set(sha, gitApi.changed(sha, head));
80 return diffs.get(sha);
81 };
82 // A lockfile change counts for a unit only if a package it uses changed.
83 const locks = new Map();
84 const lockChange = (sha, file) => {
85 const key = `${sha}:${file}`;
86 if (!locks.has(key)) {
87 const parse = file === "Cargo.lock" ? parseCargoLock : parseNpmLock;
88 const after = parse(gitApi.show(head, file));
89 locks.set(key, { after, names: changedNames(parse(gitApi.show(sha, file)), after) });
90 }
91 return locks.get(key);
92 };
Merge remote-tracking branch 'origin/main' into workspace-chat93 // A Rust source compiled only for tests (`#[cfg(test)] mod tests;`) is
94 // not in what deploys. Read at the commit being deployed, each file once.
95 const texts = new Map();
96 const listings = new Map();
97 const atHead = {
98 read: (path) => {
99 if (!texts.has(path)) texts.set(path, gitApi.show(head, path));
100 return texts.get(path);
101 },
102 list: (dir) => {
103 if (!listings.has(dir)) listings.set(dir, gitApi.list?.(head, dir) ?? []);
104 return listings.get(dir);
105 },
106 };
107 const testOnly = new Map();
108 const onlyForTests = (unit, file) => {
109 if (!file.endsWith(".rs") || !unit.crateDirs?.some((dir) => file.startsWith(`${dir}/src/`))) return false;
110 if (!testOnly.has(file)) testOnly.set(file, testOnlySource(file, unit.crateDirs, atHead));
111 return testOnly.get(file);
112 };
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow113 const relevant = (unit, sha, files) =>
114 files.filter((file) => {
Merge remote-tracking branch 'origin/main' into workspace-chat115 if (onlyForTests(unit, file)) return false;
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow116 if (file !== "Cargo.lock" && file !== "package-lock.json") return true;
117 const { after, names } = lockChange(sha, file);
118 const roots = lockRoots(unit)[file === "Cargo.lock" ? "cargo" : "npm"];
119 return reaches(after, roots, names);
120 });
121 return units.map((unit) => {
122 const found = live[unit.id] ?? { sha: null, why: "not read" };
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.123 // missing: its Worker does not exist yet (new, or renamed).
124 const decision = { unit, since: found.sha, missing: Boolean(found.missing), deploy: false, reason: "", files: [], image: false };
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow125 if (!found.sha) {
126 decision.deploy = true;
127 decision.reason = found.error ? `could not read what it runs: ${firstLine(found.error)}` : `no known commit (${found.why ?? "unknown"})`;
128 decision.image = Boolean(unit.image);
129 return decision;
130 }
131 if (found.sha === head) {
132 decision.deploy = force;
133 decision.reason = force ? "forced; already at this commit" : "up to date";
134 return decision;
135 }
Deploying: never roll production back by accident136 // What runs is newer than this commit: deploying would roll it back
137 // (a re-run of an old workflow run, say). Never by accident: only with
138 // --rollback, whatever --force says.
139 if (gitApi.has(found.sha) && gitApi.isAncestor?.(head, found.sha)) {
140 decision.deploy = rollback;
141 decision.reason = rollback
142 ? `rolling back from ${found.sha.slice(0, 12)}`
143 : `runs ${found.sha.slice(0, 12)}, which is newer than this commit; deploying would roll it back (pass --rollback to mean it)`;
144 decision.image = rollback && Boolean(unit.image);
145 return decision;
146 }
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow147 if (!gitApi.has(found.sha)) {
148 decision.deploy = true;
149 decision.reason = `runs ${found.sha.slice(0, 12)}, which this checkout does not have (fetch full history)`;
150 decision.image = Boolean(unit.image);
151 return decision;
152 }
153 const files = relevant(unit, found.sha, changedSince(found.sha));
154 const hit = touches(unit, files);
155 decision.files = hit ? files.filter((file) => touches(unit, [file])) : [];
156 decision.image = touchesImage(unit, files);
157 if (hit) {
158 decision.deploy = true;
159 decision.reason = `${decision.files.length} changed file${decision.files.length === 1 ? "" : "s"} (${hit.file}${hit.via === "its own folder" ? "" : ` via ${hit.via}`})`;
160 } else {
161 decision.deploy = force;
162 decision.reason = force ? "forced; nothing it is built from changed" : "nothing it is built from changed";
163 }
164 return decision;
165 });
166}
167
168const firstLine = (text) => String(text).trim().split("\n").find((line) => /error|\[ERROR\]|X /i.test(line)) ?? String(text).trim().split("\n")[0];
169
170/**
171 * The plan as data, for `plan --json` and the workflow: the migrations to
172 * apply, and each stage's units split into the jobs that build them.
173 */
174export function planJson(stack, decisions, migrations, head) {
175 const deploying = decisions.filter((d) => d.deploy).map((d) => d.unit);
176 const stages = {};
177 for (const stage of codeStages(stack)) {
178 const units = deploying.filter((u) => u.stage === stage);
179 stages[stage] = { units: units.map((u) => u.id), jobs: buildGroups(units, decisions.filter((d) => d.deploy && d.image).map((d) => d.unit.id)) };
180 }
181 return {
182 commit: head,
183 migrations: Object.entries(migrations)
184 .filter(([, m]) => m.pending?.length)
185 .map(([id, m]) => ({ unit: id, database: stack.units.find((u) => u.id === id).d1.database, pending: m.pending })),
186 migration_errors: Object.entries(migrations)
187 .filter(([, m]) => m.error)
188 .map(([id, m]) => ({ unit: id, error: m.error })),
189 stages,
190 units: decisions.map((d) => ({
191 unit: d.unit.id,
192 stage: d.unit.stage,
193 deploy: d.deploy,
194 reason: d.reason,
195 live_commit: d.since,
196 image: d.image,
197 })),
198 stage_order: byStage(stack, deploying).map((g) => g.stage),
199 };
200}
201
202/** A plain table. */
203export function table(rows, headers) {
204 const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => String(r[i] ?? "").length)));
205 const line = (cells) => cells.map((c, i) => String(c ?? "").padEnd(widths[i])).join(" ").trimEnd();
206 return [line(headers), line(widths.map((w) => "-".repeat(w))), ...rows.map(line)].join("\n");
207}
208
209/** Runs `task` over `items`, at most `limit` at once, in order of start. */
210export async function pool(items, limit, task) {
211 const results = new Array(items.length);
212 let next = 0;
213 const workers = Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, async () => {
214 while (next < items.length) {
215 const index = next++;
216 results[index] = await task(items[index], index);
217 }
218 });
219 await Promise.all(workers);
220 return results;
221}

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