mirror your GitHub repos to tangled.org automatically
1

Configure Feed

Select the types of activity you want to include in your feed.

at main 2.7 kB View raw
1import { installationOctokit, installationToken } from './github-app' 2import { isTerminalRejection, loadActiveMapping, markMappingError, setLastSyncedRef, terminalRejectionMessage } from './repo-mapping' 3import { splicePush } from './splice' 4 5const ZERO_SHA = '0000000000000000000000000000000000000000' 6 7export interface PushPayload { 8 installationId: number 9 githubRepoId: number 10 ref: string 11 before: string 12 after: string 13} 14 15export interface PushResult { 16 status: 'synced' | 'skipped' 17 reason?: 'no-mapping' | 'disabled' | 'already-synced' | 'deletion' | 'repo-gone' 18} 19 20/** 21 * Mirror a single push from GitHub to the configured knot. 22 * 23 * 1. Look up the repo_mapping (installationId, githubRepoId). Skip if absent 24 * or disabled. 25 * 2. Ref-tip dedupe: if lastSyncedRefs[ref] === after, no-op. Guards against 26 * GitHub redeliveries. This is a cache only; correctness comes from the 27 * protocol-level compare-and-swap in the splice. 28 * 3. Skip ref deletions (after = 0000…). Handled by github.delete. 29 * 4. Splice: open receive-pack to the knot, fetch a thin pack of `after` 30 * from GitHub with the knot's tips as haves, pipe it straight through. 31 * Nothing touches disk. 32 * 5. Update lastSyncedRefs[ref] = after. 33 * 34 * On terminal failures (repo gone from knot, auth rejected, pack too big) we 35 * mark the mapping `status='error'` so the worker stops retrying. A lost 36 * compare-and-swap (`stale-old-sha`) and other transient failures re-throw so 37 * the queue retries with backoff; the retry re-reads the knot's tip. 38 */ 39export async function syncPush(payload: PushPayload): Promise<PushResult> { 40 const loaded = await loadActiveMapping(payload.installationId, payload.githubRepoId) 41 if ('skip' in loaded) return { status: 'skipped', reason: loaded.skip } 42 const { mapping } = loaded 43 44 if (payload.after === ZERO_SHA) return { status: 'skipped', reason: 'deletion' } 45 46 if (mapping.lastSyncedRefs[payload.ref] === payload.after) { 47 return { status: 'skipped', reason: 'already-synced' } 48 } 49 50 const octokit = await installationOctokit(payload.installationId) 51 const token = await installationToken(octokit) 52 53 try { 54 const result = await splicePush({ 55 installationId: payload.installationId, 56 repoFullName: mapping.githubFullName, 57 knot: mapping.knot, 58 repoDid: mapping.tangledRepoDid, 59 ref: payload.ref, 60 want: payload.after, 61 token, 62 }) 63 64 await setLastSyncedRef(mapping.id, payload.ref, result.sha) 65 return { status: 'synced' } 66 } 67 catch (err) { 68 if (isTerminalRejection(err)) { 69 await markMappingError(mapping.id, terminalRejectionMessage(err)) 70 return { status: 'skipped', reason: 'repo-gone' } 71 } 72 throw err 73 } 74}