436d951b65
* Add schemas for gh app installations * Implement gh app installation flow * Make the gh app configs optional * Add additional org check on gh app installation callback * Save account handle and repo default branch on install * Do repo hard deletes in favor of simplicity * Disable github app by default * Fix gh env schema union issue * Use octokit's iterator for paginating repos * Parse gh app install callback with a discriminated union * Remove duplicate env vars * Use bigint for github integer IDs * Sanitize redirect paths in the gh installation and auth flow * Regenerate migration after rebase on main to fix ordering * Handle gh install updates separately from new installs
136 lines
3.8 KiB
TypeScript
136 lines
3.8 KiB
TypeScript
import { App, type Octokit } from "octokit";
|
|
import { env } from "../env.server";
|
|
import { prisma } from "~/db.server";
|
|
import { logger } from "./logger.server";
|
|
|
|
export const githubApp =
|
|
env.GITHUB_APP_ENABLED === "1"
|
|
? new App({
|
|
appId: env.GITHUB_APP_ID,
|
|
privateKey: env.GITHUB_APP_PRIVATE_KEY,
|
|
webhooks: {
|
|
secret: env.GITHUB_APP_WEBHOOK_SECRET,
|
|
},
|
|
})
|
|
: null;
|
|
|
|
/**
|
|
* Links a GitHub App installation to a Trigger organization
|
|
*/
|
|
export async function linkGitHubAppInstallation(
|
|
installationId: number,
|
|
organizationId: string
|
|
): Promise<void> {
|
|
if (!githubApp) {
|
|
throw new Error("GitHub App is not enabled");
|
|
}
|
|
|
|
const octokit = await githubApp.getInstallationOctokit(installationId);
|
|
const { data: installation } = await octokit.rest.apps.getInstallation({
|
|
installation_id: installationId,
|
|
});
|
|
|
|
const repositories = await fetchInstallationRepositories(octokit, installationId);
|
|
|
|
const repositorySelection = installation.repository_selection === "all" ? "ALL" : "SELECTED";
|
|
|
|
await prisma.githubAppInstallation.create({
|
|
data: {
|
|
appInstallationId: installationId,
|
|
organizationId,
|
|
targetId: installation.target_id,
|
|
targetType: installation.target_type,
|
|
accountHandle: installation.account
|
|
? "login" in installation.account
|
|
? installation.account.login
|
|
: "slug" in installation.account
|
|
? installation.account.slug
|
|
: "-"
|
|
: "-",
|
|
permissions: installation.permissions,
|
|
repositorySelection,
|
|
repositories: {
|
|
create: repositories,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Links a GitHub App installation to a Trigger organization
|
|
*/
|
|
export async function updateGitHubAppInstallation(installationId: number): Promise<void> {
|
|
if (!githubApp) {
|
|
throw new Error("GitHub App is not enabled");
|
|
}
|
|
|
|
const octokit = await githubApp.getInstallationOctokit(installationId);
|
|
const { data: installation } = await octokit.rest.apps.getInstallation({
|
|
installation_id: installationId,
|
|
});
|
|
|
|
const existingInstallation = await prisma.githubAppInstallation.findFirst({
|
|
where: { appInstallationId: installationId },
|
|
});
|
|
|
|
if (!existingInstallation) {
|
|
throw new Error("GitHub App installation not found");
|
|
}
|
|
|
|
const repositorySelection = installation.repository_selection === "all" ? "ALL" : "SELECTED";
|
|
|
|
// repos are updated asynchronously via webhook events
|
|
await prisma.githubAppInstallation.update({
|
|
where: { id: existingInstallation?.id },
|
|
data: {
|
|
appInstallationId: installationId,
|
|
targetId: installation.target_id,
|
|
targetType: installation.target_type,
|
|
accountHandle: installation.account
|
|
? "login" in installation.account
|
|
? installation.account.login
|
|
: "slug" in installation.account
|
|
? installation.account.slug
|
|
: "-"
|
|
: "-",
|
|
permissions: installation.permissions,
|
|
suspendedAt: existingInstallation?.suspendedAt,
|
|
repositorySelection,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function fetchInstallationRepositories(octokit: Octokit, installationId: number) {
|
|
const iterator = octokit.paginate.iterator(octokit.rest.apps.listReposAccessibleToInstallation, {
|
|
installation_id: installationId,
|
|
per_page: 100,
|
|
});
|
|
|
|
const allRepos = [];
|
|
const maxPages = 3;
|
|
let pageCount = 0;
|
|
|
|
for await (const { data } of iterator) {
|
|
pageCount++;
|
|
allRepos.push(...data);
|
|
|
|
if (maxPages && pageCount >= maxPages) {
|
|
logger.warn("GitHub installation repository fetch truncated", {
|
|
installationId,
|
|
maxPages,
|
|
totalReposFetched: allRepos.length,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
return allRepos.map((repo) => ({
|
|
githubId: repo.id,
|
|
name: repo.name,
|
|
fullName: repo.full_name,
|
|
htmlUrl: repo.html_url,
|
|
private: repo.private,
|
|
defaultBranch: repo.default_branch,
|
|
}));
|
|
}
|