import { installationOctokit, installationToken } from './github-app' import { isTerminalRejection, loadActiveMapping, markMappingError, setLastSyncedRef, terminalRejectionMessage } from './repo-mapping' import { splicePush } from './splice' const ZERO_SHA = '0000000000000000000000000000000000000000' export interface PushPayload { installationId: number githubRepoId: number ref: string before: string after: string } export interface PushResult { status: 'synced' | 'skipped' reason?: 'no-mapping' | 'disabled' | 'already-synced' | 'deletion' | 'repo-gone' } /** * Mirror a single push from GitHub to the configured knot. * * 1. Look up the repo_mapping (installationId, githubRepoId). Skip if absent * or disabled. * 2. Ref-tip dedupe: if lastSyncedRefs[ref] === after, no-op. Guards against * GitHub redeliveries. This is a cache only; correctness comes from the * protocol-level compare-and-swap in the splice. * 3. Skip ref deletions (after = 0000…). Handled by github.delete. * 4. Splice: open receive-pack to the knot, fetch a thin pack of `after` * from GitHub with the knot's tips as haves, pipe it straight through. * Nothing touches disk. * 5. Update lastSyncedRefs[ref] = after. * * On terminal failures (repo gone from knot, auth rejected, pack too big) we * mark the mapping `status='error'` so the worker stops retrying. A lost * compare-and-swap (`stale-old-sha`) and other transient failures re-throw so * the queue retries with backoff; the retry re-reads the knot's tip. */ export async function syncPush(payload: PushPayload): Promise { const loaded = await loadActiveMapping(payload.installationId, payload.githubRepoId) if ('skip' in loaded) return { status: 'skipped', reason: loaded.skip } const { mapping } = loaded if (payload.after === ZERO_SHA) return { status: 'skipped', reason: 'deletion' } if (mapping.lastSyncedRefs[payload.ref] === payload.after) { return { status: 'skipped', reason: 'already-synced' } } const octokit = await installationOctokit(payload.installationId) const token = await installationToken(octokit) try { const result = await splicePush({ installationId: payload.installationId, repoFullName: mapping.githubFullName, knot: mapping.knot, repoDid: mapping.tangledRepoDid, ref: payload.ref, want: payload.after, token, }) await setLastSyncedRef(mapping.id, payload.ref, result.sha) return { status: 'synced' } } catch (err) { if (isTerminalRejection(err)) { await markMappingError(mapping.id, terminalRejectionMessage(err)) return { status: 'skipped', reason: 'repo-gone' } } throw err } }