Skip to content
163 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.
11 */
12import type { ServiceBinding } from "./clients";
13import type { User } from "./identity";
14import type { Result } from "./result";
15
16/** Where a mirror stands with the remote it follows. */
17export type MirrorState = "standby" | "ci" | "takeover" | "handing_back";
18
19/** A repository's tie to the remote it mirrors, on `Repo.mirror`. */
20export type RepoMirror = {
21 state: MirrorState;
22 /** For people: `github.com/acme/web`. */
23 remote: string;
24 /** Its web address. */
25 url: string;
26 /** RFC 3339: when it entered this state. */
27 since: string;
28 warm?: boolean;
29 githubWorkflows?: boolean;
30 holdDeploys?: boolean;
31};
32
33/** Whether the repository takes pushes, merges, issues and agents now. */
34export function mirrorWritable(mirror: RepoMirror | null | undefined): boolean {
35 return !mirror || mirror.state === "takeover";
36}
37
38export type RemoteProvider = "github" | "g1t" | "git";
39export type RemoteRole = "leader" | "follower";
40export type RemoteState = MirrorState | "following" | "stuck";
41
42export type MirrorSettings = {
43 /** Who hears that the remote stopped answering: the banner only, or the inbox too. */
44 notify: "banner" | "inbox";
45 /** Take over on its own after this many minutes unreachable; null for only when someone does. */
46 takeOverAfter: number | null;
47 /** When a takeover goes back once the remote answers: when someone says, or by itself when clean. */
48 handBack: "ask" | "when_clean";
49 keepCiWarm: boolean;
50 githubWorkflows: boolean;
51 holdDeploys: boolean;
52 /** For followers: pushes made on the remote itself. */
53 remotePushes: "adopt" | "overwrite";
54};
55
56export const DEFAULT_MIRROR_SETTINGS: MirrorSettings = {
57 notify: "banner",
58 takeOverAfter: null,
59 handBack: "when_clean",
60 keepCiWarm: false,
61 githubWorkflows: true,
62 holdDeploys: true,
63 remotePushes: "adopt",
64};
65
66/** The shortest and longest wait before an automatic takeover, in minutes. */
67export const TAKE_OVER_AFTER_MINUTES = [5, 24 * 60] as const;
68
69export type Remote = {
70 id: string;
71 repoId: string;
72 repo: string;
73 provider: RemoteProvider;
74 role: RemoteRole;
75 /** For people: `github.com/acme/web`. */
76 name: string;
77 url: string;
78 state: RemoteState;
79 stateSince: string;
80 /** A username, or `g1t` when it happened on its own. */
81 stateBy: string | null;
82 reachable: boolean;
83 unreachableSince: string | null;
84 syncedAt: string | null;
85 lastError: string | null;
86 settings: MirrorSettings;
87 createdAt: string;
88};
89
90export type RefAction = "same" | "push" | "fetch" | "pull_request" | "diverged";
91export type RefDecision = "keep_ours" | "keep_theirs" | "pull_request";
92
93export type RefPlan = {
94 ref: string;
95 base: string | null;
96 ours: string | null;
97 theirs: string | null;
98 action: RefAction;
99 decision: RefDecision | null;
100};
101
102export type HandbackPlan = {
103 refs: RefPlan[];
104 reachable: boolean;
105 ready: boolean;
106};
107
108export type MirrorView = {
109 remotes: Remote[];
110 plan: HandbackPlan | null;
111 canManage: boolean;
112 /** What the last action did that people should know: pull requests it opened, and so on. */
113 notes?: string[];
114};
115
116export type RemoteBrief = {
117 repoId: string;
118 role: RemoteRole;
119 name: string;
120 state: RemoteState;
121 reachable: boolean;
122 /** When g1t last copied from it or pushed to it. */
123 syncedAt?: string | null;
124};
125
126export type MirrorAddInput = {
127 provider: Exclude<RemoteProvider, "github">;
128 role: RemoteRole;
129 url: string;
130 username?: string | null;
131 token?: string | null;
132};
133
134async function rpc<T>(service: ServiceBinding, method: string, args: object): Promise<T> {
135 const response = await service.fetch(`https://service/rpc/${method}`, {
136 method: "POST",
137 headers: { "content-type": "application/json" },
138 body: JSON.stringify(args),
139 });
140 if (!response.ok) throw new Error(`${method} failed with status ${response.status}`);
141 return (await response.json()) as T;
142}
143
144/** Mirroring: methods of the integrations service. */
145export function mirrorsClient(integrations: ServiceBinding) {
146 const call = <T>(method: string, args: object) => rpc<T>(integrations, method, args);
147 return {
148 view: (viewer: User | null, repoId: string) => call<Result<MirrorView>>("mirror_view", { viewer, repoId }),
149 briefs: (repoIds: string[]) => call<RemoteBrief[]>("mirror_briefs", { repoIds }),
150 takeOver: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_take_over", { actor, repoId }),
151 ci: (actor: User, repoId: string, on: boolean) => call<Result<MirrorView>>("mirror_ci", { actor, repoId, on }),
152 plan: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_hand_back_plan", { actor, repoId }),
153 handBack: (actor: User, repoId: string, decisions: Record<string, RefDecision>) =>
154 call<Result<MirrorView>>("mirror_hand_back", { actor, repoId, decisions }),
155 moveIn: (actor: User, repoId: string, keepRemoteUpdated: boolean) =>
156 call<Result<MirrorView>>("mirror_move_in", { actor, repoId, keepRemoteUpdated }),
157 sync: (actor: User, repoId: string) => call<Result<MirrorView>>("mirror_sync", { actor, repoId }),
158 settings: (actor: User, remoteId: string, settings: MirrorSettings) =>
159 call<Result<Remote>>("mirror_settings", { actor, remoteId, settings }),
160 add: (actor: User, repoId: string, input: MirrorAddInput) => call<Result<Remote>>("mirror_add", { actor, repoId, ...input }),
161 remove: (actor: User, remoteId: string) => call<Result<boolean>>("mirror_remove", { actor, remoteId }),
162 };
163}

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