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