Skip to content
131 linesCodeBlameRaw

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.

Merge branch 'mirroring' into artifacts-mode1# Mirroring: keeping remotes in sync
2
3> **2026-10-08:** "bidirectional synchronization from GitHub or whatever
4> providers … as well as setting up a different remote (think self-hosted g1t
5> and mirroring to the hosted g1t.sh platform)". "GitHub is down, I want to
6> run GitHub's workflows on g1t, but only until GitHub is back up. But what if
7> I push on g1t?" "It needs to be highly clear when a repository is mirrored,
8> and things are disabled." "When it's mirroring it's just mirroring … you
9> can use g1t as a disaster recovery option intentionally." "We really don't
10> want to force someone's hand." "When we've decided that we've moved the
11> repository entirely to g1t and it's no longer a mirror … mention that we'll
12> no longer track the remote."
13
14The user-facing guide is `apps/docs/src/content/docs/guides/mirroring.md`.
15This is the design record.
16
17## The rule
18
19**Every linked repository has exactly one leader. Work happens on the
20leader; the others follow.** g1t is never a second place where people work
21while the remote also leads, so there's nothing to merge back by surprise.
22Every state is an answer to "who leads right now?":
23
24| State | Leader | On g1t |
25| --- | --- | --- |
26| `standby` | remote | Exact, read-only copy. Runs nothing (optional: keep CI warm). |
27| `ci` | remote (code) | The remote's workflows run on g1t; results stay on g1t for now. |
28| `takeover` | g1t, for now | Everything works. Deploying workflows wait for approval. |
29| `handing_back` | moving back | Read-only while refs move. |
30| moved to g1t | g1t | Not a mirror. g1t no longer tracks the remote, or keeps it as a follower. |
31| `following` / `stuck` | g1t | A follower remote, pushed to on every `git.push`. |
32
33Outside a takeover, every commit on a mirror's branches is already on the
34remote. So a standby mirror is safe to treat as a backup, and any
35difference between the two is a bug.
36
37## Decisions taken
38
391. **A mirror is a standby by default.** No issues, pull requests, agents,
40 workflows or deployments, so mirroring is just mirroring. Settings,
41 rules, webhooks and reported checks stay open.
422. **Detection shows; it doesn't act.** Hosts are probed every minute. A
43 host is unreachable after 3 failures over at least 2 minutes, and back
44 after 3 successes over at least 5. By default this only turns the
45 repository's dot amber and offers **Take over**. Telling owners in the
46 inbox, and taking over automatically after N minutes, are opt-in
47 settings per link.
483. **Take over any time**, not only during a detected outage.
494. **Hand back is a reviewed plan**, ref by ref, against the commit each
50 ref had when the takeover began:
51 - `push`: only g1t moved.
52 - `fetch`: only the remote moved.
53 - `pull_request`: the remote protects the branch; the commits go to
54 `g1t/handback/<branch>` and g1t follows the remote's branch.
55 - `diverged`: both moved; a person decides keep ours, keep theirs or
56 pull request.
57
58 If anything is refused, g1t keeps the lead and says why. An automatic
59 hand-back happens only for takeovers g1t started itself, and only when
60 no ref has diverged.
615. **Nothing is lost silently.** When a pull would drop a commit (a
62 force-push or deletion on the remote, or a keep-theirs decision), that
63 commit is kept at `refs/g1t/replaced/<ref>/<ms>` for at least 30 days.
646. **CI failover is its own switch.** It runs `.github/workflows` as well
65 as `.g1t/workflows`; g1t's wins a `name:`.
667. **Move to g1t** is permanent. The confirmation says g1t will no longer
67 track the remote. Optionally the remote becomes a follower.
688. **Security.**
69 - A push copied in that changes `.g1t/`, `.github/workflows/` or
70 `.github/actions/` starts runs that wait for approval before they can
71 use secrets.
72 - Agents never move a repository to g1t, nor add, change or remove a
73 remote.
74
75## Where it lives
76
77- **`crates/contracts/src/mirrors.rs`** (and `packages/contracts/src/mirrors.ts`):
78 - the model: `MirrorState`, `RepoMirror` on `Repo`, `Remote`,
79 `MirrorSettings`;
80 - the hand-back logic: `ref_action` and `HandbackPlan`;
81 - `MirrorEvent`.
82- **repos** (`src/mirror.rs`, migration 0018 `repos.mirror`):
83 - `set_mirror`;
84 - `mirror`, which now also keeps replaced commits and announces pushes
85 as `mirrored`;
86 - `mirror_refs`, which returns g1t's side alone when there is no URL;
87 - `mirror_apply`, which moves named refs with a compare-and-swap;
88 - `read_only_refusal` at every write path: the git front door, the
89 commit API, merges, catch-up, releases and pull-request copies.
90- **integrations** (`src/remotes.rs`, migration 0007 `remotes`,
91 `remote_refs`, `remote_hosts`; cron every minute; `EVENTS` binding):
92 - the links;
93 - the provider adapters: `github` through the App, `g1t` and `git` with
94 a sealed token, and polling every 5 minutes for hosts without webhooks;
95 - takeover, CI, hand-back, move-in, health and the automatic levers.
96 - `github_repos` mirror and push rows became remotes in the migration;
97 the GitHub webhook's pushes go to remotes.
98- **work**: `retired::writable` refuses on a read-only mirror;
99 `not_archived` is kept for settings, rulesets and commit checks.
100- **actions** (`src/mirrored.rs`): what runs per state, `.github` reading,
101 holding deploys, approval for copied workflow changes.
102- **deployments**: production deploys only while g1t leads.
103- **events**: `mirror.*` in the inbox (only the people named in `notify`)
104 and in the webhook catalogue.
105- **api** (`src/mirrors.rs`):
106 - REST under `/repos/{owner}/{name}/mirror…`;
107 - MCP `mirror_*` actions on the `repository` tool;
108 - scopes: `repo:read` to read, `code:write` to sync, `repo:admin` for
109 everything else.
110- **web**: the badge, banner, disabled states, clone note and
111 **Settings → Mirroring**.
112
113## Not yet
114
115- **Check runs posted back to GitHub** during CI failover. Needs
116 `checks: write` on the App.
117- **Starting CI failover automatically** when GitHub starts no check suite
118 for a pushed commit. Needs `checks: read`.
119- **Copying GitHub's branch protection** into g1t during a takeover. Today
120 a takeover runs under g1t's own rules.
121- **Locking the remote** with a ruleset so only g1t pushes. Needs
122 `administration: write`.
123- **GitHub's pull requests read into a mirror**, and agents opening theirs
124 there.
125- **A working mirror** (write-through while GitHub leads). Deliberately
126 left out; it could come back as one toggle on a standby mirror.
127- **GitLab and Bitbucket adapters**: one arm each in `remotes.rs`
128 (`credential`, `protected`, `open_pull_request`), plus a webhook route.
129- **Taking in fast-forwards on a follower** skips g1t's branch rules on the
130 copied push. A follower's pushes are already gated by the remote's own
131 rules.

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