flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/deploy/self-host/configs.mjs

245 lines9,465 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;
12// - services that are off in this phase (agents, the context hub, the
13// g1t.page dispatcher, model proxy) are bound to workers/off instead, and
14// events stop queueing work for them;
15// - URLs that name g1t.sh name PUBLIC_URL instead, and billing is free.
16//
17// Usage: node configs.mjs [outDir]
18// Environment: PUBLIC_URL, GITSTORE_URL, GITSTORE_SECRET, MAIL_URL,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look19// ACTIONS_KEY, INTEGRATIONS_KEY, WEBHOOKS_KEY, IDENTITY_KEY, and optionally
20// your own GitHub App: GITHUB_APP_ID, GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID,
21// 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 today22//
23// The output is for `wrangler dev` (see start.sh): every Worker in one
24// workerd, the site first, with D1, KV and Queues kept on disk.
25
26import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
27import { dirname, join, relative, resolve } from "node:path";
28import { fileURLToPath } from "node:url";
29
30const here = dirname(fileURLToPath(import.meta.url));
31const root = resolve(here, "../..");
32const out = resolve(process.argv[2] ?? join(here, ".generated"));
33mkdirSync(out, { recursive: true });
34
35const PUBLIC_URL = (process.env.PUBLIC_URL ?? "http://localhost:8787").replace(/\/$/, "");
36
37/** Services that run, in the order Wrangler is given them (the site first). */
38export const RUNNING = [
39 { name: "g1t", dir: "apps/web", web: true },
40 { name: "g1t-identity", dir: "services/identity" },
41 { name: "g1t-repos", dir: "services/repos" },
42 { name: "g1t-work", dir: "services/work" },
43 { name: "g1t-events", dir: "services/events" },
44 { name: "g1t-projects", dir: "services/projects" },
45 { name: "g1t-search", dir: "services/search" },
46 { name: "g1t-billing", dir: "services/billing" },
47 { name: "g1t-security", dir: "services/security" },
48 { name: "g1t-actions", dir: "services/actions" },
49 { name: "g1t-webhooks", dir: "services/webhooks" },
50 { name: "g1t-integrations", dir: "services/integrations" },
51 { name: "g1t-deployments", dir: "services/deployments" },
52];
53
54/** Services that are off in phase 1, and what the off Worker calls them. */
55const OFF = {
56 "g1t-runner": "Agents",
57 "g1t-context": "Context search and memory",
58};
59
60/** Sealing keys, by the service that holds each (hosted: Wrangler secrets). */
61const SECRETS = {
62 "g1t-actions": "ACTIONS_KEY",
63 "g1t-integrations": "INTEGRATIONS_KEY",
64 "g1t-webhooks": "WEBHOOKS_KEY",
65};
66
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look67/**
68 * g1t.sh's GitHub App is its own: an installation registers one of its
69 * own, or has none, and then no GitHub buttons appear. Its public settings
70 * replace the hosted vars; its secrets go only to the service that uses each.
71 */
72const GITHUB_VARS = ["GITHUB_APP_ID", "GITHUB_APP_SLUG", "GITHUB_APP_CLIENT_ID"];
73const GITHUB_SECRETS = {
74 "g1t-identity": ["GITHUB_APP_CLIENT_SECRET", "IDENTITY_KEY", "REGISTRATION_MODE"],
75 "g1t-integrations": ["GITHUB_APP_PRIVATE_KEY", "GITHUB_APP_WEBHOOK_SECRET"],
76};
77
Running g1t yourself: the design, a docker compose proof, and a guide to what works today78/** Queues whose consumers are off: events stops sending to them. */
79const OFF_QUEUES = new Set(["g1t-events-runner", "g1t-events-context"]);
80
81/** Strips comments and trailing commas from JSONC. Strings are respected. */
82function parseJsonc(text) {
83 let result = "";
84 let inString = false;
85 for (let i = 0; i < text.length; i++) {
86 const char = text[i];
87 if (inString) {
88 result += char;
89 if (char === "\\") result += text[++i];
90 else if (char === '"') inString = false;
91 } else if (char === '"') {
92 inString = true;
93 result += char;
94 } else if (char === "/" && text[i + 1] === "/") {
95 while (i < text.length && text[i] !== "\n") i++;
96 result += "\n";
97 } else if (char === "/" && text[i + 1] === "*") {
98 i = text.indexOf("*/", i + 2) + 1;
99 } else {
100 result += char;
101 }
102 }
103 return JSON.parse(result.replace(/,(\s*[}\]])/g, "$1"));
104}
105
106const rel = (path) => relative(out, resolve(root, path)).replaceAll("\\", "/");
107
108function hostedUrl(value) {
109 return typeof value === "string" ? value.replace(/https:\/\/(api\.)?g1t\.sh/g, PUBLIC_URL) : value;
110}
111
112function selfHosted(service) {
113 const hosted = parseJsonc(readFileSync(join(root, service.dir, "wrangler.jsonc"), "utf8"));
114 const config = {
115 name: hosted.name,
116 compatibility_date: hosted.compatibility_date,
117 compatibility_flags: hosted.compatibility_flags,
118 rules: hosted.rules,
119 vars: {},
120 };
121
122 if (service.web) {
123 // The site as React Router built it (apps/web/build), not its sources.
124 config.main = rel(`${service.dir}/build/server/index.js`);
125 config.no_bundle = true;
126 config.rules = [{ type: "ESModule", globs: ["**/*.js", "**/*.mjs"] }];
127 config.assets = { directory: rel(`${service.dir}/build/client`) };
128 } else {
129 config.main = rel(join(service.dir, hosted.main));
130 }
131
132 for (const [key, value] of Object.entries(hosted.vars ?? {})) config.vars[key] = hostedUrl(value);
133
134 if (hosted.d1_databases) {
135 config.d1_databases = hosted.d1_databases.map((db) => ({
136 binding: db.binding,
137 database_name: db.database_name,
138 database_id: db.database_id,
139 migrations_dir: rel(join(service.dir, db.migrations_dir ?? "migrations")),
140 }));
141 }
142 if (hosted.kv_namespaces) config.kv_namespaces = hosted.kv_namespaces.map(({ binding, id }) => ({ binding, id }));
143 if (hosted.triggers) config.triggers = hosted.triggers;
144
145 if (hosted.queues) {
146 config.queues = {};
147 if (hosted.queues.producers) {
148 config.queues.producers = hosted.queues.producers.filter((producer) => !OFF_QUEUES.has(producer.queue));
149 }
150 if (hosted.queues.consumers) config.queues.consumers = hosted.queues.consumers;
151 }
152
153 config.services = (hosted.services ?? []).map((binding) =>
154 OFF[binding.service] ? { binding: binding.binding, service: offName(binding.service) } : binding,
155 );
156
157 // Cloudflare-only bindings, and what stands in for them.
158 if (hosted.artifacts) {
159 for (const artifacts of hosted.artifacts) {
160 config.services.push({ binding: artifacts.binding, service: "g1t-artifacts" });
161 }
162 }
163 if (hosted.send_email) {
164 for (const email of hosted.send_email) config.services.push({ binding: email.name, service: "g1t-mail" });
165 }
166
167 // Secrets the hosted services hold, given here from the environment, each
168 // only to the service that uses it.
169 const secret = SECRETS[hosted.name];
170 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 look171 for (const name of GITHUB_VARS) {
172 if (name in config.vars) config.vars[name] = process.env[name] ?? "";
173 }
174 for (const name of GITHUB_SECRETS[hosted.name] ?? []) {
175 if (process.env[name]) config.vars[name] = process.env[name];
176 }
Running g1t yourself: the design, a docker compose proof, and a guide to what works today177
178 // Self-hosted g1t charges nothing: billing records usage at cost and never
179 // stops work for it.
180 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 look181 // Anyone may register on an installation of your own unless you set
182 // REGISTRATION_MODE=invite; invites then work as on g1t.sh, and the owners
183 // of INVITE_STAFF_WORKSPACES (yours, not g1t.sh's) invite without limit.
184 if (hosted.name === "g1t-identity") {
185 config.vars.REGISTRATION_MODE = process.env.REGISTRATION_MODE || "open";
186 config.vars.INVITE_STAFF_WORKSPACES = process.env.INVITE_STAFF_WORKSPACES ?? "";
187 if (process.env.INVITES_PER_USER) config.vars.INVITES_PER_USER = process.env.INVITES_PER_USER;
188 }
Running g1t yourself: the design, a docker compose proof, and a guide to what works today189 // Nothing to deploy to: deployments are off (no Cloudflare API token).
190 if (hosted.name === "g1t-deployments") delete config.vars.CUSTOM_HOSTNAMES_ZONE_ID;
191
192 return config;
193}
194
195function offName(service) {
196 return `${service}-off`;
197}
198
199function write(name, config) {
200 const path = join(out, `${name}.json`);
201 writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`);
202 return path;
203}
204
205const files = [];
206for (const service of RUNNING) files.push(write(service.name, selfHosted(service)));
207
208const compatibility_date = "2026-09-26";
209files.push(
210 write("g1t-artifacts", {
211 name: "g1t-artifacts",
212 main: rel("deploy/self-host/workers/artifacts/index.js"),
213 compatibility_date,
214 vars: {
215 GITSTORE_URL: process.env.GITSTORE_URL ?? "http://gitstore:8080",
216 GITSTORE_SECRET: process.env.GITSTORE_SECRET ?? "",
217 },
218 }),
219);
220files.push(
221 write("g1t-mail", {
222 name: "g1t-mail",
223 main: rel("deploy/self-host/workers/mail/index.js"),
224 compatibility_date,
225 vars: {
226 PUBLIC_URL,
227 MAIL_URL: process.env.MAIL_URL ?? "",
228 MAIL_FROM: process.env.MAIL_FROM ?? "",
229 },
230 }),
231);
232for (const [service, feature] of Object.entries(OFF)) {
233 files.push(
234 write(offName(service), {
235 name: offName(service),
236 main: rel("deploy/self-host/workers/off/index.js"),
237 compatibility_date,
238 vars: { OFF_NAME: feature },
239 }),
240 );
241}
242
243// The order Wrangler takes them in: the site first, as the one that serves.
244writeFileSync(join(out, "workers.txt"), `${files.map((file) => relative(out, file)).join("\n")}\n`);
245console.log(`Wrote ${files.length} configs to ${out}`);