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

Put the stylesheet at the top of the head, before React Router's preloads1#!/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.
Put the stylesheet at the top of the head, before React Router's preloads5//
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)
15// node scripts/deploy.mjs image build and push the runner's image for this checkout (Docker)
16//
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
28import { OUT_DIR } from "./build-runner.mjs";
29import { 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,
Put the stylesheet at the top of the head, before React Router's preloads33 applyMigrations,
34 dockerAvailable,
35 exec,
36 jsonFrom,
37 lastLines,
38 pendingMigrations,
39 readLive,
40 versionFrom,
41 wrangler,
42 wranglerEnv,
43} from "./deploy/cloudflare.mjs";
44import {
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";
62import { 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";
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.64import { ROOT, byStage, codeStages, 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, reminders65import { withDeployWindow } from "./deploy/status-window.mjs";
Put the stylesheet at the top of the head, before React Router's preloads66
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]";
Put the stylesheet at the top of the head, before React Router's preloads68
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;
Put the stylesheet at the top of the head, before React Router's preloads82 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;
87 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;
90 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 }
Put the stylesheet at the top of the head, before React Router's preloads135 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());
Put the stylesheet at the top of the head, before React Router's preloads164 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 });
Put the stylesheet at the top of the head, before React Router's preloads181 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
239/**
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.`,
Put the stylesheet at the top of the head, before React Router's preloads273 );
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
309/** Builds (and unless `dryRun`, deploys) one unit. */
310async function ship(unit, decision, { head, subject, dirty, dryRun, docker, rebuildImage, rebuildBase }) {
311 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() });
Put the stylesheet at the top of the head, before React Router's preloads319 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) {
329 // 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;
336 }
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);
343 } finally {
344 if (unit.image) removeDeployConfig(unit);
345 }
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;
416 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 });
Put the stylesheet at the top of the head, before React Router's preloads420
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.
Put the stylesheet at the top of the head, before React Router's preloads424 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 }
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.432 // A Worker that does not exist yet goes before those bound to it.
433 for (const wave of waves(units, (unit) => Boolean(deploying.find((d) => d.unit === unit)?.missing))) {
434 if (failed) {
435 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" });
436 continue;
437 }
438 log(`== ${stage}: ${wave.map((u) => u.id).join(", ")}`);
439 const shipped = await pool(wave, opts.concurrency, (unit) => ship(unit, deploying.find((d) => d.unit === unit), context));
440 results.push(...shipped);
441 failed = shipped.some((r) => !r.ok);
442 }
Merge status detection: first-byte speed probe, deploy windows, 4 of 5 with a re-check, check history, reminders443 }
444 },
445 { id: head ?? null, dryRun, log },
446 );
Put the stylesheet at the top of the head, before React Router's preloads447 summary(results);
Deploying: a deploy by hand shows on the repository's Deployments page448 await settle?.(!failed);
Put the stylesheet at the top of the head, before React Router's preloads449 console.log(`\n${failed ? "Failed" : dryRun ? "Built" : "Deployed"} in ${seconds(Date.now() - started)}.`);
450 return !failed;
451}
452
453async function doctor(stack, opts) {
454 const units = selected(stack, opts);
455 const rows = await pool(units, 8, async (unit) => {
456 if (!unit.secrets.length) return [unit.id, "", "", ""];
457 const found = await wrangler(["secret", "list", "--name", unit.worker, "--format", "json"], { cwd: join(ROOT, unit.path) });
458 if (found.code !== 0) return [unit.id, unit.secrets.join(" "), "?", "could not read"];
459 const have = new Set(jsonFrom(found.out).map((s) => s.name));
460 const missing = unit.secrets.filter((name) => !have.has(name));
461 return [unit.id, unit.secrets.join(" "), missing.join(" "), missing.length ? "missing" : "ok"];
462 });
463 console.log(table(rows, ["unit", "secrets", "missing", "result"]));
464 return rows.every((r) => r[3] !== "missing");
465}
466
467async function install(stack, opts) {
468 const units = selected(stack, opts);
469 const args = npmCiArgs(units, npmWorkspace());
470 log(`npm ${args.join(" ")}`);
Deploys print no DEP0190: npm's commands go to the shell as one string471 const done = await exec(`npm ${args.join(" ")}`, [], { cwd: ROOT, shell: true, onLine: (line) => log(line) });
Put the stylesheet at the top of the head, before React Router's preloads472 return done.code === 0;
473}
474
475function manifest(stack, opts) {
476 const found = problems(stack, findWranglerConfigs());
477 if (opts.check) {
478 for (const problem of found) console.log(`- ${problem}`);
479 console.log(found.length ? `\n${found.length} problems in deploy/stack.jsonc.` : "deploy/stack.jsonc is consistent with every wrangler.jsonc.");
480 return !found.length;
481 }
482 const out = stack.units.map(({ config, ...unit }) => ({
483 ...unit,
Merge branch 'worktree-agent-ad8a36dfcd4176015' into spend-guardrails484 queues: {
485 produces: (config?.queues?.producers ?? []).map((q) => q.queue),
486 consumes: (config?.queues?.consumers ?? []).map((q) => q.queue),
487 dead_letter: [...new Set((config?.queues?.consumers ?? []).map((q) => q.dead_letter_queue).filter(Boolean))],
488 },
Put the stylesheet at the top of the head, before React Router's preloads489 kv: (config?.kv_namespaces ?? []).map((kv) => stack.resources.kv?.[kv.id] ?? kv.id),
490 r2: (config?.r2_buckets ?? []).map((b) => b.bucket_name),
491 vectorize: (config?.vectorize ?? []).map((v) => v.index_name),
492 dispatch_namespaces: (config?.dispatch_namespaces ?? []).map((d) => d.namespace),
493 routes: (config?.routes ?? []).map((r) => r.pattern),
494 }));
495 if (opts.json) console.log(JSON.stringify({ stages: stack.stages, units: out }, null, 2));
496 else
497 console.log(
498 table(
499 out.map((u) => [u.id, u.stage, u.kind, u.worker, u.d1?.database ?? "", u.dependsOn.join(" ")]),
500 ["unit", "stage", "kind", "worker", "d1", "built from (besides its folder)"],
501 ),
502 );
503 return true;
504}
505
506/** The unit with a Containers image (the runner), or the one named. */
507function imageUnit(stack, opts) {
508 const units = (opts.only.length ? pick(stack, opts.only) : stack.units).filter((u) => u.image?.base);
509 if (units.length !== 1) throw new Error("Name the unit with a Containers image: --only runner");
510 return units[0];
511}
512
513async function main() {
514 const opts = parseArgs(process.argv.slice(2));
515 const stack = resolvedStack();
516 switch (opts.command) {
517 case "plan": {
518 const p = await plan(stack, opts);
519 const data = planJson(stack, p.decisions, p.migrations, p.head);
520 if (opts.out) writeFileSync(opts.out, `${JSON.stringify(data)}\n`);
521 if (opts.githubOutput) writeGithubOutputs(data, p);
522 if (opts.json) console.log(JSON.stringify(data, null, 2));
523 else if (!opts.githubOutput || process.env.GITHUB_OUTPUT) printPlan(stack, p);
524 return true;
525 }
526 case "deploy":
527 return deploy(stack, opts);
528 case "build":
529 return deploy(stack, { ...opts, force: true }, { dryRun: true });
530 case "migrate": {
531 const units = selected(stack, opts);
532 const { migrations } = await survey(units.filter((u) => u.d1), { ...opts, noMigrations: false, noLive: true });
533 const results = await migrate(stack, units, migrations, opts);
534 if (results.length) summary(results);
535 else console.log("No migrations to apply.");
536 return results.every((r) => r.ok);
537 }
538 case "manifest":
539 return manifest(stack, opts);
540 case "doctor":
541 return doctor(stack, opts);
542 case "install":
543 return install(stack, opts);
544 case "build-base": {
545 const unit = imageUnit(stack, opts);
546 if (!(await dockerAvailable())) throw new Error("build-base needs Docker.");
547 const lock = await newBase(unit, { push: !opts.noPush, noCache: opts.noCache });
548 console.log(JSON.stringify(lock, null, 2));
549 if (lock.pushed) console.log(`\nCommit ${unit.image.base.lock}: the next deploy builds the runner's image on this base.`);
550 return true;
551 }
552 case "image": {
553 const unit = imageUnit(stack, opts);
554 const out = unitLogger(`${unit.id}-image`);
555 const docker = await dockerAvailable();
556 const image = await runnerImage(unit, { dryRun: Boolean(opts.noPush), docker, rebuildImage: opts.rebuildImage, rebuildBase: opts.rebuildBase }, out);
557 out.save();
558 console.log(`${image.ref}\n${image.note}`);
559 return true;
560 }
561 default:
562 console.error(USAGE);
563 return false;
564 }
565}
566
567main().then(
568 (ok) => process.exit(ok ? 0 : 1),
569 (error) => {
570 console.error(`\n${error.message ?? error}`);
571 process.exit(1);
572 },
573);

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