Skip to content
219 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;
108 /** For followers: pushes made on the remote itself. */
109 remotePushes: "adopt" | "overwrite";
110};
111
112export const DEFAULT_MIRROR_SETTINGS: MirrorSettings = {
113 notify: "banner",
114 takeOverAfter: null,
115 handBack: "when_clean",
116 keepCiWarm: false,
117 githubWorkflows: true,
118 holdDeploys: true,
119 remotePushes: "adopt",
120};
121
122/** The shortest and longest wait before an automatic takeover, in minutes. */
123export const TAKE_OVER_AFTER_MINUTES = [5, 24 * 60] as const;
124
125export type Remote = {
126 id: string;
127 repoId: string;
128 repo: string;
129 provider: RemoteProvider;
130 role: RemoteRole;
131 /** For people: `github.com/acme/web`. */
132 name: string;
133 url: string;
134 state: RemoteState;
135 stateSince: string;
136 /** A username, or `g1t` when it happened on its own. */
137 stateBy: string | null;
138 reachable: boolean;
139 unreachableSince: string | null;
140 syncedAt: string | null;
141 lastError: string | null;
142 settings: MirrorSettings;
143 createdAt: string;
144};
145
146export type RefAction = "same" | "push" | "fetch" | "pull_request" | "diverged";
147export type RefDecision = "keep_ours" | "keep_theirs" | "pull_request";
148
149export type RefPlan = {
150 ref: string;
151 base: string | null;
152 ours: string | null;
153 theirs: string | null;
154 action: RefAction;
155 decision: RefDecision | null;
156};
157
158export type HandbackPlan = {
159 refs: RefPlan[];
160 reachable: boolean;
161 ready: boolean;
162};
163
164export type MirrorView = {
165 remotes: Remote[];
166 plan: HandbackPlan | null;
167 canManage: boolean;
168 /** What the last action did that people should know: pull requests it opened, and so on. */
169 notes?: string[];
170};
171
172export type RemoteBrief = {
173 repoId: string;
174 role: RemoteRole;
175 name: string;
176 state: RemoteState;
177 reachable: boolean;
178 /** When g1t last copied from it or pushed to it. */
179 syncedAt?: string | null;
180};
181
182export type MirrorAddInput = {
183 provider: Exclude<RemoteProvider, "github">;
184 role: RemoteRole;
185 url: string;
186 username?: string | null;
187 token?: string | null;
188};
189
190async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
191 const response = await service.fetch(`https://service/rpc/${method}`, {
192 method: "POST",
193 headers: { "content-type": "application/json" },
194 body: JSON.stringify(args),
195 });
196 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
197 return (await response.json()) as T;
198}
199
200/** Mirroring: methods of the integrations service. */
201export function mirrorsClient(integrations: ServiceBinding) {
202 const call = <T>(method: string, args: object) => rpc<T>(integrations, method, args);
203 return {
204 view: (viewer: User | null, repoId: string) => call<Result<MirrorView>>("mirror_view", { viewer, repoId }),
205 briefs: (repoIds: string[]) => call<RemoteBrief[]>("mirror_briefs", { repoIds }),
206 takeOver: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_take_over", { actor, repoId }),
207 ci: (actor: User, repoId: string, on: boolean) => call<Result<MirrorView>>("mirror_ci", { actor, repoId, on }),
208 plan: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_hand_back_plan", { actor, repoId }),
209 handBack: (actor: User, repoId: string, decisions: Record<string, RefDecision>) =>
210 call<Result<MirrorView>>("mirror_hand_back", { actor, repoId, decisions }),
211 moveIn: (actor: User, repoId: string, keepRemoteUpdated: boolean) =>
212 call<Result<MirrorView>>("mirror_move_in", { actor, repoId, keepRemoteUpdated }),
213 sync: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_sync", { actor, repoId }),
214 settings: (actor: User, remoteId: string, settings: MirrorSettings) =>
215 call<Result<Remote>>("mirror_settings", { actor, remoteId, settings }),
216 add: (actor: User, repoId: string, input: MirrorAddInput) => call<Result<Remote>>("mirror_add", { actor, repoId, ...input }),
217 remove: (actor: User, remoteId: string) => call<Result<boolean>>("mirror_remove", { actor, remoteId }),
218 };
219}

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