Something went wrong. Try again.
mirror your GitHub repos to tangled.org automatically forked from danielroe.dev/synchub.to
Something went wrong. Try again.
4.7 kB · 125 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126import { RemoteRejectedError, WireError } from './git-wire/errors'import { fetchAdvertisement } from './git-wire/upload-pack'import { installationOctokit, installationToken } from './github-app'import { clearLastSyncedRef, isTerminalRejection, loadActiveMapping, markMappingError, setLastSyncedRef } from './repo-mapping'import { spliceDelete, splicePush } from './splice'
export type RefType = 'branch' | 'tag'
export interface CreateRefPayload { installationId: number githubRepoId: number refType: RefType /** Short ref name as GitHub delivers it (e.g. `v1.0`, `feature-x`) — NOT * the `refs/...` qualified form. */ ref: string}
export interface DeleteRefPayload extends CreateRefPayload {}
export interface RefResult { status: 'synced' | 'skipped' reason?: 'no-mapping' | 'disabled' | 'not-branch-or-tag' | 'repo-gone'}
/** * Mirror a branch or tag creation from GitHub to the configured knot. * * Triggered by GitHub's `create` webhook event. For branches, GitHub also * sends a parallel `push` event (with `before = 0000…`), so the branch will * usually have been created already by the time this fires — the splice is * then a no-op via the knot tip already matching. For lightweight and * annotated tags, no `push` event is sent, so this is the only path that * creates them on the knot. * * We resolve the ref name to a SHA from GitHub's advertisement (annotated tags * resolve to the tag object), then splice that SHA to the knot. */export async function syncCreateRef(payload: CreateRefPayload): Promise<RefResult> { if (payload.refType !== 'branch' && payload.refType !== 'tag') { return { status: 'skipped', reason: 'not-branch-or-tag' } }
const loaded = await loadActiveMapping(payload.installationId, payload.githubRepoId) if ('skip' in loaded) return { status: 'skipped', reason: loaded.skip } const { mapping } = loaded
const fullRef = qualifyRef(payload.refType, payload.ref)
const octokit = await installationOctokit(payload.installationId) const token = await installationToken(octokit)
const adv = await fetchAdvertisement(mapping.githubFullName, token) const want = adv.refs.get(fullRef) if (!want) { // GitHub can deliver the create webhook before its replicas advertise the // ref. Transient: re-throw so the queue retries with backoff. throw new WireError(`github does not yet advertise ${fullRef} for ${mapping.githubFullName}`) }
try { const result = await splicePush({ installationId: payload.installationId, repoFullName: mapping.githubFullName, knot: mapping.knot, repoDid: mapping.tangledRepoDid, ref: fullRef, want, token, }) await setLastSyncedRef(mapping.id, fullRef, result.sha) return { status: 'synced' } } catch (err) { if (isTerminalRejection(err)) { await markMappingError(mapping.id, 'knot reports repo no longer exists; stopping sync') return { status: 'skipped', reason: 'repo-gone' } } throw err }}
/** * Mirror a branch or tag deletion from GitHub to the configured knot. * * Triggered by GitHub's `delete` webhook event. For branches, GitHub also * sends a parallel `push` event with `after = 0000…`, which `syncPush` * currently skips (`reason: 'deletion'`) — this is the path that actually * removes the ref on the knot. Tag deletion arrives only via this event. * * Deletion is idempotent: if the ref is already absent on the knot we treat it * as success. We wanted it gone, it's gone. */export async function syncDeleteRef(payload: DeleteRefPayload): Promise<RefResult> { if (payload.refType !== 'branch' && payload.refType !== 'tag') { return { status: 'skipped', reason: 'not-branch-or-tag' } }
const loaded = await loadActiveMapping(payload.installationId, payload.githubRepoId) if ('skip' in loaded) return { status: 'skipped', reason: loaded.skip } const { mapping } = loaded
const fullRef = qualifyRef(payload.refType, payload.ref)
try { await spliceDelete({ installationId: payload.installationId, knot: mapping.knot, repoDid: mapping.tangledRepoDid, ref: fullRef, }) await clearLastSyncedRef(mapping.id, fullRef) return { status: 'synced' } } catch (err) { if (err instanceof RemoteRejectedError && err.reason === 'repo-gone') { await markMappingError(mapping.id, 'knot reports repo no longer exists; stopping sync') return { status: 'skipped', reason: 'repo-gone' } } throw err }}
function qualifyRef(refType: RefType, ref: string): string { return refType === 'tag' ? `refs/tags/${ref}` : `refs/heads/${ref}`}