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.

344 linesCodeBlameRaw
1# Contributing to g1t
2
3g1t is built the way it asks others to build: work items 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- `npm run format -- <files>` on the TypeScript files you added
80 (`.prettierrc.json`). Older files are not formatted yet, so format a file
81 you only edited in a change of its own, not in the middle of a feature.
82- Look at what you changed in a browser. Screenshots catch what type
83 checks do not.
84
85## Checking g1t against GitHub
86
87g1t runs GitHub Actions workflows as GitHub does. The Conformance
88workflow is how that is checked: `.g1t/workflows/conformance.yml` and
89`.github/workflows/conformance.yml` are the same file (a test in
90`crates/actions` fails when they differ), and each run's
91`conformance-report` artifact records what its jobs saw: which `GITHUB_*`
92and `RUNNER_*` variables were set and of what kind, every key of the
93contexts and the event and its type, how the workspace is laid out, and
94the values of outputs, files, caches, artifacts and expressions that
95should be the same everywhere. It reads no secrets and runs only by hand.
96
971. Run it on both, at the same commit: **Actions → Conformance → Run
98 workflow** on GitHub and on g1t.sh.
992. Download each run's `conformance-report` artifact, and name the two
100 files `github.json` and `g1t.json`.
1013. Compare them:
102
103 ```sh
104 node scripts/conformance-diff.mjs github.json g1t.json
105 ```
106
107It lists what GitHub has that g1t lacks or has otherwise, then what only
108g1t has, and exits 1 while there are differences. A difference is a bug in
109g1t, or belongs in [What g1t can't do yet](https://docs.g1t.sh/about/limitations/).
110Add a step and a `CHECK_` value to the workflow for anything new g1t
111should match.
112
113## Deploying
114
115Pushes to `main` deploy themselves: `.g1t/workflows/deploy.yml` runs
116`scripts/deploy.mjs`, which deploys only the parts that changed, migrations
117first. Every deployable part is listed in `deploy/stack.jsonc`; a new service
118or app goes there, in the table on
119[How a self-hosted g1t runs](https://docs.g1t.sh/guides/self-hosting-architecture/#each-part),
120and in the one on [Services and contracts](https://docs.g1t.sh/architecture/services/#every-unit)
121(`npm run test:deploy` says what is missing).
122[Deploy g1t to Cloudflare](https://docs.g1t.sh/guides/deploy-to-cloudflare/)
123covers the tool, the workflow, rollbacks, adding a unit and first-time
124setup.
125
126- **Migrations run before the code**, so the old code reads the new schema
127 for a minute or more. Add tables and columns; change what rows mean in
128 two deploys (code that reads both forms first); never drop what live code
129 still reads.
130- **The self-hosted runner** is released, not deployed: bump `version` in
131 `crates/runner/Cargo.toml`, merge, and push a `runner-v<version>` tag.
132 `.g1t/workflows/runner-release.yml` builds, signs and publishes it, and
133 runners update themselves to it. `scripts/runner-release.mjs` does the
134 same by hand.
135- **The g1t CLI** is released the same way: bump `version` in
136 `crates/g1t/Cargo.toml`, merge, and push a `cli-v<version>` tag.
137 `.g1t/workflows/cli-release.yml` builds it for every platform, signs
138 the release with the runner's release key and publishes it to
139 g1t.sh/downloads/cli/, where g1t.sh/install and g1t.sh/install.ps1
140 find it. `scripts/cli-release.mjs` does the same by hand. The command
141 reference in the docs is generated from the CLI
142 (`G1T_WRITE_DOCS=1 cargo test -p g1t cli_reference`), and its table of
143 API operations from the OpenAPI document
144 (`G1T_WRITE_OPS=1 cargo test -p g1t ops_table`); tests fail when either
145 falls behind. The guide is [The g1t CLI](https://docs.g1t.sh/guides/cli/).
146- **The desktop app** is released the same way: bump `version` in
147 `apps/desktop/src-tauri/Cargo.toml`, merge, and push a
148 `desktop-v<version>` tag. `.g1t/workflows/desktop-release.yml` builds
149 the installers (Linux on g1t's machines; Windows and macOS on machines
150 registered for them, or Windows cross-built on Linux, each switched on
151 by a repository variable the
152 workflow's header names; a platform with no machine is left out of that
153 release), signs the updater's files and publishes them, and installed
154 apps update themselves. `scripts/desktop-release.mjs` does the
155 same by hand. The app loads the live site, so a change to the site needs
156 no release; only a change under `apps/desktop` does. The guide is
157 [The desktop app](https://docs.g1t.sh/guides/desktop/).
158- **The phone app** for Android is released on g1t.sh/download while it
159 is not in Google Play (that waits on Flagon's D-U-N-S number): bump
160 `version` in `apps/mobile/app.config.ts` when its native side changes,
161 merge, and push a `mobile-v<version>` tag.
162 `.g1t/workflows/mobile-release.yml` has EAS build the APK and publishes
163 it; `scripts/mobile-release.mjs` does the same by hand. A change to the
164 app's code alone needs no release: `eas update --channel preview` in
165 `apps/mobile` reaches installed copies. Its server half (chat scopes,
166 `/-/notify`, notify's phone push) deploys with the site and services.
167 The guide is [The phone app](https://docs.g1t.sh/guides/mobile/).
168
169## How g1t's own CI caches
170
171g1t's workflows cache with `actions/cache` against g1t's own cache
172([the Actions guide](https://docs.g1t.sh/guides/actions/#the-cache) has its
173rules). Three of them shape everything here: a run restores from its own
174ref, its pull request's base, then `main`, so a release tag never reads
175another tag's entries; a key is written once; and a repository holds
17610 GiB, past which the entries restored longest ago go. So:
177
178- **`main` saves; pull requests and tags restore.** CI's Rust and desktop
179 jobs keep their caches on each push to `main`, and the Caches workflow
180 (`.g1t/workflows/caches.yml`) builds and saves what only releases use,
181 when its key is missing. Release workflows only restore.
182- **Rust caches are keyed by the toolchain and the dependencies**, from
183 `node scripts/deploy/ci-cache.mjs keys`: `rustc -V`, and a hash of the
184 packages `Cargo.lock` takes from crates.io and git. A release bumping a
185 workspace crate's version changes neither. A restore key never crosses
186 toolchains.
187- **A Cargo target restored from an older entry is pruned before it is
188 saved** (`ci-cache.mjs prune`), to what this build used.
189- **A partial install never saves a whole-install key.** npm's downloads
190 and the crates are saved only by jobs that fetch all of them.
191- **One set of paths per entry.** An entry's version is the hash of its
192 paths, so every step that names it, in every workflow, names the same
193 ones. `crates/actions/tests/repository_workflows.rs` holds all of this.
194- **sccache only reads on a tag** (`scripts/sccache.sh` sets
195 `SCCACHE_GHA_RW_MODE=READ_ONLY` when `GITHUB_REF_TYPE` is `tag`): a
196 release's run finds what `main`'s runs compiled, and saves nothing only
197 its own tag could read.
198- **Images a build runs in are pinned by digest.** The CLI's release
199 builds in `cargo-zigbuild` (`BUILDER` in `scripts/cli-release.mjs`), and
200 its cache is keyed by that digest (`zigbuild` from `ci-cache.mjs keys`)
201 rather than by this machine's `rustc`.
202
203| Entry | Key | Saved by | Restored by |
204| -------------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------- | --------------------------- |
205| npm's downloads | `npm-full-<os>-<package-lock.json>` | Caches (`npm`) | every npm install |
206| Crates and their index | `cargo-crates-<os>-<deps>` | CI Rust (`cargo fetch` of the whole lockfile) | Deploy, the runner release |
207| The test build | `cargo-test-<os>-<rust>-<deps>` | CI Rust | CI Rust on pull requests |
208| Workers' builds | `cargo-workers-<os>-<rust>-<group>-<deps>` | Deploy | Deploy |
209| The runner's image build | `runner-musl-<os>-<rust>-<deps>` | Deploy | Deploy |
210| worker-build and its tools | `worker-build-…`, `worker-build-tools-…` | Deploy | Deploy |
211| Compiled crates | sccache's, one per crate | every Rust job | every Rust job |
212| The desktop app's check | `desktop-check-<os>-<rust>-<desktop deps>` | CI desktop | CI desktop on pull requests |
213| apt for Tauri | `apt-tauri-<os>-<scripts/apt-install.sh>` | CI desktop | the desktop release |
214| The runner release | `runner-release-<os>-<target>-<rust>-<deps>`, zig | Caches (`runner`) | the runner release |
215| The desktop release | `desktop-release-linux-…`, `desktop-release-windows-…`, `xwin-…`, `apt-tauri-windows-…` | Caches (`desktop-linux`, `desktop-windows`) | the desktop release |
216| The CLI release | `cli-release-<os>-<zigbuild>-<deps>` (`target/zig`, every platform) | Caches (`cli`), built whole | the CLI release |
217
218To see whether a change helped, read a run's cache steps: `Cache restored
219from key` names the entry (the key itself is a hit; another is a start),
220`Cache Size` and the step's time say what it cost, and the post step says
221whether it saved. `sccache's hits and misses` is on each Rust job's summary.
222`g1t cache usage` says what the repository's entries hold against its
22310 GiB, and `g1t cache list --sort size-in-bytes` which take the most.
224
225## Adding a machine size
226
227g1t's hosted machines are one catalogue: `INSTANCE_TYPES` in
228`crates/contracts/src/actions.rs`, mirrored row for row in
229`packages/contracts/src/instances.ts`. Each row is a `runs-on` label, the
230machine it bills as (`machine`, such as `2-core`), the runner's Durable
231Object class that runs it (`class` / `sandboxClass`), the Cloudflare
232Containers instance type behind that class (`container`), its vCPUs,
233memory and disk, and its price a minute (`micros_per_minute`). The runner
234starts a job in its label's class, billing charges its minutes at its
235rate and reports them by machine, and the docs' tables follow it.
236
237- **A new label for an existing machine** (another name for the standard
238 one, say): a row with the new label and class and the machine's
239 `machine`, size and rate; the class and its binding below.
240- **A new size**: a row with a new `machine` key and its rate. The rate is
241 what a minute costs at Cloudflare's list prices with half the vCPUs
242 busy, plus 20%, rounded up to a tenth of a cent, and never under its
243 cost with every vCPU busy; `rates_cover_the_machine_at_full_cpu_and_its_usual_cost_plus_the_margin`
244 checks it.
245- **Pointing a label at another instance type**: change its `container`
246 and size (and `machine`, if it is a different machine now), and the
247 class's `instance_type` in `services/runner/wrangler.jsonc`. A custom
248 instance type works the same way, once the account may use them: add its
249 size to the runner test's table of instance types.
250- **Renaming a label**: change `label`; keep `machine`, so its minutes stay
251 on the same line of everyone's usage.
252
253Never reuse a `machine` key for a different machine: past usage is
254reported under it. For a new class, add it to `services/runner/wrangler.jsonc`
255(`containers`, a `durable_objects` binding and a migration tag with
256`new_sqlite_classes`), export it from `services/runner/src/index.ts`
257(`export class SandboxXCore extends AttemptSandbox {}`), and give it a
258binding in `SANDBOX_BINDINGS` (`services/runner/src/egress.ts`). The tests
259that hold all of this together are `cargo test -p g1t-contracts instance`
260(the table, its rates and its TypeScript mirror) and the runner's
261`npm test` (every class in wrangler.jsonc, its instance type, size and
262binding against the catalogue). Then update the machine tables in
263`guides/actions.md`, `guides/usage-and-billing.md` and
264`architecture/compute.mdx`.
265
266## Speed
267
268Pages are a few rounds of service calls, and each round costs a trip to
269the databases, so start calls together and add no rounds.
270
271- Every page answers with `Server-Timing`: each loader, each service's
272 calls and their database time. Look at it in DevTools when a page feels
273 slow.
274- `scripts/perf/measure.ps1` times pages from your machine (`-BrowserUA`
275 for signed-out pages as a browser sees them). A page that only reads
276 must not set the `g1t_d1` cookie: a new read-only service method goes in
277 `READS` in `apps/web/app/lib/perf.ts`.
278- Targets from the US: signed-out pages under 200 ms to first byte,
279 signed-in pages under 400 ms, streamed panels within a second.
280- Workers run without Smart Placement; `scripts/perf/placement-probe.mjs`
281 measures a placement before you pin one.
282- A TypeScript service reports its database time too (`db;dur`, from
283 `openD1` in `@g1t/contracts`): the round trips a method makes are what
284 it costs, so start them together (`Promise.all`), keep what never
285 changes across requests in the isolate (the artifacts service keeps a
286 workspace's slug for 30 s and whether its General space exists), and
287 don't wake a Durable Object for a read D1 can answer.
288- Nothing live gates a page. A page carries what it shows (a doc's text
289 and its saved document), opens its sockets as it hydrates rather than
290 from inside the code they feed, keeps what is typed until they answer,
291 and says so with a dot, never a spinner. A page opened with an access
292 token gets its socket tickets with the page (the root loader mints the
293 feed's, an artifact's loader its room's) rather than asking for each.
294- Code that only the browser runs (the doc editor) is still fetched with
295 the page: `vite.config.ts` notes the chunk and what it imports for the
296 route's `links`, and a link to such a page warms the chunk on hover.
297 Modules several routes share are grouped by who shares them
298 (`codeSplitting.groups`), so a page fetches tens of files, not a
299 hundred and more. Before this, opening a doc fetched 124 files
300 (1,049 KB) in three waves and the editor appeared at 2.8–3.3 s; a doc
301 now fetches 71 files (1,026 KB) in one wave, and against the deployed
302 services the editor appears at 2.1 s, of which the room's answer is
303 0.65 s and the editor's own start 0.4 s. With the saved document in the
304 page, measured on g1t.sh signed in (2026-10-10, warm, 1440×900), the
305 editor appears at 1.5 s: first byte 0.53 s, scripts by 0.75 s, the
306 editor's own start the rest. A doc without a saved document (one last
307 saved before documents were kept) waited for its room's first message
308 instead, 0.9 s after the socket opened, and appeared at 2.0–2.5 s; the
309 page now asks the room once and keeps the answer, so only its first
310 open pays. Still to cut: the live socket's four service calls before
311 the room answers, the page and sidebar's 27 database round trips in two
312 calls, the root loader's billing and identity calls (0.7–1.3 s and
313 0.45 s inside), and the 3.4 MB of script a doc page decodes.
314
315## Rate limits
316
317`RATE_LIMITS` in `packages/contracts/src/rate-limits.ts` is the table of
318record, and the [rate limits page](https://docs.g1t.sh/reference/rate-limits/)
319is the public one: change both, and the binding in the Worker's
320`wrangler.jsonc`, together (`front-door-limits.test.ts` fails when they
321disagree). Limits fail open, keys hash anything secret, and each Worker
322takes its namespace ids from its own block of a hundred (41xx packages,
32342xx web, 43xx repos, 44xx api, 45xx og, 46xx status).
324
325## Operating g1t.sh
326
327For Flagon staff.
328
329- **Incidents** are declared, updated and resolved in sudo, under
330 **Platform → Incidents**, and appear on status.g1t.sh within 30 seconds.
331 Declare as soon as people are affected, and pick the higher severity
332 when unsure. SEV1 (down for most people, or data at risk) gets an update
333 at least every 30 minutes and SEV2 (a core part broken for many) at least
334 hourly; both email subscribers and need a blameless postmortem within
335 five working days.
336- **An Artifacts outage:** `scripts/ops/gitstore-namespaces.mjs` shows a
337 failing namespace. After 15 minutes, serve it read-only from the fallback
338 git store (`scripts/ops/restore-to-gitstore.mjs restore`, then the repos
339 Worker's secret `GIT_FALLBACK_NAMESPACES`) and open an incident. To
340 switch back, `reconcile` anything the fallback took, then delete the
341 secret. The script's header has every step.
342- **Costs and margin**, credits and the platform pause are
343 in sudo, under **Costs & margin**. The code is in `services/billing/src`
344 (`costs.rs`, `margin.rs`, `pricing.rs`, `budget.rs`, `platform.rs`).