Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kentcdodds/lineage

src/handle-github-webhook.ts

67 lines · 1.9 KB · TypeScript
import ingestMissing from './ingest-missing.ts'
import { getRepoSettings } from './storage.ts'

type WebhookInput = {
	request?: {
		headers?: Record<string, string>
		json?: unknown
		body?: string
	}
}

type GithubPushPayload = {
	ref?: string
	after?: string
	repository?: { full_name?: string }
	commits?: Array<{ id?: string }>
}

/**
 * Ingest new main-branch commits from a GitHub push webhook.
 * Platform HMAC verification happens before this export runs.
 *
 * @param input - Kody webhook envelope with the GitHub push JSON body
 * @returns Whether the delivery was ignored or which SHAs were ingested
 *
 * @example
 * import handleGithubWebhook from 'kody:@kentcdodds/lineage/handle-github-webhook'
 *
 * const result = await handleGithubWebhook({
 *   request: { json: { ref: 'refs/heads/main', commits: [{ id: 'abc' }] } },
 * })
 */
export default async function handleGithubWebhook(input: WebhookInput = {}) {
	const payload = (input.request?.json ?? {}) as GithubPushPayload
	const headers = input.request?.headers ?? {}
	const event = headers['x-github-event'] || headers['X-GitHub-Event']
	if (event && event !== 'push') {
		return { ok: true, ignored: true, reason: `event:${event}` }
	}

	const repo = await getRepoSettings()
	const expectedRef = `refs/heads/${repo.branch}`
	if (payload.ref && payload.ref !== expectedRef) {
		return { ok: true, ignored: true, reason: `ref:${payload.ref}` }
	}

	const remoteName = payload.repository?.full_name
	const expectedName = `${repo.owner}/${repo.repo}`
	if (remoteName && remoteName.toLowerCase() !== expectedName.toLowerCase()) {
		return { ok: true, ignored: true, reason: `repo:${remoteName}` }
	}

	const shas = [
		...new Set(
			[
				...(payload.commits ?? []).map((commit) => commit.id),
				payload.after,
			].filter((sha): sha is string => Boolean(sha) && sha !== '0'.repeat(40)),
		),
	]

	if (shas.length === 0) {
		return ingestMissing({ limit: 10, maxPages: 1 })
	}

	return ingestMissing({ shas })
}