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

Billing's usage total is labeled at price, and Usage says what part of it was paid for1#!/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.
Billing's usage total is labeled at price, and Usage says what part of it was paid for5//
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)
Billing's usage total is labeled at price, and Usage says what part of it was paid for16//
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";
Billing's usage total is labeled at price, and Usage says what part of it was paid for29import { 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,
Billing's usage total is labeled at price, and Usage says what part of it was paid for33 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,
Billing's usage total is labeled at price, and Usage says what part of it was paid for35 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,
Billing's usage total is labeled at price, and Usage says what part of it was paid for42 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,
Billing's usage total is labeled at price, and Usage says what part of it was paid for46} 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,
Billing's usage total is labeled at price, and Usage says what part of it was paid for55 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,
Billing's usage total is labeled at price, and Usage says what part of it was paid for59 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";
A deploy that changes the runner's image or its container settings rolls every idle sandbox over at once (--containers-rollout=immediate), where Cloudflare's default rollout went in steps over wall-clock time and, with deploys often half an hour apart, never reached every sandbox: jobs kept starting on old images and old machine sizes. A change to the container settings alone, such as the instance type, now rolls out too; before, an unchanged image meant --containers-rollout none and the new settings were never applied. Busy sandboxes still get rollout_active_grace_period to finish.69import { 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";
Billing's usage total is labeled at price, and Usage says what part of it was paid for71
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]";
Billing's usage total is labeled at price, and Usage says what part of it was paid for73
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;
Billing's usage total is labeled at price, and Usage says what part of it was paid for87 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 }
Billing's usage total is labeled at price, and Usage says what part of it was paid for140 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
Billing's usage total is labeled at price, and Usage says what part of it was paid for160/** 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());
Billing's usage total is labeled at price, and Usage says what part of it was paid for175 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 });
Billing's usage total is labeled at price, and Usage says what part of it was paid for192 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 }.
Billing's usage total is labeled at price, and Usage says what part of it was paid for260 */
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.");
Billing's usage total is labeled at price, and Usage says what part of it was paid for270 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` };
Billing's usage total is labeled at price, and Usage says what part of it was paid for278 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.`);
Billing's usage total is labeled at price, and Usage says what part of it was paid for288 }
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)}` };
Billing's usage total is labeled at price, and Usage says what part of it was paid for314}
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));
A deploy that changes the runner's image or its container settings rolls every idle sandbox over at once (--containers-rollout=immediate), where Cloudflare's default rollout went in steps over wall-clock time and, with deploys often half an hour apart, never reached every sandbox: jobs kept starting on old images and old machine sizes. A change to the container settings alone, such as the instance type, now rolls out too; before, an unchanged image meant --containers-rollout none and the new settings were never applied. Busy sandboxes still get rollout_active_grace_period to finish.364 args.push("--containers-rollout", containersRollout(unit, decision, { rebuildImage, rebuildBase }));
Billing's usage total is labeled at price, and Usage says what part of it was paid for365 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)}`);
Billing's usage total is labeled at price, and Usage says what part of it was paid for377 result.version = dryRun ? "(dry run)" : versionFrom(deployed.out);
378 result.ok = true;
A deploy of a unit with containers says, for each of its container applications, what Cloudflare runs after it: the instance type, vCPU, memory and disk, the version, how many instances, and whether a rollout is still going, from wrangler containers list. A deploy log then shows whether new container settings took; reading them never fails the deploy.379 if (unit.image && !dryRun) await reportContainers(unit, cwd, out);
Billing's usage total is labeled at price, and Usage says what part of it was paid for380 } 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
A deploy of a unit with containers says, for each of its container applications, what Cloudflare runs after it: the instance type, vCPU, memory and disk, the version, how many instances, and whether a rollout is still going, from wrangler containers list. A deploy log then shows whether new container settings took; reading them never fails the deploy.391/**
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) {
When wrangler containers list does not answer in JSON, the deploy log shows what it said instead.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("
413").filter((l) => l.trim()).slice(0, 40)) out.line(` ${line}`);
A deploy of a unit with containers says, for each of its container applications, what Cloudflare runs after it: the instance type, vCPU, memory and disk, the version, how many instances, and whether a rollout is still going, from wrangler containers list. A deploy log then shows whether new container settings took; reading them never fails the deploy.414 }
415}
416
Billing's usage total is labeled at price, and Usage says what part of it was paid for417async function migrate(stack, units, migrations, opts) {
418 const due = units.filter((u) => migrations[u.id]?.pending?.length);
419 const broken = units.filter((u) => migrations[u.id]?.error);
420 if (broken.length) {
421 throw new Error(`Could not read pending migrations for ${broken.map((u) => u.id).join(", ")}; nothing was deployed.`);
422 }
423 if (!due.length) return [];
424 log(`== migrations: ${due.map((u) => `${u.d1.database} (${migrations[u.id].pending.length})`).join(", ")}`);
425 const results = await pool(due, opts.concurrency, async (unit) => {
426 const started = Date.now();
427 const out = unitLogger(`${unit.id}-migrations`);
428 const applied = await applyMigrations(unit, out.line);
429 out.save();
430 return {
431 unit: `${unit.id} (D1 ${unit.d1.database})`,
432 stage: "migrations",
433 ok: applied.code === 0,
434 version: `${migrations[unit.id].pending.length} applied`,
435 ms: Date.now() - started,
436 note: applied.code === 0 ? "" : `${lastLines(applied.out)}\n full log: ${out.file}`,
437 };
438 });
439 return results;
440}
441
442function summary(results) {
443 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]]);
444 console.log(`\n${table(rows, ["unit", "stage", "result", "version", "time", "note"])}`);
445 for (const r of results.filter((r) => !r.ok && !r.skipped)) console.log(`\n${r.unit}: ${r.note}`);
446}
447
448async function deploy(stack, opts, { dryRun = false } = {}) {
449 const started = Date.now();
450 // A build reads nothing from Cloudflare: it builds what it is given.
451 const p = dryRun ? buildPlan(stack, opts) : await plan(stack, opts);
452 if (!dryRun) printPlan(stack, p);
453 const deploying = p.decisions.filter((d) => d.deploy);
454 const touched = deploying.map((d) => d.unit);
455 const head = p.head;
456
457 // A deploy names the commit it came from, so a dirty tree would be
458 // recorded as something it is not.
459 const dirtyFiles = git.dirty();
460 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)));
461 if (dirty && !dryRun && !opts.allowDirty) {
462 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).");
463 }
464
465 const results = [];
466 if (!dryRun && !opts.noMigrations) {
467 const migrated = await migrate(stack, p.units, p.migrations, opts);
468 results.push(...migrated);
469 if (migrated.some((r) => !r.ok)) {
470 summary(results);
471 return false;
472 }
473 }
474 if (!touched.length) {
475 if (results.length) summary(results);
476 return true;
477 }
478
479 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.480 const docker = once(dockerAvailable);
Billing's usage total is labeled at price, and Usage says what part of it was paid for481 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 page482 // A deploy run by hand shows on the repository's Deployments page; a
483 // dirty tree is not a commit anyone can look at, so it is left out.
484 const settle = dryRun || dirty ? null : await reportDeployment({ head, subject: context.subject, units: touched.map((u) => u.id), log });
Billing's usage total is labeled at price, and Usage says what part of it was paid for485
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders486 // status.g1t.sh hears the deploy start and finish (STATUS_DEPLOY_TOKEN;
487 // nothing without it), so the restarts it causes are not drafted as
488 // incidents. Never in a dry run, and it never fails a deploy.
Billing's usage total is labeled at price, and Usage says what part of it was paid for489 let failed = false;
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders490 await withDeployWindow(
491 async () => {
492 for (const { stage, units } of byStage(stack, touched)) {
493 if (failed) {
494 for (const unit of units) results.push({ unit: unit.id, stage, ok: false, skipped: true, ms: 0, note: "an earlier stage failed" });
495 continue;
496 }
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.497 // A Worker that does not exist yet goes before those bound to it.
498 for (const wave of waves(units, (unit) => Boolean(deploying.find((d) => d.unit === unit)?.missing))) {
499 if (failed) {
500 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" });
501 continue;
502 }
503 log(`== ${stage}: ${wave.map((u) => u.id).join(", ")}`);
504 const shipped = await pool(wave, opts.concurrency, (unit) => ship(unit, deploying.find((d) => d.unit === unit), context));
505 results.push(...shipped);
506 failed = shipped.some((r) => !r.ok);
507 }
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders508 }
509 },
510 { id: head ?? null, dryRun, log },
511 );
Billing's usage total is labeled at price, and Usage says what part of it was paid for512 summary(results);
Deploying: a deploy by hand shows on the repository's Deployments page513 await settle?.(!failed);
Billing's usage total is labeled at price, and Usage says what part of it was paid for514 console.log(`\n${failed ? "Failed" : dryRun ? "Built" : "Deployed"} in ${seconds(Date.now() - started)}.`);
515 return !failed;
516}
517
518async function doctor(stack, opts) {
519 const units = selected(stack, opts);
520 const rows = await pool(units, 8, async (unit) => {
521 if (!unit.secrets.length) return [unit.id, "", "", ""];
522 const found = await wrangler(["secret", "list", "--name", unit.worker, "--format", "json"], { cwd: join(ROOT, unit.path) });
523 if (found.code !== 0) return [unit.id, unit.secrets.join(" "), "?", "could not read"];
524 const have = new Set(jsonFrom(found.out).map((s) => s.name));
525 const missing = unit.secrets.filter((name) => !have.has(name));
526 return [unit.id, unit.secrets.join(" "), missing.join(" "), missing.length ? "missing" : "ok"];
527 });
528 console.log(table(rows, ["unit", "secrets", "missing", "result"]));
529 return rows.every((r) => r[3] !== "missing");
530}
531
532async function install(stack, opts) {
533 const units = selected(stack, opts);
534 const args = npmCiArgs(units, npmWorkspace());
535 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.536 // Through npm-ci.mjs, which runs it again when a connection drops.
537 const done = await exec(`node scripts/deploy/npm-ci.mjs ${args.slice(1).join(" ")}`, [], { cwd: ROOT, shell: true, onLine: (line) => log(line) });
Billing's usage total is labeled at price, and Usage says what part of it was paid for538 return done.code === 0;
539}
540
541function manifest(stack, opts) {
542 const found = problems(stack, findWranglerConfigs());
543 if (opts.check) {
544 for (const problem of found) console.log(`- ${problem}`);
545 console.log(found.length ? `\n${found.length} problems in deploy/stack.jsonc.` : "deploy/stack.jsonc is consistent with every wrangler.jsonc.");
546 return !found.length;
547 }
548 const out = stack.units.map(({ config, ...unit }) => ({
549 ...unit,
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails550 queues: {
551 produces: (config?.queues?.producers ?? []).map((q) => q.queue),
552 consumes: (config?.queues?.consumers ?? []).map((q) => q.queue),
553 dead_letter: [...new Set((config?.queues?.consumers ?? []).map((q) => q.dead_letter_queue).filter(Boolean))],
554 },
Billing's usage total is labeled at price, and Usage says what part of it was paid for555 kv: (config?.kv_namespaces ?? []).map((kv) => stack.resources.kv?.[kv.id] ?? kv.id),
556 r2: (config?.r2_buckets ?? []).map((b) => b.bucket_name),
557 vectorize: (config?.vectorize ?? []).map((v) => v.index_name),
558 dispatch_namespaces: (config?.dispatch_namespaces ?? []).map((d) => d.namespace),
559 routes: (config?.routes ?? []).map((r) => r.pattern),
560 }));
561 if (opts.json) console.log(JSON.stringify({ stages: stack.stages, units: out }, null, 2));
562 else
563 console.log(
564 table(
565 out.map((u) => [u.id, u.stage, u.kind, u.worker, u.d1?.database ?? "", u.dependsOn.join(" ")]),
566 ["unit", "stage", "kind", "worker", "d1", "built from (besides its folder)"],
567 ),
568 );
569 return true;
570}
571
572/** The unit with a Containers image (the runner), or the one named. */
573function imageUnit(stack, opts) {
574 const units = (opts.only.length ? pick(stack, opts.only) : stack.units).filter((u) => u.image?.base);
575 if (units.length !== 1) throw new Error("Name the unit with a Containers image: --only runner");
576 return units[0];
577}
578
579async function main() {
580 const opts = parseArgs(process.argv.slice(2));
581 const stack = resolvedStack();
582 switch (opts.command) {
583 case "plan": {
584 const p = await plan(stack, opts);
585 const data = planJson(stack, p.decisions, p.migrations, p.head);
586 if (opts.out) writeFileSync(opts.out, `${JSON.stringify(data)}\n`);
587 if (opts.githubOutput) writeGithubOutputs(data, p);
588 if (opts.json) console.log(JSON.stringify(data, null, 2));
589 else if (!opts.githubOutput || process.env.GITHUB_OUTPUT) printPlan(stack, p);
590 return true;
591 }
592 case "deploy":
593 return deploy(stack, opts);
594 case "build":
595 return deploy(stack, { ...opts, force: true }, { dryRun: true });
596 case "migrate": {
597 const units = selected(stack, opts);
598 const { migrations } = await survey(units.filter((u) => u.d1), { ...opts, noMigrations: false, noLive: true });
599 const results = await migrate(stack, units, migrations, opts);
600 if (results.length) summary(results);
601 else console.log("No migrations to apply.");
602 return results.every((r) => r.ok);
603 }
604 case "manifest":
605 return manifest(stack, opts);
606 case "doctor":
607 return doctor(stack, opts);
608 case "install":
609 return install(stack, opts);
610 case "build-base": {
611 const unit = imageUnit(stack, opts);
612 if (!(await dockerAvailable())) throw new Error("build-base needs Docker.");
613 const lock = await newBase(unit, { push: !opts.noPush, noCache: opts.noCache });
614 console.log(JSON.stringify(lock, null, 2));
615 if (lock.pushed) console.log(`\nCommit ${unit.image.base.lock}: the next deploy builds the runner's image on this base.`);
616 return true;
617 }
618 case "image": {
619 const unit = imageUnit(stack, opts);
620 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.621 const docker = once(dockerAvailable);
Billing's usage total is labeled at price, and Usage says what part of it was paid for622 const image = await runnerImage(unit, { dryRun: Boolean(opts.noPush), docker, rebuildImage: opts.rebuildImage, rebuildBase: opts.rebuildBase }, out);
623 out.save();
624 console.log(`${image.ref}\n${image.note}`);
625 return true;
626 }
627 default:
628 console.error(USAGE);
629 return false;
630 }
631}
632
633main().then(
634 (ok) => process.exit(ok ? 0 : 1),
635 (error) => {
636 console.error(`\n${error.message ?? error}`);
637 process.exit(1);
638 },
639);

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