chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
import { App, type Octokit } from "octokit";
|
||||
import { env } from "../env.server";
|
||||
import { prisma } from "~/db.server";
|
||||
import { logger } from "./logger.server";
|
||||
import { errAsync, fromPromise, okAsync, type ResultAsync } from "neverthrow";
|
||||
|
||||
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,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a branch exists in a GitHub repository
|
||||
*/
|
||||
export function checkGitHubBranchExists(
|
||||
installationId: number,
|
||||
fullRepoName: string,
|
||||
branch: string
|
||||
): ResultAsync<boolean, { type: "other" | "github_app_not_enabled"; cause?: unknown }> {
|
||||
if (!githubApp) {
|
||||
return errAsync({ type: "github_app_not_enabled" as const });
|
||||
}
|
||||
|
||||
if (!branch || branch.trim() === "") {
|
||||
return okAsync(false);
|
||||
}
|
||||
|
||||
const [owner, repo] = fullRepoName.split("/");
|
||||
|
||||
const getOctokit = () =>
|
||||
fromPromise(githubApp.getInstallationOctokit(installationId), (error) => ({
|
||||
type: "other" as const,
|
||||
cause: error,
|
||||
}));
|
||||
|
||||
const getBranch = (octokit: Octokit) =>
|
||||
fromPromise(
|
||||
octokit.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
}),
|
||||
(error) => ({
|
||||
type: "other" as const,
|
||||
cause: error,
|
||||
})
|
||||
);
|
||||
|
||||
return getOctokit()
|
||||
.andThen((octokit) => getBranch(octokit))
|
||||
.map(() => true)
|
||||
.orElse((error) => {
|
||||
if (
|
||||
error.cause &&
|
||||
error.cause instanceof Error &&
|
||||
"status" in error.cause &&
|
||||
error.cause.status === 404
|
||||
) {
|
||||
return okAsync(false);
|
||||
}
|
||||
|
||||
return errAsync(error);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user