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; | |
| 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-mode | 110 | /** For followers: pushes made on the remote itself. */ |
| 111 | remotePushes: "adopt" | "overwrite"; | |
| 112 | }; | |
| 113 | ||
| 114 | export 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-mode | 122 | remotePushes: "adopt", |
| 123 | }; | |
| 124 | ||
| 125 | /** The shortest and longest wait before an automatic takeover, in minutes. */ | |
| 126 | export const TAKE_OVER_AFTER_MINUTES = [5, 24 * 60] as const; | |
| 127 | ||
| 128 | export 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-mode | 147 | settings: MirrorSettings; |
| 148 | createdAt: string; | |
| 149 | }; | |
| 150 | ||
| 151 | export type RefAction = "same" | "push" | "fetch" | "pull_request" | "diverged"; | |
| 152 | export type RefDecision = "keep_ours" | "keep_theirs" | "pull_request"; | |
| 153 | ||
| 154 | export 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 | ||
| 163 | export type HandbackPlan = { | |
| 164 | refs: RefPlan[]; | |
| 165 | reachable: boolean; | |
| 166 | ready: boolean; | |
| 167 | }; | |
| 168 | ||
| 169 | export 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 | ||
| 177 | export 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 | ||
| 187 | export type MirrorAddInput = { | |
| 188 | provider: Exclude<RemoteProvider, "github">; | |
| 189 | role: RemoteRole; | |
| 190 | url: string; | |
| 191 | username?: string | null; | |
| 192 | token?: string | null; | |
| 193 | }; | |
| 194 | ||
| 195 | async 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. */ | |
| 206 | export 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.