import { advertisement, artifactsSource, relayPack } from "../git/relay"; import type { RelaySource } from "../git/relay"; import type { AdvertisedRef, RefCommand } from "../git/ref-sync-protocol"; import { pushToArtifacts } from "../git/writer"; import { loadRepoById } from "../repos"; import { gitAuthorization, installationToken } from "./github-app"; import type { OutboundResult } from "./outbound"; import { parseConfig } from "./store"; import type { IntegrationRow } from "./store"; import type { DeliverInput } from "./deliver"; import type { RepoRow } from "~/lib/types"; const MAX_MIRROR_REFS = 1000; const MAX_MIRROR_BYTES = 1024 * 1024 * 1024; const ZERO_SHA = "0".repeat(40); const GITHUB_REPO = /^[A-Za-z0-9-]{1,39}\/[A-Za-z0-9._-]{1,100}$/; export type MirrorResult = { updated: number; rejected: string[]; skipped: number }; export function isGithubRepoName(value: string): boolean { return GITHUB_REPO.test(value) && !value.includes(".."); } /** Refs to send so `target` matches `source`; never deletes, and caps the count so one sync stays bounded. */ export function planMirror( source: AdvertisedRef[], target: AdvertisedRef[], ): { commands: RefCommand[]; skipped: number } { const current = new Map(target.map((ref) => [ref.name, ref.sha])); const changed = source.filter((ref) => current.get(ref.name) !== ref.sha); const commands = changed.slice(0, MAX_MIRROR_REFS).map((ref) => ({ name: ref.name, sha: ref.sha, oldSha: current.get(ref.name) ?? ZERO_SHA, })); return { commands, skipped: changed.length - commands.length }; } async function githubRemote(row: IntegrationRow): Promise { const config = parseConfig(row); if (!config.githubInstallationId || !config.githubRepo || !isGithubRepoName(config.githubRepo)) { throw new Error("The GitHub mirror is missing an installation or repository"); } const token = await installationToken(config.githubInstallationId); return { url: `https://github.com/${config.githubRepo}.git`, authorization: gitAuthorization(token) }; } /** gitfor → GitHub: relays a pack from Artifacts into GitHub's receive-pack using an installation token. */ export async function mirrorToGithub(row: IntegrationRow, repo: RepoRow): Promise { const remote = await githubRemote(row); const self = await artifactsSource(repo); const [local, upstream] = await Promise.all([advertisement(self), advertisement(remote)]); const { commands, skipped } = planMirror(local, upstream); if (commands.length === 0) return { updated: 0, rejected: [], skipped }; const report = await relayPack({ source: self, target: remote, wants: [...new Set(commands.map((command) => command.sha))], haves: upstream.map((ref) => ref.sha), commands, maxBytes: MAX_MIRROR_BYTES, }); return { updated: report.accepted, rejected: report.rejected, skipped }; } /** GitHub → gitfor: fast path for refs whose objects already exist, otherwise relays a pack into Artifacts. */ export async function mirrorFromGithub(row: IntegrationRow, repo: RepoRow): Promise { const remote = await githubRemote(row); const self = await artifactsSource(repo); const [upstream, local] = await Promise.all([advertisement(remote), advertisement(self)]); const { commands, skipped } = planMirror(upstream, local); if (commands.length === 0) return { updated: 0, rejected: [], skipped }; const present = new Set(local.map((ref) => ref.sha)); const wants = [...new Set(commands.map((command) => command.sha))].filter((sha) => !present.has(sha)); if (wants.length === 0) { await pushToArtifacts( repo, commands.map((command) => ({ ref: command.name, oldSha: command.oldSha ?? ZERO_SHA, newSha: command.sha })), [], ); return { updated: commands.length, rejected: [], skipped }; } const report = await relayPack({ source: remote, target: repo, wants, haves: [...present], commands, maxBytes: MAX_MIRROR_BYTES, }); return { updated: report.accepted, rejected: report.rejected, skipped }; } /** GitHub integrations react to pushes (mirror out) and `mirror.pull` ticks / relayed GitHub pushes (mirror in). */ export async function handleGithubEvent(row: IntegrationRow, input: DeliverInput): Promise { const ok: OutboundResult = { status: 200, body: "", error: null, durationMs: 0 }; const config = parseConfig(row); if (input.event === "ping") { if (!config.githubInstallationId) return { ...ok, status: null, error: "No GitHub App installation", permanent: true }; await installationToken(config.githubInstallationId); return { ...ok, body: "Installation token issued" }; } const direction = config.githubMirror ?? "none"; const wantsPush = direction === "push" && (input.event === "push" || input.event === "mirror.push"); const wantsPull = direction === "pull" && (input.event === "mirror.pull" || input.event === "github.push"); if ((!wantsPush && !wantsPull) || !input.repoId) return ok; const repo = await loadRepoById(input.repoId); if (!repo) return ok; const started = Date.now(); const result = wantsPush ? await mirrorToGithub(row, repo) : await mirrorFromGithub(row, repo); return { status: 200, body: JSON.stringify(result), error: result.rejected.length > 0 ? `Rejected: ${result.rejected.slice(0, 5).join(", ")}` : null, durationMs: Date.now() - started, }; }