Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
341 linesCodeBlameRaw
1# Contributing to g1t
2
3g1t is built the way it asks others to build: issues and pull requests on
4[g1t.sh/flagon-io/g1t](https://g1t.sh/flagon-io/g1t), required checks that prove
5a change works, and a merge queue that keeps `main` passing.
6
7## A change ships with its docs
8
9Documentation is part of the product, held to the same bar as the code.
10A pull request that changes what someone can do, see or call changes the
11docs in the same pull request:
12
13| If you change | Update |
14| --- | --- |
15| Something a person does on g1t.sh | The guide for it in `apps/docs/src/content/docs/guides/` |
16| An API route, a field, or an MCP tool | `apps/api/src/operations.rs` descriptions (they feed the OpenAPI document and the API reference), the example and notes for it in `apps/api/src/reference.json`, `reference/mcp.md`, and any guide that shows the call. Then refresh the docs' copy of the OpenAPI document with `G1T_WRITE_OPENAPI=1 cargo test -p g1t-api openapi`; `cargo test` fails until you do. |
17| What coding agents are told about g1t | `guides/bring-your-own-agent.mdx`, `guides/claude-code-plugin.md`, the plugin's skills in `plugins/claude-code/`, and `apps/web/public/llms.txt` |
18| The Claude Code plugin's files (`plugins/claude-code/`, except `test/`) | Its version, the same in `.claude-plugin/plugin.json` and `.claude-plugin/marketplace.json`: Claude Code updates an installed plugin only when it changes. On main, `plugins-release.yml` publishes it to `flagon-io/g1t-plugins`, and refuses a changed plugin with a version already published. Never commit to g1t-plugins itself. |
19| Settings, limits or prices | The page that names them, and the table it is in |
20| A new feature | A section in the guide that owns it, linked from the docs home if it is a new task |
21| How g1t is put together: a service, a binding, a store, an event route, the path of a request | Its page in the docs' Architecture section (`apps/docs/src/content/docs/architecture/`), and its diagram in `apps/docs/src/diagrams/architecture.ts`. [How g1t ships](https://docs.g1t.sh/architecture/shipping/#keeping-this-section-true) says which page covers what. |
22
23The style, in short: plain sentences, second person, no marketing words,
24sentence-case headings, a numbered list for steps, a table for options, and
25every API mention with its exact route or tool name. Examples are copyable
26and real. Nothing is documented that the code does not do.
27
28Check the docs build before you push:
29
30```sh
31cd apps/docs && npm run build
32```
33
34## Icons
35
36- **Interface icons come from [Lucide](https://lucide.dev/icons).** In the
37 apps, import them from `lucide-react`. In the docs, use `@lucide/astro`:
38 import an icon component, or give `apps/docs/src/components/Card.astro`
39 and `Aside.astro` a Lucide name such as `icon="git-branch"`. Starlight's
40 own `<Card>`, `<LinkCard>` and `<Aside>` draw Starlight's icon set, so the
41 docs import the wrappers in `apps/docs/src/components/` instead.
42- **Brand marks come from [Simple Icons](https://simpleicons.org)**
43 (`simple-icons`), and follow each brand's own usage guidelines. Add the
44 dependency with the first one you show.
45- **The g1t logo and illustrations are our own artwork**, in
46 `apps/web/app/components/logo.tsx` and `art.tsx`.
47- Never hand-copy an icon's SVG paths into a file. If Lucide lacks the
48 icon you need, pick the closest one that it has.
49
50## Interface components
51
52- **Build the site from the components in `apps/web/app/components/ui`**,
53 which are [shadcn/ui](https://ui.shadcn.com) components themed with
54 g1t's tokens: buttons, cards, badges, tables, tabs, separators,
55 breadcrumbs, menus, dialogs, sheets, tooltips, form controls.
56- **If shadcn has a component you need and `ui` doesn't, add it there**
57 first, themed with the tokens, then use it. Don't rebuild it inline.
58- **Change how something looks through the component's variants**, not
59 with a one-off class string at the call site. A long `className` on a
60 `div` that draws a card, a pill or a button is a component that should
61 exist.
62- Hover hints use the `Tooltip` component, never `title=`. Native
63 `<select>`, file inputs, checkboxes and radios use their `ui`
64 components.
65- **Every avatar is `Avatar`** (`ui/avatar.tsx`), and @g1t's is `Avatar`
66 with `agent` set, or `AgentAvatar` (`components/agent-avatar.tsx`), which
67 wraps it: g1t's pixel 1 with the agent marker in its corner, named
68 "…, agent" to a screen reader. Never hand-draw a sparkle, a letter on a
69 tile or the marker at a call site: a place that shows @g1t differently is
70 a bug.
71- Look at every page you change, in light and dark, on a desktop and a
72 phone, before calling it done.
73
74## Before you push
75
76- `cargo test` in the crate or service you changed.
77- `npx tsc -b --force` in `apps/web` (the incremental build misses changes
78 in `packages/contracts`).
79- Look at what you changed in a browser. Screenshots catch what type
80 checks do not.
81
82## Checking g1t against GitHub
83
84g1t runs GitHub Actions workflows as GitHub does. The Conformance
85workflow is how that is checked: `.g1t/workflows/conformance.yml` and
86`.github/workflows/conformance.yml` are the same file (a test in
87`crates/actions` fails when they differ), and each run's
88`conformance-report` artifact records what its jobs saw: which `GITHUB_*`
89and `RUNNER_*` variables were set and of what kind, every key of the
90contexts and the event and its type, how the workspace is laid out, and
91the values of outputs, files, caches, artifacts and expressions that
92should be the same everywhere. It reads no secrets and runs only by hand.
93
941. Run it on both, at the same commit: **Actions → Conformance → Run
95 workflow** on GitHub and on g1t.sh.
962. Download each run's `conformance-report` artifact, and name the two
97 files `github.json` and `g1t.json`.
983. Compare them:
99
100 ```sh
101 node scripts/conformance-diff.mjs github.json g1t.json
102 ```
103
104It lists what GitHub has that g1t lacks or has otherwise, then what only
105g1t has, and exits 1 while there are differences. A difference is a bug in
106g1t, or belongs in [What g1t can't do yet](https://docs.g1t.sh/about/limitations/).
107Add a step and a `CHECK_` value to the workflow for anything new g1t
108should match.
109
110## Deploying
111
112Pushes to `main` deploy themselves: `.g1t/workflows/deploy.yml` runs
113`scripts/deploy.mjs`, which deploys only the parts that changed, migrations
114first. Every deployable part is listed in `deploy/stack.jsonc`; a new service
115or app goes there, in the table on
116[How a self-hosted g1t runs](https://docs.g1t.sh/guides/self-hosting-architecture/#each-part),
117and in the one on [Services and contracts](https://docs.g1t.sh/architecture/services/#every-unit)
118(`npm run test:deploy` says what is missing).
119[Deploy g1t to Cloudflare](https://docs.g1t.sh/guides/deploy-to-cloudflare/)
120covers the tool, the workflow, rollbacks, adding a unit and first-time
121setup.
122
123- **Migrations run before the code**, so the old code reads the new schema
124 for a minute or more. Add tables and columns; change what rows mean in
125 two deploys (code that reads both forms first); never drop what live code
126 still reads.
127- **The self-hosted runner** is released, not deployed: bump `version` in
128 `crates/runner/Cargo.toml`, merge, and push a `runner-v<version>` tag.
129 `.g1t/workflows/runner-release.yml` builds, signs and publishes it, and
130 runners update themselves to it. `scripts/runner-release.mjs` does the
131 same by hand.
132- **The g1t CLI** is released the same way: bump `version` in
133 `crates/g1t/Cargo.toml`, merge, and push a `cli-v<version>` tag.
134 `.g1t/workflows/cli-release.yml` builds it for every platform, signs
135 the release with the runner's release key and publishes it to
136 g1t.sh/downloads/cli/, where g1t.sh/install and g1t.sh/install.ps1
137 find it. `scripts/cli-release.mjs` does the same by hand. The command
138 reference in the docs is generated from the CLI
139 (`G1T_WRITE_DOCS=1 cargo test -p g1t cli_reference`), and its table of
140 API operations from the OpenAPI document
141 (`G1T_WRITE_OPS=1 cargo test -p g1t ops_table`); tests fail when either
142 falls behind. The guide is [The g1t CLI](https://docs.g1t.sh/guides/cli/).
143- **The desktop app** is released the same way: bump `version` in
144 `apps/desktop/src-tauri/Cargo.toml`, merge, and push a
145 `desktop-v<version>` tag. `.g1t/workflows/desktop-release.yml` builds
146 the installers (Linux on g1t's machines; Windows and macOS on machines
147 registered for them, or Windows cross-built on Linux, each switched on
148 by a repository variable the
149 workflow's header names; a platform with no machine is left out of that
150 release), signs the updater's files and publishes them, and installed
151 apps update themselves. `scripts/desktop-release.mjs` does the
152 same by hand. The app loads the live site, so a change to the site needs
153 no release; only a change under `apps/desktop` does. The guide is
154 [The desktop app](https://docs.g1t.sh/guides/desktop/).
155- **The phone app** for Android is released on g1t.sh/download while it
156 is not in Google Play (that waits on Flagon's D-U-N-S number): bump
157 `version` in `apps/mobile/app.config.ts` when its native side changes,
158 merge, and push a `mobile-v<version>` tag.
159 `.g1t/workflows/mobile-release.yml` has EAS build the APK and publishes
160 it; `scripts/mobile-release.mjs` does the same by hand. A change to the
161 app's code alone needs no release: `eas update --channel preview` in
162 `apps/mobile` reaches installed copies. Its server half (chat scopes,
163 `/-/notify`, notify's phone push) deploys with the site and services.
164 The guide is [The phone app](https://docs.g1t.sh/guides/mobile/).
165
166## How g1t's own CI caches
167
168g1t's workflows cache with `actions/cache` against g1t's own cache
169([the Actions guide](https://docs.g1t.sh/guides/actions/#the-cache) has its
170rules). Three of them shape everything here: a run restores from its own
171ref, its pull request's base, then `main`, so a release tag never reads
172another tag's entries; a key is written once; and a repository holds
17310 GiB, past which the entries restored longest ago go. So:
174
175- **`main` saves; pull requests and tags restore.** CI's Rust and desktop
176 jobs keep their caches on each push to `main`, and the Caches workflow
177 (`.g1t/workflows/caches.yml`) builds and saves what only releases use,
178 when its key is missing. Release workflows only restore.
179- **Rust caches are keyed by the toolchain and the dependencies**, from
180 `node scripts/deploy/ci-cache.mjs keys`: `rustc -V`, and a hash of the
181 packages `Cargo.lock` takes from crates.io and git. A release bumping a
182 workspace crate's version changes neither. A restore key never crosses
183 toolchains.
184- **A Cargo target restored from an older entry is pruned before it is
185 saved** (`ci-cache.mjs prune`), to what this build used.
186- **A partial install never saves a whole-install key.** npm's downloads
187 and the crates are saved only by jobs that fetch all of them.
188- **One set of paths per entry.** An entry's version is the hash of its
189 paths, so every step that names it, in every workflow, names the same
190 ones. `crates/actions/tests/repository_workflows.rs` holds all of this.
191- **sccache only reads on a tag** (`scripts/sccache.sh` sets
192 `SCCACHE_GHA_RW_MODE=READ_ONLY` when `GITHUB_REF_TYPE` is `tag`): a
193 release's run finds what `main`'s runs compiled, and saves nothing only
194 its own tag could read.
195- **Images a build runs in are pinned by digest.** The CLI's release
196 builds in `cargo-zigbuild` (`BUILDER` in `scripts/cli-release.mjs`), and
197 its cache is keyed by that digest (`zigbuild` from `ci-cache.mjs keys`)
198 rather than by this machine's `rustc`.
199
200| Entry | Key | Saved by | Restored by |
201| --- | --- | --- | --- |
202| npm's downloads | `npm-full-<os>-<package-lock.json>` | Caches (`npm`) | every npm install |
203| Crates and their index | `cargo-crates-<os>-<deps>` | CI Rust (`cargo fetch` of the whole lockfile) | Deploy, the runner release |
204| The test build | `cargo-test-<os>-<rust>-<deps>` | CI Rust | CI Rust on pull requests |
205| Workers' builds | `cargo-workers-<os>-<rust>-<group>-<deps>` | Deploy | Deploy |
206| The runner's image build | `runner-musl-<os>-<rust>-<deps>` | Deploy | Deploy |
207| worker-build and its tools | `worker-build-…`, `worker-build-tools-…` | Deploy | Deploy |
208| Compiled crates | sccache's, one per crate | every Rust job | every Rust job |
209| The desktop app's check | `desktop-check-<os>-<rust>-<desktop deps>` | CI desktop | CI desktop on pull requests |
210| apt for Tauri | `apt-tauri-<os>-<scripts/apt-install.sh>` | CI desktop | the desktop release |
211| The runner release | `runner-release-<os>-<target>-<rust>-<deps>`, zig | Caches (`runner`) | the runner release |
212| The desktop release | `desktop-release-linux-…`, `desktop-release-windows-…`, `xwin-…`, `apt-tauri-windows-…` | Caches (`desktop-linux`, `desktop-windows`) | the desktop release |
213| The CLI release | `cli-release-<os>-<zigbuild>-<deps>` (`target/zig`, every platform) | Caches (`cli`), built whole | the CLI release |
214
215To see whether a change helped, read a run's cache steps: `Cache restored
216from key` names the entry (the key itself is a hit; another is a start),
217`Cache Size` and the step's time say what it cost, and the post step says
218whether it saved. `sccache's hits and misses` is on each Rust job's summary.
219`g1t cache usage` says what the repository's entries hold against its
22010 GiB, and `g1t cache list --sort size-in-bytes` which take the most.
221
222## Adding a machine size
223
224g1t's hosted machines are one catalogue: `INSTANCE_TYPES` in
225`crates/contracts/src/actions.rs`, mirrored row for row in
226`packages/contracts/src/instances.ts`. Each row is a `runs-on` label, the
227machine it bills as (`machine`, such as `2-core`), the runner's Durable
228Object class that runs it (`class` / `sandboxClass`), the Cloudflare
229Containers instance type behind that class (`container`), its vCPUs,
230memory and disk, and its price a minute (`micros_per_minute`). The runner
231starts a job in its label's class, billing charges its minutes at its
232rate and reports them by machine, and the docs' tables follow it.
233
234- **A new label for an existing machine** (another name for the standard
235 one, say): a row with the new label and class and the machine's
236 `machine`, size and rate; the class and its binding below.
237- **A new size**: a row with a new `machine` key and its rate. The rate is
238 what a minute costs at Cloudflare's list prices with half the vCPUs
239 busy, plus 20%, rounded up to a tenth of a cent, and never under its
240 cost with every vCPU busy; `rates_cover_the_machine_at_full_cpu_and_its_usual_cost_plus_the_margin`
241 checks it.
242- **Pointing a label at another instance type**: change its `container`
243 and size (and `machine`, if it is a different machine now), and the
244 class's `instance_type` in `services/runner/wrangler.jsonc`. A custom
245 instance type works the same way, once the account may use them: add its
246 size to the runner test's table of instance types.
247- **Renaming a label**: change `label`; keep `machine`, so its minutes stay
248 on the same line of everyone's usage.
249
250Never reuse a `machine` key for a different machine: past usage is
251reported under it. For a new class, add it to `services/runner/wrangler.jsonc`
252(`containers`, a `durable_objects` binding and a migration tag with
253`new_sqlite_classes`), export it from `services/runner/src/index.ts`
254(`export class SandboxXCore extends AttemptSandbox {}`), and give it a
255binding in `SANDBOX_BINDINGS` (`services/runner/src/egress.ts`). The tests
256that hold all of this together are `cargo test -p g1t-contracts instance`
257(the table, its rates and its TypeScript mirror) and the runner's
258`npm test` (every class in wrangler.jsonc, its instance type, size and
259binding against the catalogue). Then update the machine tables in
260`guides/actions.md`, `guides/usage-and-billing.md` and
261`architecture/compute.mdx`.
262
263## Speed
264
265Pages are a few rounds of service calls, and each round costs a trip to
266the databases, so start calls together and add no rounds.
267
268- Every page answers with `Server-Timing`: each loader, each service's
269 calls and their database time. Look at it in DevTools when a page feels
270 slow.
271- `scripts/perf/measure.ps1` times pages from your machine (`-BrowserUA`
272 for signed-out pages as a browser sees them). A page that only reads
273 must not set the `g1t_d1` cookie: a new read-only service method goes in
274 `READS` in `apps/web/app/lib/perf.ts`.
275- Targets from the US: signed-out pages under 200 ms to first byte,
276 signed-in pages under 400 ms, streamed panels within a second.
277- Workers run without Smart Placement; `scripts/perf/placement-probe.mjs`
278 measures a placement before you pin one.
279- A TypeScript service reports its database time too (`db;dur`, from
280 `openD1` in `@g1t/contracts`): the round trips a method makes are what
281 it costs, so start them together (`Promise.all`), keep what never
282 changes across requests in the isolate (the artifacts service keeps a
283 workspace's slug for 30 s and whether its General space exists), and
284 don't wake a Durable Object for a read D1 can answer.
285- Nothing live gates a page. A page carries what it shows (a doc's text
286 and its saved document), opens its sockets as it hydrates rather than
287 from inside the code they feed, keeps what is typed until they answer,
288 and says so with a dot, never a spinner. A page opened with an access
289 token gets its socket tickets with the page (the root loader mints the
290 feed's, an artifact's loader its room's) rather than asking for each.
291- Code that only the browser runs (the doc editor) is still fetched with
292 the page: `vite.config.ts` notes the chunk and what it imports for the
293 route's `links`, and a link to such a page warms the chunk on hover.
294 Modules several routes share are grouped by who shares them
295 (`codeSplitting.groups`), so a page fetches tens of files, not a
296 hundred and more. Before this, opening a doc fetched 124 files
297 (1,049 KB) in three waves and the editor appeared at 2.8–3.3 s; a doc
298 now fetches 71 files (1,026 KB) in one wave, and against the deployed
299 services the editor appears at 2.1 s, of which the room's answer is
300 0.65 s and the editor's own start 0.4 s. With the saved document in the
301 page, measured on g1t.sh signed in (2026-10-10, warm, 1440×900), the
302 editor appears at 1.5 s: first byte 0.53 s, scripts by 0.75 s, the
303 editor's own start the rest. A doc without a saved document (one last
304 saved before documents were kept) waited for its room's first message
305 instead, 0.9 s after the socket opened, and appeared at 2.0–2.5 s; the
306 page now asks the room once and keeps the answer, so only its first
307 open pays. Still to cut: the live socket's four service calls before
308 the room answers, the page and sidebar's 27 database round trips in two
309 calls, the root loader's billing and identity calls (0.7–1.3 s and
310 0.45 s inside), and the 3.4 MB of script a doc page decodes.
311
312## Rate limits
313
314`RATE_LIMITS` in `packages/contracts/src/rate-limits.ts` is the table of
315record, and the [rate limits page](https://docs.g1t.sh/reference/rate-limits/)
316is the public one: change both, and the binding in the Worker's
317`wrangler.jsonc`, together (`front-door-limits.test.ts` fails when they
318disagree). Limits fail open, keys hash anything secret, and each Worker
319takes its namespace ids from its own block of a hundred (41xx packages,
32042xx web, 43xx repos, 44xx api, 45xx og, 46xx status).
321
322## Operating g1t.sh
323
324For Flagon staff.
325
326- **Incidents** are declared, updated and resolved in sudo, under
327 **Platform → Incidents**, and appear on status.g1t.sh within 30 seconds.
328 Declare as soon as people are affected, and pick the higher severity
329 when unsure. SEV1 (down for most people, or data at risk) gets an update
330 at least every 30 minutes and SEV2 (a core part broken for many) at least
331 hourly; both email subscribers and need a blameless postmortem within
332 five working days.
333- **An Artifacts outage:** `scripts/ops/gitstore-namespaces.mjs` shows a
334 failing namespace. After 15 minutes, serve it read-only from the fallback
335 git store (`scripts/ops/restore-to-gitstore.mjs restore`, then the repos
336 Worker's secret `GIT_FALLBACK_NAMESPACES`) and open an incident. To
337 switch back, `reconcile` anything the fallback took, then delete the
338 secret. The script's header has every step.
339- **Costs and margin**, credits and the platform pause are
340 in sudo, under **Costs & margin**. The code is in `services/billing/src`
341 (`costs.rs`, `margin.rs`, `pricing.rs`, `budget.rs`, `platform.rs`).