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. | |
| 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-mode | 26 | */ |
| 27 | import type { ServiceBinding } from "./clients"; | |
| 28 | import type { User } from "./identity"; | |
| 29 | import type { Result } from "./result"; | |
| 30 | ||
| 31 | /** Where a mirror stands with the remote it follows. */ | |
| 32 | export type MirrorState = "standby" | "ci" | "takeover" | "handing_back"; | |
| 33 | ||
| 34 | /** A repository's tie to the remote it mirrors, on `Repo.mirror`. */ | |
| 35 | export 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. */ | |
| 49 | export 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 | */ | |
| 59 | export 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. */ | |
| 72 | export 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 | */ | |
| 82 | export 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. */ | |
| 90 | export 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-mode | 94 | export type RemoteProvider = "github" | "g1t" | "git"; |
| 95 | export type RemoteRole = "leader" | "follower"; | |
| 96 | export type RemoteState = MirrorState | "following" | "stuck"; | |
| 97 | ||
| 98 | export 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 | ||
| 112 | export 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. */ | |
| 123 | export const TAKE_OVER_AFTER_MINUTES = [5, 24 * 60] as const; | |
| 124 | ||
| 125 | export 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 | ||
| 146 | export type RefAction = "same" | "push" | "fetch" | "pull_request" | "diverged"; | |
| 147 | export type RefDecision = "keep_ours" | "keep_theirs" | "pull_request"; | |
| 148 | ||
| 149 | export 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 | ||
| 158 | export type HandbackPlan = { | |
| 159 | refs: RefPlan[]; | |
| 160 | reachable: boolean; | |
| 161 | ready: boolean; | |
| 162 | }; | |
| 163 | ||
| 164 | export 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 | ||
| 172 | export 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 | ||
| 182 | export type MirrorAddInput = { | |
| 183 | provider: Exclude<RemoteProvider, "github">; | |
| 184 | role: RemoteRole; | |
| 185 | url: string; | |
| 186 | username?: string | null; | |
| 187 | token?: string | null; | |
| 188 | }; | |
| 189 | ||
| 190 | async 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. */ | |
| 201 | export 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.