import { createAppAuth } from '@octokit/auth-app'; import { Octokit } from '@octokit/rest'; import { ConvexError } from 'convex/values'; import type { Doc } from './_generated/dataModel'; export type GitHubCommitSummary = { sha: string; message: string; authorName?: string; authorEmail?: string; authorLogin?: string; committedAt?: number; htmlUrl?: string; filesChanged?: number; additions?: number; deletions?: number; }; export type GitHubPullRequestSummary = { githubId: number; number: number; repoFullName: string; title: string; state: 'open' | 'closed' | 'merged'; draft: boolean; authorLogin?: string; baseRef: string; headRef: string; headRepoFullName?: string; htmlUrl: string; createdAtGithub?: number; updatedAtGithub?: number; mergedAtGithub?: number; }; export type GitHubCompareSummary = { aheadBy: number; mergeBaseSha?: string; headSha?: string; baseSha?: string; htmlUrl?: string; commits: GitHubCommitSummary[]; }; const getEnv = (name: string) => { const value = process.env[name]?.trim(); if (!value) throw new ConvexError(`${name} is not configured.`); return value; }; const normalizePrivateKey = (value: string) => value.replaceAll('\\n', '\n'); export const getInstallationOctokit = (installationId: string) => new Octokit({ authStrategy: createAppAuth, auth: { appId: getEnv('GITHUB_APP_ID'), privateKey: normalizePrivateKey(getEnv('GITHUB_APP_PRIVATE_KEY')), installationId, }, userAgent: 'Spoon', request: { headers: { 'X-GitHub-Api-Version': '2022-11-28', }, }, }); export const getSpoonInstallationId = ( spoon: Doc<'spoons'>, connection?: Doc<'gitConnections'> | null, ) => { const installationId = spoon.githubInstallationId ?? connection?.installationId ?? undefined; if (!installationId) { throw new ConvexError('Connect a GitHub App installation first.'); } return installationId; }; export const getRepository = async ( octokit: Octokit, owner: string, repo: string, ) => { const result = await octokit.rest.repos.get({ owner, repo }); return result.data; }; const toMillis = (value?: string | null) => value ? new Date(value).getTime() : undefined; const normalizeCompareCommit = ( commit: Awaited< ReturnType >['data']['commits'][number], ): GitHubCommitSummary => ({ sha: commit.sha, message: commit.commit.message, authorName: commit.commit.author?.name ?? undefined, authorEmail: commit.commit.author?.email ?? undefined, authorLogin: commit.author?.login ?? undefined, committedAt: toMillis( commit.commit.author?.date ?? commit.commit.committer?.date, ), htmlUrl: commit.html_url, }); export const compareAcrossForkNetwork = async ( octokit: Octokit, args: { owner: string; repo: string; baseOwner: string; baseBranch: string; headOwner: string; headBranch: string; }, ): Promise => { const basehead = `${args.baseOwner}:${args.baseBranch}...${args.headOwner}:${args.headBranch}`; const result = await octokit.rest.repos.compareCommitsWithBasehead({ owner: args.owner, repo: args.repo, basehead, per_page: 100, }); const commits = result.data.commits.map(normalizeCompareCommit); return { aheadBy: result.data.ahead_by, mergeBaseSha: result.data.merge_base_commit.sha, headSha: commits[commits.length - 1]?.sha, baseSha: result.data.base_commit.sha, htmlUrl: result.data.html_url, commits, }; }; const normalizePullRequest = ( repoFullName: string, pull: Awaited>['data'][number], ): GitHubPullRequestSummary => ({ githubId: pull.id, number: pull.number, repoFullName, title: pull.title, state: pull.merged_at ? 'merged' : pull.state === 'open' ? 'open' : 'closed', draft: pull.draft === true, authorLogin: pull.user?.login ?? undefined, baseRef: pull.base.ref, headRef: pull.head.ref, headRepoFullName: pull.head.repo.full_name, htmlUrl: pull.html_url, createdAtGithub: toMillis(pull.created_at), updatedAtGithub: toMillis(pull.updated_at), mergedAtGithub: toMillis(pull.merged_at), }); export const listPullRequests = async ( octokit: Octokit, args: { owner: string; repo: string; head?: string }, ) => { const result = await octokit.rest.pulls.list({ owner: args.owner, repo: args.repo, state: 'all', per_page: 100, head: args.head, }); return result.data.map((pull) => normalizePullRequest(`${args.owner}/${args.repo}`, pull), ); }; export const syncForkBranch = async ( octokit: Octokit, args: { forkOwner: string; forkRepo: string; branch: string }, ) => { const result = await octokit.rest.repos.mergeUpstream({ owner: args.forkOwner, repo: args.forkRepo, branch: args.branch, }); return result.data; };