Add agent workflows & stuff
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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<Octokit['rest']['repos']['compareCommitsWithBasehead']>
|
||||
>['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<GitHubCompareSummary> => {
|
||||
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<ReturnType<Octokit['rest']['pulls']['list']>>['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;
|
||||
};
|
||||
Reference in New Issue
Block a user