g1t/deploy/self-host/configs.mjs

380 lines15,844 bytesCodeBlame

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.

Running g1t yourself: the design, a docker compose proof, and a guide to what works today1#!/usr/bin/env node
2// Writes the Wrangler configs a self-hosted g1t runs with, derived from the
3// hosted ones, so the two never drift apart.
4//
5// Each hosted service's wrangler.jsonc is read and changed only where
6// Cloudflare-only things live:
7//
8// - account, routes, placement, observability and builds are dropped;
9// - ARTIFACTS (git storage) becomes a service binding to workers/artifacts,
10// which keeps repositories in the git store (gitstore/server.mjs);
11// - EMAIL (Email Sending) becomes a service binding to workers/mail;
Merge branch 'worktree-agent-af58ac8933b0dd125'12// - the packages service keeps files in S3-compatible storage (RustFS)
Merge branch 'worktree-agent-ac5b181a013e54348'13// instead of R2, and the repos service its nightly backups (a bucket of
14// their own, BACKUP_S3_BUCKET);
Running g1t yourself: the design, a docker compose proof, and a guide to what works today15// - services that are off in this phase (agents, the context hub, the
16// g1t.page dispatcher, model proxy) are bound to workers/off instead, and
17// events stop queueing work for them;
18// - URLs that name g1t.sh name PUBLIC_URL instead, and billing is free.
19//
20// Usage: node configs.mjs [outDir]
21// Environment: PUBLIC_URL, GITSTORE_URL, GITSTORE_SECRET, MAIL_URL,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member22// ACTIONS_KEY, INTEGRATIONS_KEY, WEBHOOKS_KEY, IDENTITY_KEY,
Merge branch 'worktree-agent-ac5b181a013e54348'23// PACKAGES_TOKEN_SECRET, S3_ENDPOINT, S3_BUCKET, BACKUP_S3_BUCKET, S3_REGION,
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member24// S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_PUBLIC_ENDPOINT, and optionally
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look25// your own GitHub App: GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID,
26// GITHUB_APP_CLIENT_SECRET, GITHUB_APP_PRIVATE_KEY, GITHUB_APP_WEBHOOK_SECRET.
Running g1t yourself: the design, a docker compose proof, and a guide to what works today27//
28// The output is for `wrangler dev` (see start.sh): every Worker in one
29// workerd, the site first, with D1, KV and Queues kept on disk.
30
31import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
32import { dirname, join, relative, resolve } from "node:path";
33import { fileURLToPath } from "node:url";
34
35const here = dirname(fileURLToPath(import.meta.url));
36const root = resolve(here, "../..");
37const out = resolve(process.argv[2] ?? join(here, ".generated"));
38mkdirSync(out, { recursive: true });
39
40const PUBLIC_URL = (process.env.PUBLIC_URL ?? "http://localhost:8787").replace(/\/$/, "");
41
Merge branch 'worktree-agent-aaf03bdceac799c89'42/**
43 * Where the API (REST, OAuth) is reached: API_URL, or PUBLIC_URL's host on
44 * API_PORT (8789). The MCP server is a path on it: MCP_URL, or
45 * `<API_URL>/mcp`. The API's OAuth issuer is API_URL.
46 */
47export function apiAddresses(env = process.env, publicUrl = PUBLIC_URL) {
48 let api = (env.API_URL ?? "").trim().replace(/\/$/, "");
49 if (!api) {
50 const url = new URL(publicUrl);
51 url.port = env.API_PORT || "8789";
52 api = url.origin;
53 }
54 const mcp = (env.MCP_URL ?? "").trim().replace(/\/$/, "") || `${api}/mcp`;
55 return { api, mcp };
56}
57const { api: API_URL, mcp: MCP_URL } = apiAddresses();
58
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow59// What runs, and what is off, is each unit's `self_host` in
60// deploy/stack.jsonc: the list hosted g1t deploys from.
61const STACK = Object.values(parseJsonc(readFileSync(join(root, "deploy/stack.jsonc"), "utf8")).units);
62
Running g1t yourself: the design, a docker compose proof, and a guide to what works today63/** Services that run, in the order Wrangler is given them (the site first). */
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow64export const RUNNING = STACK.filter((unit) => unit.self_host === "run")
65 .map((unit) => ({ name: unit.worker, dir: unit.path, web: unit.kind === "react-router" }))
66 .sort((a, b) => Number(b.web) - Number(a.web));
Running g1t yourself: the design, a docker compose proof, and a guide to what works today67
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow68/** What the off Worker calls each service that is off in phase 1. */
69const OFF_NAMES = {
Running g1t yourself: the design, a docker compose proof, and a guide to what works today70 "g1t-runner": "Agents",
71 "g1t-context": "Context search and memory",
72};
Deploys as code: a manifest of every Worker, a deploy tool that ships only what changed in parallel stages, and a g1t Actions workflow73const OFF = Object.fromEntries(
74 STACK.filter((unit) => unit.self_host === "off").map((unit) => [unit.worker, OFF_NAMES[unit.worker] ?? unit.worker]),
75);
Running g1t yourself: the design, a docker compose proof, and a guide to what works today76
77/** Sealing keys, by the service that holds each (hosted: Wrangler secrets). */
78const SECRETS = {
79 "g1t-actions": "ACTIONS_KEY",
80 "g1t-integrations": "INTEGRATIONS_KEY",
81 "g1t-webhooks": "WEBHOOKS_KEY",
82};
83
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look84/**
85 * g1t.sh's GitHub App is its own: an installation registers one of its
86 * own, or has none, and then no GitHub buttons appear. Its public settings
87 * replace the hosted vars; its secrets go only to the service that uses each.
88 */
89const GITHUB_VARS = ["GITHUB_APP_ID", "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID"];
90const GITHUB_SECRETS = {
91 "g1t-identity": ["GITHUB_APP_CLIENT_SECRET", "IDENTITY_KEY", "REGISTRATION_MODE"],
92 "g1t-integrations": ["GITHUB_APP_PRIVATE_KEY", "GITHUB_APP_WEBHOOK_SECRET"],
93};
94
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member95/**
96 * Services whose cron triggers scheduler.mjs runs here: sweeps and
97 * reminders that need nothing self-hosting lacks. Not run: actions (its
98 * minute would start scheduled workflows with no runner to take them),
99 * billing (reconciles against Cloudflare and Stripe), deployments (calls
100 * Cloudflare's API) and the services that are off.
101 */
102const SELF_HOST_CRONS = new Set(["g1t-repos", "g1t-events", "g1t-identity", "g1t-security", "g1t-webhooks", "g1t-packages"]);
103
Running g1t yourself: the design, a docker compose proof, and a guide to what works today104/** Queues whose consumers are off: events stops sending to them. */
105const OFF_QUEUES = new Set(["g1t-events-runner", "g1t-events-context"]);
106
107/** Strips comments and trailing commas from JSONC. Strings are respected. */
108function parseJsonc(text) {
109 let result = "";
110 let inString = false;
111 for (let i = 0; i < text.length; i++) {
112 const char = text[i];
113 if (inString) {
114 result += char;
115 if (char === "\\") result += text[++i];
116 else if (char === '"') inString = false;
117 } else if (char === '"') {
118 inString = true;
119 result += char;
120 } else if (char === "/" && text[i + 1] === "/") {
121 while (i < text.length && text[i] !== "\n") i++;
122 result += "\n";
123 } else if (char === "/" && text[i + 1] === "*") {
124 i = text.indexOf("*/", i + 2) + 1;
125 } else {
126 result += char;
127 }
128 }
129 return JSON.parse(result.replace(/,(\s*[}\]])/g, "$1"));
130}
131
132const rel = (path) => relative(out, resolve(root, path)).replaceAll("\\", "/");
133
134function hostedUrl(value) {
Merge branch 'worktree-agent-aaf03bdceac799c89'135 return typeof value === "string"
136 ? value.replace(/https:\/\/api\.g1t\.sh/g, API_URL).replace(/https:\/\/g1t\.sh/g, PUBLIC_URL)
137 : value;
Running g1t yourself: the design, a docker compose proof, and a guide to what works today138}
139
140function selfHosted(service) {
141 const hosted = parseJsonc(readFileSync(join(root, service.dir, "wrangler.jsonc"), "utf8"));
142 const config = {
143 name: hosted.name,
144 compatibility_date: hosted.compatibility_date,
145 compatibility_flags: hosted.compatibility_flags,
146 rules: hosted.rules,
147 vars: {},
148 };
149
150 if (service.web) {
151 // The site as React Router built it (apps/web/build), not its sources.
152 config.main = rel(`${service.dir}/build/server/index.js`);
153 config.no_bundle = true;
154 config.rules = [{ type: "ESModule", globs: ["**/*.js", "**/*.mjs"] }];
155 config.assets = { directory: rel(`${service.dir}/build/client`) };
156 } else {
157 config.main = rel(join(service.dir, hosted.main));
158 }
159
160 for (const [key, value] of Object.entries(hosted.vars ?? {})) config.vars[key] = hostedUrl(value);
161
162 if (hosted.d1_databases) {
163 config.d1_databases = hosted.d1_databases.map((db) => ({
164 binding: db.binding,
165 database_name: db.database_name,
166 database_id: db.database_id,
167 migrations_dir: rel(join(service.dir, db.migrations_dir ?? "migrations")),
168 }));
169 }
170 if (hosted.kv_namespaces) config.kv_namespaces = hosted.kv_namespaces.map(({ binding, id }) => ({ binding, id }));
171 if (hosted.triggers) config.triggers = hosted.triggers;
172
173 if (hosted.queues) {
174 config.queues = {};
175 if (hosted.queues.producers) {
176 config.queues.producers = hosted.queues.producers.filter((producer) => !OFF_QUEUES.has(producer.queue));
177 }
178 if (hosted.queues.consumers) config.queues.consumers = hosted.queues.consumers;
179 }
180
181 config.services = (hosted.services ?? []).map((binding) =>
182 OFF[binding.service] ? { binding: binding.binding, service: offName(binding.service) } : binding,
183 );
184
185 // Cloudflare-only bindings, and what stands in for them.
186 if (hosted.artifacts) {
187 for (const artifacts of hosted.artifacts) {
188 config.services.push({ binding: artifacts.binding, service: "g1t-artifacts" });
189 }
190 }
191 if (hosted.send_email) {
192 for (const email of hosted.send_email) config.services.push({ binding: email.name, service: "g1t-mail" });
193 }
194
195 // Secrets the hosted services hold, given here from the environment, each
196 // only to the service that uses it.
197 const secret = SECRETS[hosted.name];
198 if (secret && process.env[secret]) config.vars[secret] = process.env[secret];
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look199 for (const name of GITHUB_VARS) {
200 if (name in config.vars) config.vars[name] = process.env[name] ?? "";
201 }
202 for (const name of GITHUB_SECRETS[hosted.name] ?? []) {
203 if (process.env[name]) config.vars[name] = process.env[name];
204 }
Running g1t yourself: the design, a docker compose proof, and a guide to what works today205
206 // Self-hosted g1t charges nothing: billing records usage at cost and never
207 // stops work for it.
208 if (hosted.name === "g1t-billing") config.vars.FREE_WHILE_BUILDING = "true";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look209 // Anyone may register on an installation of your own unless you set
210 // REGISTRATION_MODE=invite; invites then work as on g1t.sh, and the owners
211 // of INVITE_STAFF_WORKSPACES (yours, not g1t.sh's) invite without limit.
212 if (hosted.name === "g1t-identity") {
213 config.vars.REGISTRATION_MODE = process.env.REGISTRATION_MODE || "open";
214 config.vars.INVITE_STAFF_WORKSPACES = process.env.INVITE_STAFF_WORKSPACES ?? "";
215 if (process.env.INVITES_PER_USER) config.vars.INVITES_PER_USER = process.env.INVITES_PER_USER;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas216 // Access requests are summarised to your own address, not g1t.sh's.
217 config.vars.WAITLIST_NOTIFY_EMAIL = process.env.WAITLIST_NOTIFY_EMAIL ?? "";
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look218 }
Merge branch 'worktree-agent-af58ac8933b0dd125'219 // Packages' files go to the compose file's RustFS (or any S3-compatible
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member220 // store) instead of R2, with no request size limit, and package
221 // addresses start with this installation's host.
222 if (hosted.name === "g1t-packages") {
223 Object.assign(config.vars, {
224 BLOB_STORE: "s3",
Merge branch 'worktree-agent-af58ac8933b0dd125'225 S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member226 S3_BUCKET: process.env.S3_BUCKET ?? "g1t-packages",
227 S3_REGION: process.env.S3_REGION ?? "us-east-1",
228 S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
229 S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
230 S3_PUBLIC_ENDPOINT: process.env.S3_PUBLIC_ENDPOINT ?? "",
231 MAX_REQUEST_BYTES: "0",
232 STORAGE_LIMITS: "off",
233 REGISTRY_HOST: new URL(PUBLIC_URL).host,
234 PACKAGES_TOKEN_SECRET: process.env.PACKAGES_TOKEN_SECRET ?? "",
235 });
236 delete config.vars.R2_ACCOUNT_ID;
237 delete config.vars.R2_BUCKET;
238 }
Merge branch 'worktree-agent-aaf03bdceac799c89'239 // Where this installation is reached, for the links and addresses each
240 // shows: the site's clone URLs, meta tags and agent setup, the API's
241 // OAuth issuer and MCP server, and identity's mail. No social cards: the
242 // card service (services/og) is not run here.
243 if (service.web) Object.assign(config.vars, { SITE_URL: PUBLIC_URL, API_URL, MCP_URL, OG_URL: "" });
244 if (hosted.name === "g1t-api") Object.assign(config.vars, { SITE_URL: PUBLIC_URL, API_URL, MCP_URL });
245 if (hosted.name === "g1t-identity") config.vars.SITE_URL = PUBLIC_URL;
Merge branch 'worktree-agent-ac5b181a013e54348'246 // Nightly backups' bundles go to a bucket of their own on the same
Merge branch 'worktree-agent-aaf03bdceac799c89'247 // S3-compatible store, instead of the BACKUPS R2 bucket, and so do the
248 // packs kept for fresh clones (src/pack_cache.rs), instead of GIT_PACKS.
Merge branch 'worktree-agent-ac5b181a013e54348'249 if (hosted.name === "g1t-repos") {
250 Object.assign(config.vars, {
251 BACKUP_STORE: "s3",
252 BACKUP_S3_BUCKET: process.env.BACKUP_S3_BUCKET ?? "g1t-backups",
Merge branch 'worktree-agent-aaf03bdceac799c89'253 PACK_STORE: "s3",
254 PACK_S3_BUCKET: process.env.PACK_S3_BUCKET ?? "g1t-git-packs",
Merge branch 'worktree-agent-af58ac8933b0dd125'255 S3_ENDPOINT: process.env.S3_ENDPOINT ?? "http://rustfs:9000",
Merge branch 'worktree-agent-ac5b181a013e54348'256 S3_REGION: process.env.S3_REGION ?? "us-east-1",
257 S3_ACCESS_KEY_ID: process.env.S3_ACCESS_KEY_ID ?? "",
258 S3_SECRET_ACCESS_KEY: process.env.S3_SECRET_ACCESS_KEY ?? "",
259 });
260 }
Running g1t yourself: the design, a docker compose proof, and a guide to what works today261 // Nothing to deploy to: deployments are off (no Cloudflare API token).
262 if (hosted.name === "g1t-deployments") delete config.vars.CUSTOM_HOSTNAMES_ZONE_ID;
263
264 return config;
265}
266
267function offName(service) {
268 return `${service}-off`;
269}
270
271function write(name, config) {
272 const path = join(out, `${name}.json`);
273 writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
274 return path;
275}
276
277const files = [];
278for (const service of RUNNING) files.push(write(service.name, selfHosted(service)));
279
280const compatibility_date = "2026-09-26";
281files.push(
282 write("g1t-artifacts", {
283 name: "g1t-artifacts",
284 main: rel("deploy/self-host/workers/artifacts/index.js"),
285 compatibility_date,
286 vars: {
287 GITSTORE_URL: process.env.GITSTORE_URL ?? "http://gitstore:8080",
288 GITSTORE_SECRET: process.env.GITSTORE_SECRET ?? "",
289 },
290 }),
291);
292files.push(
293 write("g1t-mail", {
294 name: "g1t-mail",
295 main: rel("deploy/self-host/workers/mail/index.js"),
296 compatibility_date,
297 vars: {
298 PUBLIC_URL,
299 MAIL_URL: process.env.MAIL_URL ?? "",
300 MAIL_FROM: process.env.MAIL_FROM ?? "",
301 },
302 }),
303);
304for (const [service, feature] of Object.entries(OFF)) {
305 files.push(
306 write(offName(service), {
307 name: offName(service),
308 main: rel("deploy/self-host/workers/off/index.js"),
309 compatibility_date,
310 vars: { OFF_NAME: feature },
311 }),
312 );
313}
314
315// The order Wrangler takes them in: the site first, as the one that serves.
316writeFileSync(join(out, "workers.txt"), `${files.map((file) => relative(out, file)).join("\n")}\n`);
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas317
Merge branch 'worktree-agent-aaf03bdceac799c89'318// The API (REST, MCP and OAuth) is served on a port of its own (API_PORT),
319// by a second `wrangler dev` (start.sh): not in workers.txt. Its service
320// bindings reach the Workers above through Wrangler's dev registry, as
321// any two `wrangler dev` sessions on one machine do.
322{
323 const api = STACK.find((unit) => unit.worker === "g1t-api");
324 write("g1t-api", selfHosted({ name: api.worker, dir: api.path, web: false }));
325 writeFileSync(join(out, "api.json"), `${JSON.stringify({ api: API_URL, mcp: MCP_URL }, null, 2)}\n`);
326}
327
Packages, with a container registry on g1t.sh; workspaces deleted whole and kept 30 days; Members for every member328// What scheduler.mjs runs: each service's own crons, as hosted g1t's Cron
329// Triggers run them, for the services in SELF_HOST_CRONS.
330const schedules = RUNNING.filter((service) => SELF_HOST_CRONS.has(service.name))
331 .map((service) => ({
332 worker: service.name,
333 crons: parseJsonc(readFileSync(join(root, service.dir, "wrangler.jsonc"), "utf8")).triggers?.crons ?? [],
334 }))
335 .filter((schedule) => schedule.crons.length > 0);
336writeFileSync(join(out, "schedules.json"), `${JSON.stringify(schedules, null, 2)}\n`);
337
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas338// The status page runs in a workerd of its own (status.sh, the `status`
339// service in docker-compose.yml), so it stays up when the site does not:
340// not in workers.txt. It checks the site from inside Compose
341// (STATUS_CHECK_URL) and links to it at PUBLIC_URL. Parts this
342// installation does not run (the API, MCP, docs, g1t.page, the model
343// proxy, billing) are left off its page; a public repository of yours in
344// STATUS_PROBE_REPO adds the git check.
345{
346 const hosted = parseJsonc(readFileSync(join(root, "apps/status/wrangler.jsonc"), "utf8"));
347 const db = hosted.d1_databases[0];
348 write("g1t-status", {
349 name: hosted.name,
350 main: rel(join("apps/status", hosted.main)),
351 compatibility_date: hosted.compatibility_date,
352 rules: hosted.rules,
353 triggers: hosted.triggers,
354 d1_databases: [
355 {
356 binding: db.binding,
357 database_name: db.database_name,
358 database_id: db.database_id,
359 migrations_dir: rel(join("apps/status", db.migrations_dir)),
360 },
361 ],
362 vars: {
363 SITE_URL: (process.env.STATUS_CHECK_URL ?? "http://g1t:8787").replace(/\/$/, ""),
364 PUBLIC_SITE_URL: PUBLIC_URL,
365 API_URL: "",
366 MCP_URL: "",
367 DOCS_URL: "",
368 PAGES_URL: "",
369 MODELS_URL: "",
370 PROBE_REPO: process.env.STATUS_PROBE_REPO ?? "",
371 SUPPORT_URL: `${PUBLIC_URL}/support`,
372 OG_IMAGE: "",
373 // Its own address, for links made outside a request. No email
374 // binding here: subscribing by email is off, the feeds work.
375 STATUS_URL: `http://localhost:${process.env.STATUS_PORT ?? "8788"}`,
376 STATUS_ALERT_EMAIL: "",
377 },
378 });
379}
Running g1t yourself: the design, a docker compose proof, and a guide to what works today380console.log(`Wrote ${files.length} configs to ${out}`);

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