import { isDryRun } from './dry-run.ts' import migrateToRepo from './migrate-to-repo.ts' import { SKILLS_REPO_NAME } from './repo.ts' /** * Cloudflare Artifacts → Kody `repo.pushed` payload (metadata-first). * @see https://github.com/kentcdodds/kody/blob/main/docs/guides/package-subscriptions.md */ export type RepoPushedEvent = { event: 'repo.pushed' repo: { source_id: string repo_id: string entity_kind: 'repo' | 'package' | 'job' entity_id: string name: string | null kody_id: string | null } push: { ref: string before: string after: string total_commits_count: number commits_truncated: boolean commits: Array<{ id: string message: string message_truncated: boolean timestamp: string author: { name: string; email: string } committer: { name: string; email: string } parents: Array }> } artifacts: { namespace: string event_timestamp: string event_subscription_id: string } } export type OnRepoPushedResult = | { skipped: true reason: | 'wrong_event' | 'not_plain_repo' | 'different_repo' | 'session_branch' | 'not_default_branch' repo_name?: string | null entity_kind?: string | null ref?: string } | { skipped: false synced: true ref: string after: string skills_indexed: number skill_ids: string[] backend_after: string | null dryRun?: true } function asRepoPushedEvent( params: Record | undefined, ): RepoPushedEvent | null { if (!params || params.event !== 'repo.pushed') return null const repo = params.repo const push = params.push const artifacts = params.artifacts if (!repo || typeof repo !== 'object' || !push || typeof push !== 'object') { return null } if (!artifacts || typeof artifacts !== 'object') return null return params as unknown as RepoPushedEvent } /** * Subscription handler for `repo.pushed`. * When the plain `skills` repo receives commits on the default branch * (git lane or session publish), resync the skills_index projection used by * skill_list / skill-search. Session-branch pushes are ignored: opening a * session creates those refs, and reacting to them re-opens another session. */ export default async function onRepoPushed( params: Record = {}, ): Promise { const event = asRepoPushedEvent(params) if (!event) { return { skipped: true, reason: 'wrong_event' } } const entityKind = event.repo?.entity_kind const repoName = event.repo?.name if (entityKind !== 'repo') { return { skipped: true, reason: 'not_plain_repo', entity_kind: entityKind ?? null, repo_name: repoName ?? null, } } if (repoName !== SKILLS_REPO_NAME) { return { skipped: true, reason: 'different_repo', entity_kind: entityKind, repo_name: repoName ?? null, } } const ref = event.push?.ref ?? '' const branch = ref.startsWith('refs/heads/') ? ref.slice('refs/heads/'.length) : ref // Opening a repo session git-pushes `sessions/`. That push used to // re-enter this handler, which opened another session — a tight loop that // exhausted the repo_sessions entitlement. if (branch.startsWith('sessions/')) { return { skipped: true, reason: 'session_branch', entity_kind: entityKind, repo_name: repoName, ref, } } if (branch !== 'main' && branch !== 'master') { return { skipped: true, reason: 'not_default_branch', entity_kind: entityKind, repo_name: repoName, ref, } } const dryRun = isDryRun(params.dryRun) const result = await migrateToRepo({ switch_reads: true, dryRun }) return { skipped: false, synced: true, ref: event.push?.ref ?? '', after: event.push?.after ?? '', skills_indexed: result.skills_indexed, skill_ids: result.skill_ids, backend_after: result.backend_after, ...(dryRun ? { dryRun: true as const } : {}), } }