mirror your GitHub repos to tangled.org automatically
1

Configure Feed

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

1import { sql } from 'drizzle-orm' 2import { userIdentity } from '../db/schema' 3import { useOAuthClient } from './atproto-oauth' 4import { useDb } from './db' 5import { installationOctokit } from './github-app' 6import type { JobEnvelope } from './queue' 7import { enqueue } from './queue' 8import { generateAndPublishKey } from './tangled-pubkey' 9import { enrollRepo } from './tangled-repo' 10 11/** 12 * Map of job kind → handler. Each commit fills in its slice: 13 * - 'github.push' → commit 12 (sync push events) 14 * - 'github.create' / 'github.delete' → commit 13 (branch/tag ref ops) 15 * - 'github.repository' → commit 14/15 (description, lifecycle) 16 * - 'github.installation_repositories' → this commit (fan-out enrolment) 17 * - 'tangled.backfill-installation' → this commit (paginate + fan-out) 18 * - 'tangled.create-repo' → this commit (per-repo enrolment) 19 * - 'atproto.publish-pubkey' → commit 9 20 * 21 * Unknown kinds throw so they surface as job failures rather than silent 22 * acknowledgement. 23 */ 24const KNOWN_KINDS = new Set([ 25 'github.push', 26 'github.create', 27 'github.delete', 28 'github.repository', 29 'github.installation_repositories', 30 'tangled.backfill-installation', 31 'tangled.create-repo', 32 'atproto.publish-pubkey', 33]) 34 35const BACKFILL_PAGE_SIZE = 100 36 37interface PublishPubkeyPayload { 38 did: string 39 installationId: number 40} 41 42interface CreateRepoPayload { 43 installationId: number 44 githubRepoId: number 45} 46 47interface InstallationRepositoriesPayload { 48 installationId: number 49 action: 'added' | 'removed' 50 addedRepoIds: number[] 51 removedRepoIds: number[] 52} 53 54interface BackfillInstallationPayload { 55 installationId: number 56 page: number 57} 58 59export async function dispatch(envelope: JobEnvelope): Promise<void> { 60 if (!KNOWN_KINDS.has(envelope.kind)) { 61 throw new Error(`unknown job kind: ${envelope.kind}`) 62 } 63 64 if (envelope.kind === 'atproto.publish-pubkey') { 65 const { did, installationId } = envelope.payload as PublishPubkeyPayload 66 const client = await useOAuthClient() 67 const session = await client.restore(did) 68 await generateAndPublishKey({ oauthSession: session, installationId }) 69 return 70 } 71 72 if (envelope.kind === 'tangled.create-repo') { 73 const { installationId, githubRepoId } = envelope.payload as CreateRepoPayload 74 75 // Find the user identity bound to this install. If OAuth hasn't completed 76 // yet, drop this job silently \u2014 OAuth callback re-enqueues for all 77 // accessible repos at completion time, so we'll get a fresh trigger. 78 const db = useDb() 79 const identity = await db.select({ did: userIdentity.did }) 80 .from(userIdentity) 81 .where(sql`${userIdentity.installationId} = ${installationId}`) 82 if (identity.length === 0) return 83 84 const client = await useOAuthClient() 85 const session = await client.restore(identity[0]!.did) 86 await enrollRepo({ oauthSession: session, installationId, githubRepoId }) 87 return 88 } 89 90 if (envelope.kind === 'tangled.backfill-installation') { 91 const { installationId, page } = envelope.payload as BackfillInstallationPayload 92 const octokit = await installationOctokit(installationId) 93 const { data } = await octokit.request('GET /installation/repositories', { 94 per_page: BACKFILL_PAGE_SIZE, 95 page, 96 }) 97 98 // Fan out one tangled.create-repo job per repo on this page. 99 for (const repo of data.repositories) { 100 // eslint-disable-next-line no-await-in-loop -- enqueue is sequential by design 101 await enqueue('tangled.create-repo', { installationId, githubRepoId: repo.id }) 102 } 103 104 // If there are more pages, re-queue ourselves for the next one. This 105 // keeps each tick small and bounded; an install with thousands of repos 106 // walks through over many minutes rather than blocking one worker. 107 const seenSoFar = (page - 1) * BACKFILL_PAGE_SIZE + data.repositories.length 108 if (seenSoFar < data.total_count && data.repositories.length > 0) { 109 await enqueue('tangled.backfill-installation', { installationId, page: page + 1 }) 110 } 111 return 112 } 113 114 if (envelope.kind === 'github.installation_repositories') { 115 const { installationId, action, addedRepoIds } = envelope.payload as InstallationRepositoriesPayload 116 if (action !== 'added') return 117 118 // Fan out one tangled.create-repo job per added repo. The fan-out keeps 119 // each unit small enough to fit comfortably in the per-job lease, lets 120 // failures retry independently, and runs the OAuth precondition check 121 // per repo (an install can outlive a tangled identity disconnection). 122 for (const id of addedRepoIds) { 123 // eslint-disable-next-line no-await-in-loop -- fan-out enqueue is sequential by design 124 await enqueue('tangled.create-repo', { installationId, githubRepoId: id }) 125 } 126 return 127 } 128 129 // Other kinds: still no-op until handlers land in their commits. 130}