Something went wrong. Try again.
mirror your GitHub repos to tangled.org automatically
Something went wrong. Try again.
4.2 kB · 103 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104/** * Typed failures from the git wire splice. `reason` drives the worker's * retry-vs-give-up decision in `sync-push.ts` / `sync-ref.ts`. * * - repo-gone the knot no longer has the repo (or our key was revoked * such that it reports "not found"); terminal, mark error. * - auth-rejected ssh public-key auth refused; terminal, mark error. * - stale-old-sha our compare-and-swap lost: the knot's ref moved between * reading its advertisement and sending the command, or a * concurrent worker won. Transient; retry re-reads the tip. * - too-big the pack exceeded the configured byte cap; terminal, it * will never fit. * - other anything unclassified; transient, let the queue retry. */export type WireFailureReason = | 'repo-gone' | 'auth-rejected' | 'stale-old-sha' | 'too-big' | 'other'
export class WireError extends Error { constructor(message: string) { super(message) this.name = 'WireError' }}
export class RemoteRejectedError extends WireError { constructor(message: string, public readonly reason: WireFailureReason) { super(message) this.name = 'RemoteRejectedError' }}
/** * True for failures that are worth retrying immediately within the same job: a * flaky knot SSH endpoint that resets or times out the connection at the * handshake, or drops the channel mid-stream. These clear within seconds, so a * short in-job retry usually succeeds where waiting out the queue's minutes-long * backoff would leave the mirror stale. * * A `RemoteRejectedError` is never transient-transport: it's a protocol-level * verdict (CAS loss, repo gone, auth refused, pack too big) that a reconnect * won't change, so those fall through to the caller's own handling. */export function isTransientTransport(err: unknown): boolean { if (!(err instanceof WireError) || err instanceof RemoteRejectedError) return false const lc = err.message.toLowerCase() return ( lc.includes('econnreset') || lc.includes('connection reset') || lc.includes('timed out') || lc.includes('handshake') || lc.includes('no advertisement') || lc.includes('connection lost') || lc.includes('stdin closed') || lc.includes('receive-pack exited') || lc.includes('empty report-status') )}
/** * Classify ssh / sshd / knot stderr (the child process's stderr band, since * we deliberately do not request side-band multiplexing). Returns null when * nothing matches so the caller can fall back to a generic transient error. */export function classifySshStderr(stderr: string): RemoteRejectedError | null { const lc = stderr.toLowerCase() if (lc.includes('repository not found') || lc.includes('does not exist') || lc.includes('does not appear to be a git repository')) { return new RemoteRejectedError(stderr.trim(), 'repo-gone') } if (lc.includes('permission denied') || (lc.includes('publickey') && lc.includes('denied'))) { return new RemoteRejectedError(stderr.trim(), 'auth-rejected') } return null}
/** * Classify a receive-pack `ng <ref> <reason>` rejection. Any rejection that * means "the ref's current value is not what you said" maps to stale-old-sha * so the worker retries against a fresh advertisement. git phrases this two * ways: `non-fast-forward` / `stale info` when updating a moved ref, and * `failed to update ref` (stderr: "reference already exists") when our command * claimed a create but the ref already exists. */export function classifyNgReason(reason: string): RemoteRejectedError { const lc = reason.toLowerCase() if ( lc.includes('non-fast-forward') || lc.includes('fetch first') || lc.includes('stale info') || lc.includes('not a fast forward') || lc.includes('failed to update ref') || lc.includes('reference already exists') ) { return new RemoteRejectedError(reason.trim(), 'stale-old-sha') } if (lc.includes('not found') || lc.includes('does not exist')) { return new RemoteRejectedError(reason.trim(), 'repo-gone') } return new RemoteRejectedError(reason.trim(), 'other')}