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-mode | 1 | # 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 | ||
| 14 | The user-facing guide is `apps/docs/src/content/docs/guides/mirroring.md`. | |
| 15 | This is the design record. | |
| 16 | ||
| 17 | ## The rule | |
| 18 | ||
| 19 | **Every linked repository has exactly one leader. Work happens on the | |
| 20 | leader; the others follow.** g1t is never a second place where people work | |
| 21 | while the remote also leads, so there's nothing to merge back by surprise. | |
| 22 | Every 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 | ||
| 33 | Outside a takeover, every commit on a mirror's branches is already on the | |
| 34 | remote. So a standby mirror is safe to treat as a backup, and any | |
| 35 | difference between the two is a bug. | |
| 36 | ||
| 37 | ## Decisions taken | |
| 38 | ||
| 39 | 1. **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. | |
| 42 | 2. **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. | |
| 48 | 3. **Take over any time**, not only during a detected outage. | |
| 49 | 4. **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. | |
| 61 | 5. **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. | |
| 64 | 6. **CI failover is its own switch.** It runs `.github/workflows` as well | |
| 65 | as `.g1t/workflows`; g1t's wins a `name:`. | |
| 66 | 7. **Move to g1t** is permanent. The confirmation says g1t will no longer | |
| 67 | track the remote. Optionally the remote becomes a follower. | |
| 68 | 8. **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.