Skip to content
224 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/**
2 * Mirroring: a repository kept in step with copies of it on other hosts.
3 * Mirrors `g1t_contracts::mirrors`.
4 *
5 * Every linked repository has exactly one leader, where work happens. A
6 * **mirror** follows a remote that leads; while it stands by it is an exact,
7 * read-only copy that runs nothing. Someone can **take over** (g1t leads for
8 * a while, then **hands back**), or **move it to g1t** for good, after which
9 * g1t no longer tracks the remote. A repository g1t leads can be **mirrored
10 * to** any number of followers.
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.11 *
12 * What a mirror offers on g1t is one rule, `mirrorOffers`, by state:
13 *
14 * | Area | Standing by | CI failover | Taken over | Handing back |
15 * | --- | --- | --- | --- | --- |
16 * | `code`: files, commits, branches, tags, compare, releases (read) | ✓ | ✓ | ✓ | ✓ |
17 * | `security`: what scanning found, read-only | ✓ | ✓ | ✓ | ✓ |
18 * | `insights`, `settings` (mirroring settings included) | ✓ | ✓ | ✓ | ✓ |
19 * | `workflows`: runs on g1t | with Keep CI warm | ✓ | ✓ | – |
20 * | `issues`, `pulls`, `agents`, `deployments` | – | – | ✓ | – |
21 * | `updates`: security and dependency updates, upkeep, anything that opens a branch or a pull request on its own | – | – | ✓ | – |
22 *
23 * The sidebar lists only what is offered, a page not offered says so in
24 * place of its list or form, every service refuses the writes, and g1t's
25 * own automation checks the same rule before it opens anything.
Merge branch 'mirroring' into artifacts-mode26 */
27import type { ServiceBinding } from "./clients";
28import type { User } from "./identity";
29import type { Result } from "./result";
30
31/** Where a mirror stands with the remote it follows. */
32export type MirrorState = "standby" | "ci" | "takeover" | "handing_back";
33
34/** A repository's tie to the remote it mirrors, on `Repo.mirror`. */
35export type RepoMirror = {
36 state: MirrorState;
37 /** For people: `github.com/acme/web`. */
38 remote: string;
39 /** Its web address. */
40 url: string;
41 /** RFC 3339: when it entered this state. */
42 since: string;
43 warm?: boolean;
44 githubWorkflows?: boolean;
45 holdDeploys?: boolean;
46};
47
48/** Whether the repository takes pushes, merges, issues and agents now. */
49export function mirrorWritable(mirror: RepoMirror | null | undefined): boolean {
50 return !mirror || mirror.state === "takeover";
51}
52
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.53/**
54 * The parts of a repository a mirror may or may not offer: the sidebar's
55 * pages, and `updates` for what g1t does on its own (security and
56 * dependency updates, upkeep, anything that opens a branch or a pull
57 * request without being asked).
58 */
59export type RepoArea =
60 | "code"
61 | "issues"
62 | "pulls"
63 | "agents"
64 | "workflows"
65 | "deployments"
66 | "security"
67 | "insights"
68 | "settings"
69 | "updates";
70
71/** What every mirror offers, whatever its state: an exact copy, read. */
72export const MIRROR_READ_AREAS: readonly RepoArea[] = ["code", "security", "insights", "settings"];
73
74/**
75 * Whether a repository with `mirror` offers `area` now (the table in this
76 * module's comment). A repository that leads offers everything, as does a
77 * mirror g1t has taken over. One standing by or handing back is a
78 * read-only copy: code, what security scanning found, insights and
79 * settings. Workflows run in CI failover, and while standing by only when
80 * the link keeps CI warm.
81 */
82export function mirrorOffers(mirror: RepoMirror | null | undefined, area: RepoArea): boolean {
83 if (mirrorWritable(mirror)) return true;
84 if (MIRROR_READ_AREAS.includes(area)) return true;
85 if (area === "workflows") return mirror!.state === "ci" || (mirror!.state === "standby" && mirror!.warm === true);
86 return false;
87}
88
89/** Why g1t's own updates wait on a mirror: the sentence the security pages and the service share. */
90export function mirrorUpdatesWait(namespace: string, name: string, mirror: RepoMirror): string {
91 return `Security updates wait until ${namespace}/${name} is taken over or moved to g1t. It is a mirror of ${mirror.remote}, so nothing opens a branch or a pull request here on its own.`;
92}
93
Merge branch 'mirroring' into artifacts-mode94export type RemoteProvider = "github" | "g1t" | "git";
95export type RemoteRole = "leader" | "follower";
96export type RemoteState = MirrorState | "following" | "stuck";
97
98export type MirrorSettings = {
99 /** Who hears that the remote stopped answering: the banner only, or the inbox too. */
100 notify: "banner" | "inbox";
101 /** Take over on its own after this many minutes unreachable; null for only when someone does. */
102 takeOverAfter: number | null;
103 /** When a takeover goes back once the remote answers: when someone says, or by itself when clean. */
104 handBack: "ask" | "when_clean";
105 keepCiWarm: boolean;
106 githubWorkflows: boolean;
107 holdDeploys: boolean;
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.108 /** A GitHub mirror: report g1t's statuses to the commit on GitHub while g1t runs its workflows. */
109 reportChecks: boolean;
Merge branch 'mirroring' into artifacts-mode110 /** For followers: pushes made on the remote itself. */
111 remotePushes: "adopt" | "overwrite";
112};
113
114export const DEFAULT_MIRROR_SETTINGS: MirrorSettings = {
115 notify: "banner",
116 takeOverAfter: null,
117 handBack: "when_clean",
118 keepCiWarm: false,
119 githubWorkflows: true,
120 holdDeploys: true,
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.121 reportChecks: true,
Merge branch 'mirroring' into artifacts-mode122 remotePushes: "adopt",
123};
124
125/** The shortest and longest wait before an automatic takeover, in minutes. */
126export const TAKE_OVER_AFTER_MINUTES = [5, 24 * 60] as const;
127
128export type Remote = {
129 id: string;
130 repoId: string;
131 repo: string;
132 provider: RemoteProvider;
133 role: RemoteRole;
134 /** For people: `github.com/acme/web`. */
135 name: string;
136 url: string;
137 state: RemoteState;
138 stateSince: string;
139 /** A username, or `g1t` when it happened on its own. */
140 stateBy: string | null;
141 reachable: boolean;
142 unreachableSince: string | null;
143 syncedAt: string | null;
144 lastError: string | null;
Merge branch 'claude/cool-newton-7nm8r9' (#36): real GitHub Actions on g1t, managed deploy workflows, g1t/deploy and the Sysbox runner 0.2.0. The merge keeps both sides' webhook events and API operations, says agent images need tar, guards the job engine's stop to Unix so the runner builds on Windows again, and keeps the moves tests' previews from going idle as the calendar passes their fixture's date.145 /** Why g1t could not report checks to the remote, until it next can. */
146 checksProblem?: string | null;
Merge branch 'mirroring' into artifacts-mode147 settings: MirrorSettings;
148 createdAt: string;
149};
150
151export type RefAction = "same" | "push" | "fetch" | "pull_request" | "diverged";
152export type RefDecision = "keep_ours" | "keep_theirs" | "pull_request";
153
154export type RefPlan = {
155 ref: string;
156 base: string | null;
157 ours: string | null;
158 theirs: string | null;
159 action: RefAction;
160 decision: RefDecision | null;
161};
162
163export type HandbackPlan = {
164 refs: RefPlan[];
165 reachable: boolean;
166 ready: boolean;
167};
168
169export type MirrorView = {
170 remotes: Remote[];
171 plan: HandbackPlan | null;
172 canManage: boolean;
173 /** What the last action did that people should know: pull requests it opened, and so on. */
174 notes?: string[];
175};
176
177export type RemoteBrief = {
178 repoId: string;
179 role: RemoteRole;
180 name: string;
181 state: RemoteState;
182 reachable: boolean;
183 /** When g1t last copied from it or pushed to it. */
184 syncedAt?: string | null;
185};
186
187export type MirrorAddInput = {
188 provider: Exclude<RemoteProvider, "github">;
189 role: RemoteRole;
190 url: string;
191 username?: string | null;
192 token?: string | null;
193};
194
195async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
196 const response = await service.fetch(`https://service/rpc/${method}`, {
197 method: "POST",
198 headers: { "content-type": "application/json" },
199 body: JSON.stringify(args),
200 });
201 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
202 return (await response.json()) as T;
203}
204
205/** Mirroring: methods of the integrations service. */
206export function mirrorsClient(integrations: ServiceBinding) {
207 const call = <T>(method: string, args: object) => rpc<T>(integrations, method, args);
208 return {
209 view: (viewer: User | null, repoId: string) => call<Result<MirrorView>>("mirror_view", { viewer, repoId }),
210 briefs: (repoIds: string[]) => call<RemoteBrief[]>("mirror_briefs", { repoIds }),
211 takeOver: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_take_over", { actor, repoId }),
212 ci: (actor: User, repoId: string, on: boolean) => call<Result<MirrorView>>("mirror_ci", { actor, repoId, on }),
213 plan: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_hand_back_plan", { actor, repoId }),
214 handBack: (actor: User, repoId: string, decisions: Record<string, RefDecision>) =>
215 call<Result<MirrorView>>("mirror_hand_back", { actor, repoId, decisions }),
216 moveIn: (actor: User, repoId: string, keepRemoteUpdated: boolean) =>
217 call<Result<MirrorView>>("mirror_move_in", { actor, repoId, keepRemoteUpdated }),
218 sync: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_sync", { actor, repoId }),
219 settings: (actor: User, remoteId: string, settings: MirrorSettings) =>
220 call<Result<Remote>>("mirror_settings", { actor, remoteId, settings }),
221 add: (actor: User, repoId: string, input: MirrorAddInput) => call<Result<Remote>>("mirror_add", { actor, repoId, ...input }),
222 remove: (actor: User, remoteId: string) => call<Result<boolean>>("mirror_remove", { actor, remoteId }),
223 };
224}

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