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 | /** |
| 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 | */ | |
| 12 | import type { ServiceBinding } from "./clients"; | |
| 13 | import type { User } from "./identity"; | |
| 14 | import type { Result } from "./result"; | |
| 15 | ||
| 16 | /** Where a mirror stands with the remote it follows. */ | |
| 17 | export type MirrorState = "standby" | "ci" | "takeover" | "handing_back"; | |
| 18 | ||
| 19 | /** A repository's tie to the remote it mirrors, on `Repo.mirror`. */ | |
| 20 | export 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. */ | |
| 34 | export function mirrorWritable(mirror: RepoMirror | null | undefined): boolean { | |
| 35 | return !mirror || mirror.state === "takeover"; | |
| 36 | } | |
| 37 | ||
| 38 | export type RemoteProvider = "github" | "g1t" | "git"; | |
| 39 | export type RemoteRole = "leader" | "follower"; | |
| 40 | export type RemoteState = MirrorState | "following" | "stuck"; | |
| 41 | ||
| 42 | export 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 | ||
| 56 | export 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. */ | |
| 67 | export const TAKE_OVER_AFTER_MINUTES = [5, 24 * 60] as const; | |
| 68 | ||
| 69 | export 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 | ||
| 90 | export type RefAction = "same" | "push" | "fetch" | "pull_request" | "diverged"; | |
| 91 | export type RefDecision = "keep_ours" | "keep_theirs" | "pull_request"; | |
| 92 | ||
| 93 | export 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 | ||
| 102 | export type HandbackPlan = { | |
| 103 | refs: RefPlan[]; | |
| 104 | reachable: boolean; | |
| 105 | ready: boolean; | |
| 106 | }; | |
| 107 | ||
| 108 | export 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 | ||
| 116 | export 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 | ||
| 126 | export type MirrorAddInput = { | |
| 127 | provider: Exclude<RemoteProvider, "github">; | |
| 128 | role: RemoteRole; | |
| 129 | url: string; | |
| 130 | username?: string | null; | |
| 131 | token?: string | null; | |
| 132 | }; | |
| 133 | ||
| 134 | async 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. */ | |
| 145 | export 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.