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

Projects: when each was last pushed, and how active it is lately1#!/usr/bin/env node
2// Deploys g1t to Cloudflare from deploy/stack.jsonc: only what changed since
3// each Worker's live commit, migrations first, then stage by stage.
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.4// docs.g1t.sh/guides/deploy-to-cloudflare/ is the guide.
Projects: when each was last pushed, and how active it is lately5//
6// node scripts/deploy.mjs plan what would deploy, and why (read-only)
7// node scripts/deploy.mjs deploy migrations, then every changed unit
8// node scripts/deploy.mjs deploy --only web,api just these (if changed; --force: anyway)
9// node scripts/deploy.mjs build --only events build as a deploy would, upload nothing
10// node scripts/deploy.mjs migrate pending D1 migrations only
11// node scripts/deploy.mjs manifest [--check] the resolved manifest, or its problems
12// node scripts/deploy.mjs doctor which units lack their secrets
13// node scripts/deploy.mjs install --only a,b npm ci of just what those units need (CI)
14// node scripts/deploy.mjs build-base build and push the runner's base image (Docker)
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.15// node scripts/deploy.mjs image make the runner's image for this checkout in the registry (--no-push: build it with Docker)
Projects: when each was last pushed, and how active it is lately16//
17// Flags: --all (every unit, changed or not), --only a,b, --skip a,b,
18// --force, --concurrency N (default 4), --stage core (one stage),
19// --json (plan), --out FILE (plan), --no-migrations, --allow-dirty,
20// --rebuild-image, --rebuild-base, --since REV (Workers with no recorded
21// commit are taken to run REV); for build-base and image, --no-push, and
22// for build-base, --no-cache.
23
24import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
25import { tmpdir } from "node:os";
26import { join } from "node:path";
27
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.28import { BINARY, OUT_DIR } from "./build-runner.mjs";
Projects: when each was last pushed, and how active it is lately29import { ensureWorkerBuild } from "./build-rust-worker.mjs";
30import {
31 annotation,
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check32 apiAuth,
Projects: when each was last pushed, and how active it is lately33 applyMigrations,
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.34 DEPLOY_RETRY_WAITS_MS,
Projects: when each was last pushed, and how active it is lately35 dockerAvailable,
36 exec,
37 jsonFrom,
38 lastLines,
39 pendingMigrations,
40 readLive,
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.41 retryableDeploy,
Projects: when each was last pushed, and how active it is lately42 versionFrom,
43 wrangler,
44 wranglerEnv,
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.45 wranglerError,
Projects: when each was last pushed, and how active it is lately46} from "./deploy/cloudflare.mjs";
47import {
48 baseInputs,
49 baseState,
50 baseTag,
51 baseVersions,
52 buildBase,
53 buildRunnerImage,
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.54 diskReport,
Projects: when each was last pushed, and how active it is lately55 dockerHas,
56 dockerLogin,
57 imageSize,
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.58 makeRunnerImage,
Projects: when each was last pushed, and how active it is lately59 pushImage,
60 readBaseLock,
61 registryHas,
62 removeDeployConfig,
63 runnerRef,
64 writeBaseLock,
65 writeDeployConfig,
66} from "./deploy/image.mjs";
67import { decide, git, planJson, pool, table } from "./deploy/plan.mjs";
Deploying: a deploy by hand shows on the repository's Deployments page68import { reportDeployment } from "./deploy/report.mjs";
Merge branch 'main' into ask-integration69import { ROOT, byStage, codeStages, containersRollout, findWranglerConfigs, npmCiArgs, npmWorkspace, pick, problems, resolvedStack, waves } from "./deploy/stack.mjs";
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders70import { withDeployWindow } from "./deploy/status-window.mjs";
Projects: when each was last pushed, and how active it is lately71
Deploying: never roll production back by accident72const USAGE = "usage: node scripts/deploy.mjs plan|deploy|build|migrate|manifest|doctor|install|build-base|image [--all] [--only a,b] [--skip a,b] [--force] [--rollback] [--concurrency N] [--stage S] [--json]";
Projects: when each was last pushed, and how active it is lately73
74function parseArgs(argv) {
75 const opts = { command: argv[0], only: [], skip: [], concurrency: 4, force: false, all: false, json: false };
76 for (let i = 1; i < argv.length; i++) {
77 const arg = argv[i];
78 const [flag, inline] = arg.split(/=(.*)/s);
79 const value = () => inline ?? argv[++i];
80 if (flag === "--only") opts.only.push(value());
81 else if (flag === "--skip") opts.skip.push(value());
82 else if (flag === "--concurrency") opts.concurrency = Number(value());
83 else if (flag === "--stage") opts.stage = value();
84 else if (flag === "--all") opts.all = true;
85 else if (flag === "--force") opts.force = true;
Deploying: never roll production back by accident86 else if (flag === "--rollback") opts.rollback = true;
Projects: when each was last pushed, and how active it is lately87 else if (flag === "--json") opts.json = true;
88 else if (flag === "--check") opts.check = true;
89 else if (flag === "--no-migrations") opts.noMigrations = true;
90 else if (flag === "--allow-dirty") opts.allowDirty = true;
91 else if (flag === "--rebuild-image") opts.rebuildImage = true;
92 else if (flag === "--rebuild-base") opts.rebuildBase = true;
93 else if (flag === "--no-push") opts.noPush = true;
94 else if (flag === "--no-cache") opts.noCache = true;
95 else if (flag === "--out") opts.out = value();
96 else if (flag === "--since") opts.since = value();
97 else if (flag === "--github-output") opts.githubOutput = true;
98 else throw new Error(`unknown flag ${arg}\n${USAGE}`);
99 }
100 if (!Number.isInteger(opts.concurrency) || opts.concurrency < 1) throw new Error("--concurrency is a whole number, 1 or more");
101 return opts;
102}
103
104/** The units a command works on: --only (or all), less --skip, in --stage. */
105function selected(stack, opts) {
106 let units = opts.only.length ? pick(stack, opts.only) : [...stack.units];
107 const skipped = pick(stack, opts.skip);
108 units = units.filter((u) => !skipped.includes(u));
109 if (opts.stage) {
110 if (!codeStages(stack).includes(opts.stage)) throw new Error(`--stage is one of ${codeStages(stack).join(", ")}`);
111 units = units.filter((u) => u.stage === opts.stage);
112 }
113 // Manifest order, whatever order they were named in.
114 return stack.units.filter((u) => units.includes(u));
115}
116
117const log = (...args) => console.error(...args);
118
119/**
120 * The plan for a workflow (.g1t/workflows/deploy.yml): job outputs in
121 * $GITHUB_OUTPUT, and the plan as a table in $GITHUB_STEP_SUMMARY.
122 */
123function writeGithubOutputs(data, p) {
124 const lines = [
125 `commit=${data.commit}`,
126 `migrate=${data.migrations.length > 0}`,
127 `migrate_units=${data.migrations.map((m) => m.unit).join(",")}`,
128 `deploying=${data.units.filter((u) => u.deploy).map((u) => u.unit).join(",")}`,
129 ];
130 for (const [stage, { jobs }] of Object.entries(data.stages)) {
131 // A matrix needs one entry; an empty stage's job is skipped by its `if`.
132 const include = jobs.length ? jobs : [{ group: "none", units: "" }];
133 lines.push(`has_${stage}=${jobs.length > 0}`, `${stage}=${JSON.stringify({ include })}`);
134 }
Deploying: a Plan that cannot read migrations says why135 if (data.migration_errors.length) {
136 // The units, then why for the first: one cause (a token, say) is usually all of them.
137 throw new Error(`Could not read pending migrations: ${data.migration_errors.map((e) => e.unit).join(", ")}
138${data.migration_errors[0].error}`);
139 }
Projects: when each was last pushed, and how active it is lately140 if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join("\n")}\n`);
141 else console.log(lines.join("\n"));
142 if (process.env.GITHUB_STEP_SUMMARY) {
143 const rows = p.decisions.map((d) => `| ${d.unit.id} | ${d.unit.stage} | ${d.deploy ? "deploy" : ""} | ${d.since ? d.since.slice(0, 12) : "?"} | ${d.reason.replaceAll("|", "\\|")} |`);
144 const pending = data.migrations.map((m) => `- ${m.database}: ${m.pending.join(", ")}`);
145 appendFileSync(
146 process.env.GITHUB_STEP_SUMMARY,
147 [`## Deploy plan for ${data.commit.slice(0, 12)}`, "", "| unit | stage | action | live | why |", "| --- | --- | --- | --- | --- |", ...rows, "", pending.length ? "### Pending migrations" : "", ...pending, ""].join("\n"),
148 );
149 }
150}
151
152const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
153
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.154/** `fn`, called the first time it is wanted and then remembered. */
155const once = (fn) => {
156 let answer = null;
157 return () => (answer ??= fn());
158};
159
Projects: when each was last pushed, and how active it is lately160/** Reads what each unit runs and what its database is waiting for. */
161async function survey(units, opts) {
162 const live = {};
163 const migrations = {};
164 const tasks = [
165 ...(opts.noLive ? [] : units).map((unit) => async () => {
166 live[unit.id] = await readLive(unit);
167 }),
168 ...(opts.noMigrations ? [] : units.filter((u) => u.d1)).map((unit) => async () => {
169 migrations[unit.id] = await pendingMigrations(unit);
170 }),
171 ];
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check172 // Through the API every read starts at once (the requests themselves are
173 // held to API_CONCURRENCY); through Wrangler, a few processes at a time.
174 await pool(tasks, apiAuth() ? tasks.length : Math.max(8, opts.concurrency * 2), (task) => task());
Projects: when each was last pushed, and how active it is lately175 return { live, migrations };
176}
177
178async function plan(stack, opts) {
179 const units = selected(stack, opts);
180 const head = git.head();
181 const { live, migrations } = await survey(units, opts);
182 // --since: what a Worker with no recorded commit is taken to run (once,
183 // to adopt Workers deployed before this tool), for example --since HEAD~3.
184 if (opts.since) {
185 const since = git.resolve(opts.since);
186 for (const unit of units) {
187 const found = live[unit.id];
188 if (found && !found.sha && !found.missing && !found.error) live[unit.id] = { ...found, sha: since, assumed: true };
189 }
190 }
Deploying: never roll production back by accident191 const decisions = decide(units, { live, head, force: opts.force || opts.all, rollback: opts.rollback });
Projects: when each was last pushed, and how active it is lately192 return { head, units, live, migrations, decisions };
193}
194
195function buildPlan(stack, opts) {
196 const units = selected(stack, opts);
197 return {
198 head: git.head(),
199 units,
200 live: {},
201 migrations: {},
202 decisions: units.map((unit) => ({ unit, deploy: true, reason: "build", since: null, files: [], image: false })),
203 };
204}
205
206function printPlan(stack, { head, decisions, migrations, live }) {
207 console.log(`Deploying ${head.slice(0, 12)} (${git.subject()})\n`);
208 const rows = decisions.map((d) => [
209 d.unit.id,
210 d.unit.stage,
211 d.deploy ? "deploy" : "-",
212 d.since ? d.since.slice(0, 12) + (live[d.unit.id]?.assumed ? "*" : "") : "?",
213 d.unit.d1 ? (migrations[d.unit.id]?.error ? "error" : String(migrations[d.unit.id]?.pending?.length ?? "-")) : "",
214 d.reason + (d.image ? "; image rebuilds" : ""),
215 ]);
216 console.log(table(rows, ["unit", "stage", "action", "live", "migrations", "why"]));
217 if (decisions.some((d) => live[d.unit.id]?.assumed)) console.log("* taken from --since: no commit was recorded for it");
218 const pending = Object.entries(migrations).filter(([, m]) => m.pending?.length);
219 if (pending.length) {
220 console.log("\nPending migrations:");
221 for (const [id, m] of pending) console.log(` ${stack.units.find((u) => u.id === id).d1.database}: ${m.pending.join(", ")}`);
222 }
223 for (const [id, m] of Object.entries(migrations).filter(([, m]) => m.error)) {
224 console.log(`\nCould not list ${id}'s migrations:\n${m.error}`);
225 }
226 const deploying = decisions.filter((d) => d.deploy).map((d) => d.unit);
227 console.log(
228 deploying.length
229 ? `\n${deploying.length} to deploy: ${byStage(stack, deploying).map((g) => `${g.stage} (${g.units.map((u) => u.id).join(", ")})`).join(" -> ")}`
230 : "\nNothing to deploy.",
231 );
232}
233
234/** Output of one unit, prefixed, to the terminal and a log file. */
235function unitLogger(id) {
236 const dir = join(tmpdir(), "g1t-deploy");
237 mkdirSync(dir, { recursive: true });
238 const file = join(dir, `${id}.log`);
239 const lines = [];
240 return {
241 file,
242 line: (text) => {
243 lines.push(text);
244 if (text.trim()) log(`[${id}] ${text}`);
245 },
246 save: () => writeFileSync(file, `${lines.join("\n")}\n`),
247 };
248}
249
250/**
251 * The runner's image for this checkout, by registry reference: already in
252 * the registry (built by an earlier deploy, `deploy.mjs image`, or on
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.253 * another machine), or made here. Pushing, it is made in the registry
254 * (scripts/deploy/registry.mjs: the base's layers and one with the
255 * binary, nothing pulled), which needs no Docker and no room for the
256 * base; if that fails and Docker answers, it is built and pushed with
257 * Docker. A dry run builds it with Docker and pushes nothing. `docker` is
258 * asked only when needed: asking starts a job's Docker Engine. Returns
259 * { ref, note }.
Projects: when each was last pushed, and how active it is lately260 */
261async function runnerImage(unit, { dryRun, docker, rebuildImage, rebuildBase }, out) {
262 let base = baseState(unit);
263 if (!base.current || rebuildBase) {
264 if (!rebuildBase) {
265 throw new Error(
266 `${unit.image.base.context} has changed since ${unit.image.base.lock} was written${base.lock ? "" : " (the base has never been built)"}. Build and push the base, and commit ${unit.image.base.lock}: node scripts/deploy.mjs build-base`,
267 );
268 }
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.269 if (!(await docker())) throw new Error("--rebuild-base needs Docker.");
Projects: when each was last pushed, and how active it is lately270 await newBase(unit, { push: !dryRun, onLine: out.line });
271 base = baseState(unit);
272 }
273 if (!base.pushed && !dryRun) throw new Error(`${unit.image.base.lock} records a base that was never pushed: node scripts/deploy.mjs build-base`);
274 const ref = runnerRef(unit);
275 const tag = ref.split(":").pop();
276 if (!rebuildImage) {
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.277 if (dryRun && (await docker()) && (await dockerHas(ref))) return { ref, note: `image ${tag} already built here` };
Projects: when each was last pushed, and how active it is lately278 if (!dryRun) {
279 try {
280 if (await registryHas(ref)) return { ref, note: `image ${tag} already in the registry` };
281 } catch (error) {
282 out.line(`could not ask the registry for ${tag}: ${error.message ?? error}`);
283 }
284 }
285 }
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.286 if (dryRun && !(await docker())) {
287 throw new Error(`its image ${tag} is not built here, and Docker does not answer here: a dry run builds it with Docker.`);
Projects: when each was last pushed, and how active it is lately288 }
289 const started = Date.now();
290 const binary = await exec(process.execPath, [join(ROOT, "scripts/build-runner.mjs")], { onLine: out.line });
291 if (binary.code !== 0) throw new Error(`the runner binary did not build:\n${lastLines(binary.out)}`);
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.292 if (!dryRun) {
293 try {
294 await makeRunnerImage({ ref, base: base.ref, binary: BINARY, onLine: out.line });
295 return { ref, note: `image ${tag} made in the registry in ${seconds(Date.now() - started)}` };
296 } catch (error) {
297 if (!(await docker())) {
298 throw new Error(`its image ${tag} could not be made in the registry (${error.message ?? error}), and Docker does not answer here to build it instead.`);
299 }
300 out.line(`could not make ${tag} in the registry (${error.message ?? error}); building it with Docker instead`);
301 }
302 }
303 // With Docker: the base is pulled and unpacked on this disk, so what the
304 // disk held is logged before and after, and when it fails.
305 await diskReport(out.line, "before the build");
306 try {
307 if (!dryRun) await dockerLogin({ push: true });
308 await buildRunnerImage(unit, { ref, base: base.ref, binaryDir: OUT_DIR, onLine: out.line });
309 if (!dryRun) await pushImage(ref, { onLine: out.line });
310 } finally {
311 await diskReport(out.line, "after the build");
312 }
313 return { ref, note: `image ${tag} ${dryRun ? "built" : "built and pushed"} with Docker in ${seconds(Date.now() - started)}` };
Projects: when each was last pushed, and how active it is lately314}
315
316/** Builds the base (and pushes it unless `push` is false), and writes its lock. */
317async function newBase(unit, { push = true, noCache = false, onLine = log } = {}) {
318 const started = Date.now();
319 const inputs = baseInputs(unit);
320 const tag = baseTag(inputs);
321 const previous = readBaseLock(unit);
322 // Logged in, the base it replaces is pulled as cache.
323 if (push || previous?.pushed) {
324 try {
325 await dockerLogin({ push });
326 } catch (error) {
327 if (push) throw error;
328 onLine(`not logged in to the registry, so no cache from it: ${error.message ?? error}`);
329 }
330 }
331 const ref = await buildBase(unit, { tag, previous: previous?.pushed ? previous.image : null, onLine, noCache });
332 const built = Date.now();
333 const [size, versions] = await Promise.all([imageSize(ref), baseVersions(ref)]);
334 const digest = push ? await pushImage(ref, { onLine }) : null;
335 const lock = { image: ref, digest, pushed: push, inputs, built_at: new Date().toISOString(), size_bytes: size, versions };
336 writeBaseLock(unit, lock);
337 onLine(`base ${tag}: built in ${seconds(built - started)}${push ? `, pushed in ${seconds(Date.now() - built)}` : ""}, ${(size / 1e9).toFixed(2)} GB unpacked`);
338 return lock;
339}
340
341/** Builds (and unless `dryRun`, deploys) one unit. */
342async function ship(unit, decision, { head, subject, dirty, dryRun, docker, rebuildImage, rebuildBase }) {
343 const started = Date.now();
344 const out = unitLogger(unit.id);
345 const cwd = join(ROOT, unit.path);
346 const result = { unit: unit.id, stage: unit.stage, ok: false, version: null, ms: 0, note: "" };
347 try {
348 if (unit.kind === "react-router" || unit.kind === "astro") {
349 // One command string: Node warns about arguments passed beside shell: true.
350 const built = await exec("npm run build", [], { cwd, onLine: out.line, shell: true, env: wranglerEnv() });
351 if (built.code !== 0) throw new Error(`npm run build failed:\n${lastLines(built.out)}`);
352 }
353 const args = ["deploy"];
354 if (dryRun) {
355 args.push("--dry-run", "--outdir", join(tmpdir(), "g1t-deploy", "dist", unit.id));
356 } else {
357 const { message, tag } = annotation(head, subject);
358 args.push("--message", dirty ? message.replace(/^g1t-deploy/, "g1t-deploy-dirty") : message, "--tag", tag);
359 }
360 if (unit.image) {
361 // Wrangler is given the image by reference, so it builds nothing.
362 const image = await runnerImage(unit, { dryRun, docker, rebuildImage, rebuildBase }, out);
363 args.push("--config", writeDeployConfig(unit, image.ref));
Merge branch 'main' into ask-integration364 args.push("--containers-rollout", containersRollout(unit, decision, { rebuildImage, rebuildBase }));
Projects: when each was last pushed, and how active it is lately365 result.note = image.note;
366 }
Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them.367 // Tried again after a wait when Cloudflare, not what is deployed, is
368 // why it failed: deploying the same version again is harmless.
369 let deployed = await wrangler(args, { cwd, onLine: out.line });
370 for (const wait of DEPLOY_RETRY_WAITS_MS) {
371 if (deployed.code === 0 || dryRun || !retryableDeploy(deployed.out)) break;
372 out.line(`wrangler deploy failed; trying again in ${wait / 1000}s:\n${wranglerError(deployed.out)}`);
373 await new Promise((resolve) => setTimeout(resolve, wait));
374 deployed = await wrangler(args, { cwd, onLine: out.line });
375 }
376 if (deployed.code !== 0) throw new Error(`wrangler deploy failed:\n${wranglerError(deployed.out)}`);
Projects: when each was last pushed, and how active it is lately377 result.version = dryRun ? "(dry run)" : versionFrom(deployed.out);
378 result.ok = true;
Merge branch 'main' into ask-integration379 if (unit.image && !dryRun) await reportContainers(unit, cwd, out);
Projects: when each was last pushed, and how active it is lately380 } catch (error) {
381 result.note = String(error.message ?? error);
382 } finally {
383 if (unit.image) removeDeployConfig(unit);
384 }
385 result.ms = Date.now() - started;
386 out.save();
387 if (!result.ok) result.note += `\n full log: ${out.file}`;
388 return result;
389}
390
Merge branch 'main' into ask-integration391/**
392 * What Cloudflare runs for a unit's container applications after a deploy:
393 * each one's instance type, size, version and rollout, from `wrangler
394 * containers list`, so a deploy log says whether new settings took. Never
395 * fails the deploy.
396 */
397async function reportContainers(unit, cwd, out) {
398 try {
399 const listed = await wrangler(["containers", "list"], { cwd });
400 const text = listed.out.slice(listed.out.indexOf("["));
401 const apps = JSON.parse(text.slice(0, text.lastIndexOf("]") + 1));
402 for (const app of apps.filter((a) => String(a.name ?? "").startsWith(`${unit.worker}-`))) {
403 const c = app.configuration ?? {};
404 const disk = c.disk?.size_mb ?? c.disk_mb ?? c.disk?.size;
405 const size = [c.instance_type, c.vcpu != null && `${c.vcpu} vCPU`, c.memory_mib != null && `${c.memory_mib} MiB`, disk != null && `${disk} MB disk`].filter(Boolean).join(", ");
406 const rollout = app.active_rollout_id ? `rollout ${app.active_rollout_id} in progress` : "no rollout in progress";
407 out.line(`container ${app.name}: ${size || JSON.stringify(c).slice(0, 200)}; version ${app.version ?? "?"}; ${app.instances ?? "?"} instances; ${rollout}`);
408 }
409 } catch (error) {
410 out.line(`could not read the container applications as JSON (${error.message ?? error}); wrangler said:`);
411 const said = await wrangler(["containers", "list"], { cwd }).catch(() => ({ out: "" }));
412 for (const line of said.out.split("\n").filter((l) => l.trim()).slice(0, 40)) out.line(` ${line}`);
413 }
414}
415
Projects: when each was last pushed, and how active it is lately416async function migrate(stack, units, migrations, opts) {
417 const due = units.filter((u) => migrations[u.id]?.pending?.length);
418 const broken = units.filter((u) => migrations[u.id]?.error);
419 if (broken.length) {
420 throw new Error(`Could not read pending migrations for ${broken.map((u) => u.id).join(", ")}; nothing was deployed.`);
421 }
422 if (!due.length) return [];
423 log(`== migrations: ${due.map((u) => `${u.d1.database} (${migrations[u.id].pending.length})`).join(", ")}`);
424 const results = await pool(due, opts.concurrency, async (unit) => {
425 const started = Date.now();
426 const out = unitLogger(`${unit.id}-migrations`);
427 const applied = await applyMigrations(unit, out.line);
428 out.save();
429 return {
430 unit: `${unit.id} (D1 ${unit.d1.database})`,
431 stage: "migrations",
432 ok: applied.code === 0,
433 version: `${migrations[unit.id].pending.length} applied`,
434 ms: Date.now() - started,
435 note: applied.code === 0 ? "" : `${lastLines(applied.out)}\n full log: ${out.file}`,
436 };
437 });
438 return results;
439}
440
441function summary(results) {
442 const rows = results.map((r) => [r.unit, r.stage, r.ok ? "ok" : r.skipped ? "not started" : "FAILED", r.version ?? "", r.ms ? seconds(r.ms) : "", r.note.split("\n")[0]]);
443 console.log(`\n${table(rows, ["unit", "stage", "result", "version", "time", "note"])}`);
444 for (const r of results.filter((r) => !r.ok && !r.skipped)) console.log(`\n${r.unit}: ${r.note}`);
445}
446
447async function deploy(stack, opts, { dryRun = false } = {}) {
448 const started = Date.now();
449 // A build reads nothing from Cloudflare: it builds what it is given.
450 const p = dryRun ? buildPlan(stack, opts) : await plan(stack, opts);
451 if (!dryRun) printPlan(stack, p);
452 const deploying = p.decisions.filter((d) => d.deploy);
453 const touched = deploying.map((d) => d.unit);
454 const head = p.head;
455
456 // A deploy names the commit it came from, so a dirty tree would be
457 // recorded as something it is not.
458 const dirtyFiles = git.dirty();
459 const dirty = deploying.some((d) => dirtyFiles.some((file) => file.startsWith(`${d.unit.path}/`) || d.unit.dependsOn.some((dir) => file.startsWith(`${dir}/`)) || d.unit.inputs.includes(file)));
460 if (dirty && !dryRun && !opts.allowDirty) {
461 throw new Error("Uncommitted changes touch what would deploy. Commit them, or pass --allow-dirty (the deploy then records no commit, and the next plan deploys it again).");
462 }
463
464 const results = [];
465 if (!dryRun && !opts.noMigrations) {
466 const migrated = await migrate(stack, p.units, p.migrations, opts);
467 results.push(...migrated);
468 if (migrated.some((r) => !r.ok)) {
469 summary(results);
470 return false;
471 }
472 }
473 if (!touched.length) {
474 if (results.length) summary(results);
475 return true;
476 }
477
478 if (touched.some((u) => u.kind === "rust-worker")) ensureWorkerBuild();
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.479 const docker = once(dockerAvailable);
Projects: when each was last pushed, and how active it is lately480 const context = { head, subject: git.subject(), dirty, dryRun, docker, rebuildImage: opts.rebuildImage, rebuildBase: opts.rebuildBase };
Deploying: a deploy by hand shows on the repository's Deployments page481 // A deploy run by hand shows on the repository's Deployments page; a
482 // dirty tree is not a commit anyone can look at, so it is left out.
483 const settle = dryRun || dirty ? null : await reportDeployment({ head, subject: context.subject, units: touched.map((u) => u.id), log });
Projects: when each was last pushed, and how active it is lately484
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders485 // status.g1t.sh hears the deploy start and finish (STATUS_DEPLOY_TOKEN;
486 // nothing without it), so the restarts it causes are not drafted as
487 // incidents. Never in a dry run, and it never fails a deploy.
Projects: when each was last pushed, and how active it is lately488 let failed = false;
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders489 await withDeployWindow(
490 async () => {
491 for (const { stage, units } of byStage(stack, touched)) {
492 if (failed) {
493 for (const unit of units) results.push({ unit: unit.id, stage, ok: false, skipped: true, ms: 0, note: "an earlier stage failed" });
494 continue;
495 }
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.496 // A Worker that does not exist yet goes before those bound to it.
497 for (const wave of waves(units, (unit) => Boolean(deploying.find((d) => d.unit === unit)?.missing))) {
498 if (failed) {
499 for (const unit of wave) results.push({ unit: unit.id, stage, ok: false, skipped: true, ms: 0, note: "a Worker it binds to failed to deploy" });
500 continue;
501 }
502 log(`== ${stage}: ${wave.map((u) => u.id).join(", ")}`);
503 const shipped = await pool(wave, opts.concurrency, (unit) => ship(unit, deploying.find((d) => d.unit === unit), context));
504 results.push(...shipped);
505 failed = shipped.some((r) => !r.ok);
506 }
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders507 }
508 },
509 { id: head ?? null, dryRun, log },
510 );
Projects: when each was last pushed, and how active it is lately511 summary(results);
Deploying: a deploy by hand shows on the repository's Deployments page512 await settle?.(!failed);
Projects: when each was last pushed, and how active it is lately513 console.log(`\n${failed ? "Failed" : dryRun ? "Built" : "Deployed"} in ${seconds(Date.now() - started)}.`);
514 return !failed;
515}
516
517async function doctor(stack, opts) {
518 const units = selected(stack, opts);
519 const rows = await pool(units, 8, async (unit) => {
520 if (!unit.secrets.length) return [unit.id, "", "", ""];
521 const found = await wrangler(["secret", "list", "--name", unit.worker, "--format", "json"], { cwd: join(ROOT, unit.path) });
522 if (found.code !== 0) return [unit.id, unit.secrets.join(" "), "?", "could not read"];
523 const have = new Set(jsonFrom(found.out).map((s) => s.name));
524 const missing = unit.secrets.filter((name) => !have.has(name));
525 return [unit.id, unit.secrets.join(" "), missing.join(" "), missing.length ? "missing" : "ok"];
526 });
527 console.log(table(rows, ["unit", "secrets", "missing", "result"]));
528 return rows.every((r) => r[3] !== "missing");
529}
530
531async function install(stack, opts) {
532 const units = selected(stack, opts);
533 const args = npmCiArgs(units, npmWorkspace());
534 log(`npm ${args.join(" ")}`);
Every install in g1t's workflows and deploy tool goes through scripts/deploy/npm-ci.mjs, which runs npm ci again when a connection drops: npm does not retry a body that breaks off midway, so an ECONNRESET or network aborted, an install that ends without saying why, or one that prints nothing for 12 minutes is tried again, up to three times; a real error, such as a lockfile out of step, still fails at once. A test keeps every workflow on it.535 // Through npm-ci.mjs, which runs it again when a connection drops.
536 const done = await exec(`node scripts/deploy/npm-ci.mjs ${args.slice(1).join(" ")}`, [], { cwd: ROOT, shell: true, onLine: (line) => log(line) });
Projects: when each was last pushed, and how active it is lately537 return done.code === 0;
538}
539
540function manifest(stack, opts) {
541 const found = problems(stack, findWranglerConfigs());
542 if (opts.check) {
543 for (const problem of found) console.log(`- ${problem}`);
544 console.log(found.length ? `\n${found.length} problems in deploy/stack.jsonc.` : "deploy/stack.jsonc is consistent with every wrangler.jsonc.");
545 return !found.length;
546 }
547 const out = stack.units.map(({ config, ...unit }) => ({
548 ...unit,
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails549 queues: {
550 produces: (config?.queues?.producers ?? []).map((q) => q.queue),
551 consumes: (config?.queues?.consumers ?? []).map((q) => q.queue),
552 dead_letter: [...new Set((config?.queues?.consumers ?? []).map((q) => q.dead_letter_queue).filter(Boolean))],
553 },
Projects: when each was last pushed, and how active it is lately554 kv: (config?.kv_namespaces ?? []).map((kv) => stack.resources.kv?.[kv.id] ?? kv.id),
555 r2: (config?.r2_buckets ?? []).map((b) => b.bucket_name),
556 vectorize: (config?.vectorize ?? []).map((v) => v.index_name),
557 dispatch_namespaces: (config?.dispatch_namespaces ?? []).map((d) => d.namespace),
558 routes: (config?.routes ?? []).map((r) => r.pattern),
559 }));
560 if (opts.json) console.log(JSON.stringify({ stages: stack.stages, units: out }, null, 2));
561 else
562 console.log(
563 table(
564 out.map((u) => [u.id, u.stage, u.kind, u.worker, u.d1?.database ?? "", u.dependsOn.join(" ")]),
565 ["unit", "stage", "kind", "worker", "d1", "built from (besides its folder)"],
566 ),
567 );
568 return true;
569}
570
571/** The unit with a Containers image (the runner), or the one named. */
572function imageUnit(stack, opts) {
573 const units = (opts.only.length ? pick(stack, opts.only) : stack.units).filter((u) => u.image?.base);
574 if (units.length !== 1) throw new Error("Name the unit with a Containers image: --only runner");
575 return units[0];
576}
577
578async function main() {
579 const opts = parseArgs(process.argv.slice(2));
580 const stack = resolvedStack();
581 switch (opts.command) {
582 case "plan": {
583 const p = await plan(stack, opts);
584 const data = planJson(stack, p.decisions, p.migrations, p.head);
585 if (opts.out) writeFileSync(opts.out, `${JSON.stringify(data)}\n`);
586 if (opts.githubOutput) writeGithubOutputs(data, p);
587 if (opts.json) console.log(JSON.stringify(data, null, 2));
588 else if (!opts.githubOutput || process.env.GITHUB_OUTPUT) printPlan(stack, p);
589 return true;
590 }
591 case "deploy":
592 return deploy(stack, opts);
593 case "build":
594 return deploy(stack, { ...opts, force: true }, { dryRun: true });
595 case "migrate": {
596 const units = selected(stack, opts);
597 const { migrations } = await survey(units.filter((u) => u.d1), { ...opts, noMigrations: false, noLive: true });
598 const results = await migrate(stack, units, migrations, opts);
599 if (results.length) summary(results);
600 else console.log("No migrations to apply.");
601 return results.every((r) => r.ok);
602 }
603 case "manifest":
604 return manifest(stack, opts);
605 case "doctor":
606 return doctor(stack, opts);
607 case "install":
608 return install(stack, opts);
609 case "build-base": {
610 const unit = imageUnit(stack, opts);
611 if (!(await dockerAvailable())) throw new Error("build-base needs Docker.");
612 const lock = await newBase(unit, { push: !opts.noPush, noCache: opts.noCache });
613 console.log(JSON.stringify(lock, null, 2));
614 if (lock.pushed) console.log(`\nCommit ${unit.image.base.lock}: the next deploy builds the runner's image on this base.`);
615 return true;
616 }
617 case "image": {
618 const unit = imageUnit(stack, opts);
619 const out = unitLogger(`${unit.id}-image`);
A deploy makes the runner's image in the registry, without Docker: it reads the base's manifest and config from Cloudflare's registry, uploads one gzipped layer holding the runner binary at /usr/local/bin/g1t-runner with mode 0755, and writes a config (the entrypoint g1t-runner, the base's command cleared, as the Dockerfile's ENTRYPOINT does) and a manifest that name the base's layers and that one, under the runner's tag, for linux/amd64 (scripts/deploy/registry.mjs, the registry's own upload calls with the credentials Wrangler gives). Deploy #256 ran out of disk because docker build pulled the 4.5 GB base into the job's own Docker Engine, compressed and unpacked, on a 20 GB sandbox whose own filesystem is that same base; now nothing of the base is downloaded and the job's Docker Engine is never started. Docker is asked for only when it is needed: a dry run (build, or image --no-push) still builds services/runner/Dockerfile with it, and so does a deploy whose registry step fails, logging df -h and docker system df before and after. The image job removes npm's downloads, which it only restores, once its install has read them, and logs the disk before and after the deploy. Tests cover the layer's tar, the config and manifest, the calls against a registry kept in memory (the base's layers are never fetched), the token flow, and that the Dockerfile says the same path, mode and entrypoint; the image was also checked against a local registry, where it runs and has the same config as a docker build of the Dockerfile.620 const docker = once(dockerAvailable);
Projects: when each was last pushed, and how active it is lately621 const image = await runnerImage(unit, { dryRun: Boolean(opts.noPush), docker, rebuildImage: opts.rebuildImage, rebuildBase: opts.rebuildBase }, out);
622 out.save();
623 console.log(`${image.ref}\n${image.note}`);
624 return true;
625 }
626 default:
627 console.error(USAGE);
628 return false;
629 }
630}
631
632main().then(
633 (ok) => process.exit(ok ? 0 : 1),
634 (error) => {
635 console.error(`\n${error.message ?? error}`);
636 process.exit(1);
637 },
638);

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