Skip to content
566 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow1#!/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.
4// docs/DEPLOYING.md is the guide.
5//
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)
Fast pages, required checks on the branch, self-hosted runners, honest incidents14// node scripts/deploy.mjs build-base build and push the runner's base image (Docker)
15// node scripts/deploy.mjs image build and push the runner's image for this checkout (Docker)
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow16//
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,
Fast pages, required checks on the branch, self-hosted runners, honest incidents20// --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.
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow23
24import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
25import { tmpdir } from "node:os";
26import { join } from "node:path";
27
Fast pages, required checks on the branch, self-hosted runners, honest incidents28import { OUT_DIR } from "./build-runner.mjs";
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow29import { 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,
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow33 applyMigrations,
34 dockerAvailable,
35 exec,
36 jsonFrom,
37 lastLines,
38 pendingMigrations,
39 readLive,
40 versionFrom,
41 wrangler,
42 wranglerEnv,
43} from "./deploy/cloudflare.mjs";
Fast pages, required checks on the branch, self-hosted runners, honest incidents44import {
45 baseInputs,
46 baseState,
47 baseTag,
48 baseVersions,
49 buildBase,
50 buildRunnerImage,
51 dockerHas,
52 dockerLogin,
53 imageSize,
54 pushImage,
55 readBaseLock,
56 registryHas,
57 removeDeployConfig,
58 runnerRef,
59 writeBaseLock,
60 writeDeployConfig,
61} from "./deploy/image.mjs";
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow62import { decide, git, planJson, pool, table } from "./deploy/plan.mjs";
Deploying: a deploy by hand shows on the repository's Deployments page63import { reportDeployment } from "./deploy/report.mjs";
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow64import { ROOT, byStage, codeStages, findWranglerConfigs, npmCiArgs, npmWorkspace, pick, problems, resolvedStack } from "./deploy/stack.mjs";
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders65import { withDeployWindow } from "./deploy/status-window.mjs";
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow66
Deploying: never roll production back by accident67const 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]";
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow68
69function parseArgs(argv) {
70 const opts = { command: argv[0], only: [], skip: [], concurrency: 4, force: false, all: false, json: false };
71 for (let i = 1; i < argv.length; i++) {
72 const arg = argv[i];
73 const [flag, inline] = arg.split(/=(.*)/s);
74 const value = () => inline ?? argv[++i];
75 if (flag === "--only") opts.only.push(value());
76 else if (flag === "--skip") opts.skip.push(value());
77 else if (flag === "--concurrency") opts.concurrency = Number(value());
78 else if (flag === "--stage") opts.stage = value();
79 else if (flag === "--all") opts.all = true;
80 else if (flag === "--force") opts.force = true;
Deploying: never roll production back by accident81 else if (flag === "--rollback") opts.rollback = true;
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow82 else if (flag === "--json") opts.json = true;
83 else if (flag === "--check") opts.check = true;
84 else if (flag === "--no-migrations") opts.noMigrations = true;
85 else if (flag === "--allow-dirty") opts.allowDirty = true;
86 else if (flag === "--rebuild-image") opts.rebuildImage = true;
Fast pages, required checks on the branch, self-hosted runners, honest incidents87 else if (flag === "--rebuild-base") opts.rebuildBase = true;
88 else if (flag === "--no-push") opts.noPush = true;
89 else if (flag === "--no-cache") opts.noCache = true;
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow90 else if (flag === "--out") opts.out = value();
91 else if (flag === "--since") opts.since = value();
92 else if (flag === "--github-output") opts.githubOutput = true;
93 else throw new Error(`unknown flag ${arg}\n${USAGE}`);
94 }
95 if (!Number.isInteger(opts.concurrency) || opts.concurrency < 1) throw new Error("--concurrency is a whole number, 1 or more");
96 return opts;
97}
98
99/** The units a command works on: --only (or all), less --skip, in --stage. */
100function selected(stack, opts) {
101 let units = opts.only.length ? pick(stack, opts.only) : [...stack.units];
102 const skipped = pick(stack, opts.skip);
103 units = units.filter((u) => !skipped.includes(u));
104 if (opts.stage) {
105 if (!codeStages(stack).includes(opts.stage)) throw new Error(`--stage is one of ${codeStages(stack).join(", ")}`);
106 units = units.filter((u) => u.stage === opts.stage);
107 }
108 // Manifest order, whatever order they were named in.
109 return stack.units.filter((u) => units.includes(u));
110}
111
112const log = (...args) => console.error(...args);
113
114/**
115 * The plan for a workflow (.g1t/workflows/deploy.yml): job outputs in
116 * $GITHUB_OUTPUT, and the plan as a table in $GITHUB_STEP_SUMMARY.
117 */
118function writeGithubOutputs(data, p) {
119 const lines = [
120 `commit=${data.commit}`,
121 `migrate=${data.migrations.length > 0}`,
122 `migrate_units=${data.migrations.map((m) => m.unit).join(",")}`,
123 `deploying=${data.units.filter((u) => u.deploy).map((u) => u.unit).join(",")}`,
124 ];
125 for (const [stage, { jobs }] of Object.entries(data.stages)) {
126 // A matrix needs one entry; an empty stage's job is skipped by its `if`.
127 const include = jobs.length ? jobs : [{ group: "none", units: "" }];
128 lines.push(`has_${stage}=${jobs.length > 0}`, `${stage}=${JSON.stringify({ include })}`);
129 }
Deploying: a Plan that cannot read migrations says why130 if (data.migration_errors.length) {
131 // The units, then why for the first: one cause (a token, say) is usually all of them.
132 throw new Error(`Could not read pending migrations: ${data.migration_errors.map((e) => e.unit).join(", ")}
133${data.migration_errors[0].error}`);
134 }
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow135 if (process.env.GITHUB_OUTPUT) appendFileSync(process.env.GITHUB_OUTPUT, `${lines.join("\n")}\n`);
136 else console.log(lines.join("\n"));
137 if (process.env.GITHUB_STEP_SUMMARY) {
138 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("|", "\\|")} |`);
139 const pending = data.migrations.map((m) => `- ${m.database}: ${m.pending.join(", ")}`);
140 appendFileSync(
141 process.env.GITHUB_STEP_SUMMARY,
142 [`## Deploy plan for ${data.commit.slice(0, 12)}`, "", "| unit | stage | action | live | why |", "| --- | --- | --- | --- | --- |", ...rows, "", pending.length ? "### Pending migrations" : "", ...pending, ""].join("\n"),
143 );
144 }
145}
146
147const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
148
149/** Reads what each unit runs and what its database is waiting for. */
150async function survey(units, opts) {
151 const live = {};
152 const migrations = {};
153 const tasks = [
154 ...(opts.noLive ? [] : units).map((unit) => async () => {
155 live[unit.id] = await readLive(unit);
156 }),
157 ...(opts.noMigrations ? [] : units.filter((u) => u.d1)).map((unit) => async () => {
158 migrations[unit.id] = await pendingMigrations(unit);
159 }),
160 ];
Merge a faster deploy plan: Cloudflare's API read directly and in parallel (2 s against 4 min), and the plan runs beside the check161 // Through the API every read starts at once (the requests themselves are
162 // held to API_CONCURRENCY); through Wrangler, a few processes at a time.
163 await pool(tasks, apiAuth() ? tasks.length : Math.max(8, opts.concurrency * 2), (task) => task());
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow164 return { live, migrations };
165}
166
167async function plan(stack, opts) {
168 const units = selected(stack, opts);
169 const head = git.head();
170 const { live, migrations } = await survey(units, opts);
171 // --since: what a Worker with no recorded commit is taken to run (once,
172 // to adopt Workers deployed before this tool), for example --since HEAD~3.
173 if (opts.since) {
174 const since = git.resolve(opts.since);
175 for (const unit of units) {
176 const found = live[unit.id];
177 if (found && !found.sha && !found.missing && !found.error) live[unit.id] = { ...found, sha: since, assumed: true };
178 }
179 }
Deploying: never roll production back by accident180 const decisions = decide(units, { live, head, force: opts.force || opts.all, rollback: opts.rollback });
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow181 return { head, units, live, migrations, decisions };
182}
183
184function buildPlan(stack, opts) {
185 const units = selected(stack, opts);
186 return {
187 head: git.head(),
188 units,
189 live: {},
190 migrations: {},
191 decisions: units.map((unit) => ({ unit, deploy: true, reason: "build", since: null, files: [], image: false })),
192 };
193}
194
195function printPlan(stack, { head, decisions, migrations, live }) {
196 console.log(`Deploying ${head.slice(0, 12)} (${git.subject()})\n`);
197 const rows = decisions.map((d) => [
198 d.unit.id,
199 d.unit.stage,
200 d.deploy ? "deploy" : "-",
201 d.since ? d.since.slice(0, 12) + (live[d.unit.id]?.assumed ? "*" : "") : "?",
202 d.unit.d1 ? (migrations[d.unit.id]?.error ? "error" : String(migrations[d.unit.id]?.pending?.length ?? "-")) : "",
203 d.reason + (d.image ? "; image rebuilds" : ""),
204 ]);
205 console.log(table(rows, ["unit", "stage", "action", "live", "migrations", "why"]));
206 if (decisions.some((d) => live[d.unit.id]?.assumed)) console.log("* taken from --since: no commit was recorded for it");
207 const pending = Object.entries(migrations).filter(([, m]) => m.pending?.length);
208 if (pending.length) {
209 console.log("\nPending migrations:");
210 for (const [id, m] of pending) console.log(` ${stack.units.find((u) => u.id === id).d1.database}: ${m.pending.join(", ")}`);
211 }
212 for (const [id, m] of Object.entries(migrations).filter(([, m]) => m.error)) {
213 console.log(`\nCould not list ${id}'s migrations:\n${m.error}`);
214 }
215 const deploying = decisions.filter((d) => d.deploy).map((d) => d.unit);
216 console.log(
217 deploying.length
218 ? `\n${deploying.length} to deploy: ${byStage(stack, deploying).map((g) => `${g.stage} (${g.units.map((u) => u.id).join(", ")})`).join(" -> ")}`
219 : "\nNothing to deploy.",
220 );
221}
222
223/** Output of one unit, prefixed, to the terminal and a log file. */
224function unitLogger(id) {
225 const dir = join(tmpdir(), "g1t-deploy");
226 mkdirSync(dir, { recursive: true });
227 const file = join(dir, `${id}.log`);
228 const lines = [];
229 return {
230 file,
231 line: (text) => {
232 lines.push(text);
233 if (text.trim()) log(`[${id}] ${text}`);
234 },
235 save: () => writeFileSync(file, `${lines.join("\n")}\n`),
236 };
237}
238
Fast pages, required checks on the branch, self-hosted runners, honest incidents239/**
240 * The runner's image for this checkout, by registry reference: already in
241 * the registry (built by an earlier deploy, `deploy.mjs image`, or on
242 * another machine), or built and pushed here, which needs Docker. A dry
243 * run builds it locally and pushes nothing. Returns { ref, note }.
244 */
245async function runnerImage(unit, { dryRun, docker, rebuildImage, rebuildBase }, out) {
246 let base = baseState(unit);
247 if (!base.current || rebuildBase) {
248 if (!rebuildBase) {
249 throw new Error(
250 `${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`,
251 );
252 }
253 if (!docker) throw new Error("--rebuild-base needs Docker.");
254 await newBase(unit, { push: !dryRun, onLine: out.line });
255 base = baseState(unit);
256 }
257 if (!base.pushed && !dryRun) throw new Error(`${unit.image.base.lock} records a base that was never pushed: node scripts/deploy.mjs build-base`);
258 const ref = runnerRef(unit);
259 const tag = ref.split(":").pop();
260 if (!rebuildImage) {
261 if (dryRun && docker && (await dockerHas(ref))) return { ref, note: `image ${tag} already built here` };
262 if (!dryRun) {
263 try {
264 if (await registryHas(ref)) return { ref, note: `image ${tag} already in the registry` };
265 } catch (error) {
266 out.line(`could not ask the registry for ${tag}: ${error.message ?? error}`);
267 }
268 }
269 }
270 if (!docker) {
271 throw new Error(
Merge branch 'main' into actions-toolkit-oidc-artifacts272 `its image ${tag} is not in the registry, and Docker does not answer here. Build and push it where Docker runs (a g1t Actions job, or a machine with Docker: node scripts/deploy.mjs image), then run this again.`,
Fast pages, required checks on the branch, self-hosted runners, honest incidents273 );
274 }
275 const started = Date.now();
276 const binary = await exec(process.execPath, [join(ROOT, "scripts/build-runner.mjs")], { onLine: out.line });
277 if (binary.code !== 0) throw new Error(`the runner binary did not build:\n${lastLines(binary.out)}`);
278 if (!dryRun) await dockerLogin({ push: true });
279 await buildRunnerImage(unit, { ref, base: base.ref, binaryDir: OUT_DIR, onLine: out.line });
280 if (!dryRun) await pushImage(ref, { onLine: out.line });
281 return { ref, note: `image ${tag} ${dryRun ? "built" : "built and pushed"} in ${seconds(Date.now() - started)}` };
282}
283
284/** Builds the base (and pushes it unless `push` is false), and writes its lock. */
285async function newBase(unit, { push = true, noCache = false, onLine = log } = {}) {
286 const started = Date.now();
287 const inputs = baseInputs(unit);
288 const tag = baseTag(inputs);
289 const previous = readBaseLock(unit);
290 // Logged in, the base it replaces is pulled as cache.
291 if (push || previous?.pushed) {
292 try {
293 await dockerLogin({ push });
294 } catch (error) {
295 if (push) throw error;
296 onLine(`not logged in to the registry, so no cache from it: ${error.message ?? error}`);
297 }
298 }
299 const ref = await buildBase(unit, { tag, previous: previous?.pushed ? previous.image : null, onLine, noCache });
300 const built = Date.now();
301 const [size, versions] = await Promise.all([imageSize(ref), baseVersions(ref)]);
302 const digest = push ? await pushImage(ref, { onLine }) : null;
303 const lock = { image: ref, digest, pushed: push, inputs, built_at: new Date().toISOString(), size_bytes: size, versions };
304 writeBaseLock(unit, lock);
305 onLine(`base ${tag}: built in ${seconds(built - started)}${push ? `, pushed in ${seconds(Date.now() - built)}` : ""}, ${(size / 1e9).toFixed(2)} GB unpacked`);
306 return lock;
307}
308
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow309/** Builds (and unless `dryRun`, deploys) one unit. */
Fast pages, required checks on the branch, self-hosted runners, honest incidents310async function ship(unit, decision, { head, subject, dirty, dryRun, docker, rebuildImage, rebuildBase }) {
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow311 const started = Date.now();
312 const out = unitLogger(unit.id);
313 const cwd = join(ROOT, unit.path);
314 const result = { unit: unit.id, stage: unit.stage, ok: false, version: null, ms: 0, note: "" };
315 try {
316 if (unit.kind === "react-router" || unit.kind === "astro") {
Deploys print no DEP0190: npm's commands go to the shell as one string317 // One command string: Node warns about arguments passed beside shell: true.
318 const built = await exec("npm run build", [], { cwd, onLine: out.line, shell: true, env: wranglerEnv() });
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow319 if (built.code !== 0) throw new Error(`npm run build failed:\n${lastLines(built.out)}`);
320 }
321 const args = ["deploy"];
322 if (dryRun) {
323 args.push("--dry-run", "--outdir", join(tmpdir(), "g1t-deploy", "dist", unit.id));
324 } else {
325 const { message, tag } = annotation(head, subject);
326 args.push("--message", dirty ? message.replace(/^g1t-deploy/, "g1t-deploy-dirty") : message, "--tag", tag);
327 }
328 if (unit.image) {
Fast pages, required checks on the branch, self-hosted runners, honest incidents329 // Wrangler is given the image by reference, so it builds nothing.
330 const image = await runnerImage(unit, { dryRun, docker, rebuildImage, rebuildBase }, out);
331 args.push("--config", writeDeployConfig(unit, image.ref));
332 // Nothing the image is built from changed: the running sandboxes
333 // keep going, and new ones start from the same image.
334 if (!rebuildImage && !rebuildBase && !decision.image) args.push("--containers-rollout", "none");
335 result.note = image.note;
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow336 }
337 const deployed = await wrangler(args, { cwd, onLine: out.line });
338 if (deployed.code !== 0) throw new Error(`wrangler deploy failed:\n${lastLines(deployed.out)}`);
339 result.version = dryRun ? "(dry run)" : versionFrom(deployed.out);
340 result.ok = true;
341 } catch (error) {
342 result.note = String(error.message ?? error);
Fast pages, required checks on the branch, self-hosted runners, honest incidents343 } finally {
344 if (unit.image) removeDeployConfig(unit);
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow345 }
346 result.ms = Date.now() - started;
347 out.save();
348 if (!result.ok) result.note += `\n full log: ${out.file}`;
349 return result;
350}
351
352async function migrate(stack, units, migrations, opts) {
353 const due = units.filter((u) => migrations[u.id]?.pending?.length);
354 const broken = units.filter((u) => migrations[u.id]?.error);
355 if (broken.length) {
356 throw new Error(`Could not read pending migrations for ${broken.map((u) => u.id).join(", ")}; nothing was deployed.`);
357 }
358 if (!due.length) return [];
359 log(`== migrations: ${due.map((u) => `${u.d1.database} (${migrations[u.id].pending.length})`).join(", ")}`);
360 const results = await pool(due, opts.concurrency, async (unit) => {
361 const started = Date.now();
362 const out = unitLogger(`${unit.id}-migrations`);
363 const applied = await applyMigrations(unit, out.line);
364 out.save();
365 return {
366 unit: `${unit.id} (D1 ${unit.d1.database})`,
367 stage: "migrations",
368 ok: applied.code === 0,
369 version: `${migrations[unit.id].pending.length} applied`,
370 ms: Date.now() - started,
371 note: applied.code === 0 ? "" : `${lastLines(applied.out)}\n full log: ${out.file}`,
372 };
373 });
374 return results;
375}
376
377function summary(results) {
378 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]]);
379 console.log(`\n${table(rows, ["unit", "stage", "result", "version", "time", "note"])}`);
380 for (const r of results.filter((r) => !r.ok && !r.skipped)) console.log(`\n${r.unit}: ${r.note}`);
381}
382
383async function deploy(stack, opts, { dryRun = false } = {}) {
384 const started = Date.now();
385 // A build reads nothing from Cloudflare: it builds what it is given.
386 const p = dryRun ? buildPlan(stack, opts) : await plan(stack, opts);
387 if (!dryRun) printPlan(stack, p);
388 const deploying = p.decisions.filter((d) => d.deploy);
389 const touched = deploying.map((d) => d.unit);
390 const head = p.head;
391
392 // A deploy names the commit it came from, so a dirty tree would be
393 // recorded as something it is not.
394 const dirtyFiles = git.dirty();
395 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)));
396 if (dirty && !dryRun && !opts.allowDirty) {
397 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).");
398 }
399
400 const results = [];
401 if (!dryRun && !opts.noMigrations) {
402 const migrated = await migrate(stack, p.units, p.migrations, opts);
403 results.push(...migrated);
404 if (migrated.some((r) => !r.ok)) {
405 summary(results);
406 return false;
407 }
408 }
409 if (!touched.length) {
410 if (results.length) summary(results);
411 return true;
412 }
413
414 if (touched.some((u) => u.kind === "rust-worker")) ensureWorkerBuild();
415 const docker = touched.some((u) => u.image) ? await dockerAvailable() : false;
Fast pages, required checks on the branch, self-hosted runners, honest incidents416 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 page417 // A deploy run by hand shows on the repository's Deployments page; a
418 // dirty tree is not a commit anyone can look at, so it is left out.
419 const settle = dryRun || dirty ? null : await reportDeployment({ head, subject: context.subject, units: touched.map((u) => u.id), log });
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow420
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders421 // status.g1t.sh hears the deploy start and finish (STATUS_DEPLOY_TOKEN;
422 // nothing without it), so the restarts it causes are not drafted as
423 // incidents. Never in a dry run, and it never fails a deploy.
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow424 let failed = false;
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders425 await withDeployWindow(
426 async () => {
427 for (const { stage, units } of byStage(stack, touched)) {
428 if (failed) {
429 for (const unit of units) results.push({ unit: unit.id, stage, ok: false, skipped: true, ms: 0, note: "an earlier stage failed" });
430 continue;
431 }
432 log(`== ${stage}: ${units.map((u) => u.id).join(", ")}`);
433 const shipped = await pool(units, opts.concurrency, (unit) => ship(unit, deploying.find((d) => d.unit === unit), context));
434 results.push(...shipped);
435 failed = shipped.some((r) => !r.ok);
436 }
437 },
438 { id: head ?? null, dryRun, log },
439 );
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow440 summary(results);
Deploying: a deploy by hand shows on the repository's Deployments page441 await settle?.(!failed);
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow442 console.log(`\n${failed ? "Failed" : dryRun ? "Built" : "Deployed"} in ${seconds(Date.now() - started)}.`);
443 return !failed;
444}
445
446async function doctor(stack, opts) {
447 const units = selected(stack, opts);
448 const rows = await pool(units, 8, async (unit) => {
449 if (!unit.secrets.length) return [unit.id, "", "", ""];
450 const found = await wrangler(["secret", "list", "--name", unit.worker, "--format", "json"], { cwd: join(ROOT, unit.path) });
451 if (found.code !== 0) return [unit.id, unit.secrets.join(" "), "?", "could not read"];
452 const have = new Set(jsonFrom(found.out).map((s) => s.name));
453 const missing = unit.secrets.filter((name) => !have.has(name));
454 return [unit.id, unit.secrets.join(" "), missing.join(" "), missing.length ? "missing" : "ok"];
455 });
456 console.log(table(rows, ["unit", "secrets", "missing", "result"]));
457 return rows.every((r) => r[3] !== "missing");
458}
459
460async function install(stack, opts) {
461 const units = selected(stack, opts);
462 const args = npmCiArgs(units, npmWorkspace());
463 log(`npm ${args.join(" ")}`);
Deploys print no DEP0190: npm's commands go to the shell as one string464 const done = await exec(`npm ${args.join(" ")}`, [], { cwd: ROOT, shell: true, onLine: (line) => log(line) });
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow465 return done.code === 0;
466}
467
468function manifest(stack, opts) {
469 const found = problems(stack, findWranglerConfigs());
470 if (opts.check) {
471 for (const problem of found) console.log(`- ${problem}`);
472 console.log(found.length ? `\n${found.length} problems in deploy/stack.jsonc.` : "deploy/stack.jsonc is consistent with every wrangler.jsonc.");
473 return !found.length;
474 }
475 const out = stack.units.map(({ config, ...unit }) => ({
476 ...unit,
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails477 queues: {
478 produces: (config?.queues?.producers ?? []).map((q) => q.queue),
479 consumes: (config?.queues?.consumers ?? []).map((q) => q.queue),
480 dead_letter: [...new Set((config?.queues?.consumers ?? []).map((q) => q.dead_letter_queue).filter(Boolean))],
481 },
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow482 kv: (config?.kv_namespaces ?? []).map((kv) => stack.resources.kv?.[kv.id] ?? kv.id),
483 r2: (config?.r2_buckets ?? []).map((b) => b.bucket_name),
484 vectorize: (config?.vectorize ?? []).map((v) => v.index_name),
485 dispatch_namespaces: (config?.dispatch_namespaces ?? []).map((d) => d.namespace),
486 routes: (config?.routes ?? []).map((r) => r.pattern),
487 }));
488 if (opts.json) console.log(JSON.stringify({ stages: stack.stages, units: out }, null, 2));
489 else
490 console.log(
491 table(
492 out.map((u) => [u.id, u.stage, u.kind, u.worker, u.d1?.database ?? "", u.dependsOn.join(" ")]),
493 ["unit", "stage", "kind", "worker", "d1", "built from (besides its folder)"],
494 ),
495 );
496 return true;
497}
498
Fast pages, required checks on the branch, self-hosted runners, honest incidents499/** The unit with a Containers image (the runner), or the one named. */
500function imageUnit(stack, opts) {
501 const units = (opts.only.length ? pick(stack, opts.only) : stack.units).filter((u) => u.image?.base);
502 if (units.length !== 1) throw new Error("Name the unit with a Containers image: --only runner");
503 return units[0];
504}
505
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow506async function main() {
507 const opts = parseArgs(process.argv.slice(2));
508 const stack = resolvedStack();
509 switch (opts.command) {
510 case "plan": {
511 const p = await plan(stack, opts);
512 const data = planJson(stack, p.decisions, p.migrations, p.head);
513 if (opts.out) writeFileSync(opts.out, `${JSON.stringify(data)}\n`);
514 if (opts.githubOutput) writeGithubOutputs(data, p);
515 if (opts.json) console.log(JSON.stringify(data, null, 2));
516 else if (!opts.githubOutput || process.env.GITHUB_OUTPUT) printPlan(stack, p);
517 return true;
518 }
519 case "deploy":
520 return deploy(stack, opts);
521 case "build":
522 return deploy(stack, { ...opts, force: true }, { dryRun: true });
523 case "migrate": {
524 const units = selected(stack, opts);
525 const { migrations } = await survey(units.filter((u) => u.d1), { ...opts, noMigrations: false, noLive: true });
526 const results = await migrate(stack, units, migrations, opts);
527 if (results.length) summary(results);
528 else console.log("No migrations to apply.");
529 return results.every((r) => r.ok);
530 }
531 case "manifest":
532 return manifest(stack, opts);
533 case "doctor":
534 return doctor(stack, opts);
535 case "install":
536 return install(stack, opts);
Fast pages, required checks on the branch, self-hosted runners, honest incidents537 case "build-base": {
538 const unit = imageUnit(stack, opts);
539 if (!(await dockerAvailable())) throw new Error("build-base needs Docker.");
540 const lock = await newBase(unit, { push: !opts.noPush, noCache: opts.noCache });
541 console.log(JSON.stringify(lock, null, 2));
542 if (lock.pushed) console.log(`\nCommit ${unit.image.base.lock}: the next deploy builds the runner's image on this base.`);
543 return true;
544 }
545 case "image": {
546 const unit = imageUnit(stack, opts);
547 const out = unitLogger(`${unit.id}-image`);
548 const docker = await dockerAvailable();
549 const image = await runnerImage(unit, { dryRun: Boolean(opts.noPush), docker, rebuildImage: opts.rebuildImage, rebuildBase: opts.rebuildBase }, out);
550 out.save();
551 console.log(`${image.ref}\n${image.note}`);
552 return true;
553 }
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow554 default:
555 console.error(USAGE);
556 return false;
557 }
558}
559
560main().then(
561 (ok) => process.exit(ok ? 0 : 1),
562 (error) => {
563 console.error(`\n${error.message ?? error}`);
564 process.exit(1);
565 },
566);

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