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
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.

The workspace cost alert compares only days that carry their value, not the days before it was kept1// 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";
The workspace cost alert compares only days that carry their value, not the days before it was kept9
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. */
The workspace cost alert compares only days that carry their value, not the days before it was kept41 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 },
The workspace cost alert compares only days that carry their value, not the days before it was kept52 /** 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 }) {
The workspace cost alert compares only days that carry their value, not the days before it was kept77 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 };
The workspace cost alert compares only days that carry their value, not the days before it was kept113 const relevant = (unit, sha, files) =>
114 files.filter((file) => {
Merge remote-tracking branch 'origin/main' into workspace-chat115 if (onlyForTests(unit, file)) return false;
The workspace cost alert compares only days that carry their value, not the days before it was kept116 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 };
The workspace cost alert compares only days that carry their value, not the days before it was kept125 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 }
The workspace cost alert compares only days that carry their value, not the days before it was kept147 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.