Files
triggerdotdev--trigger.dev/apps/webapp/app/services/github/githubApp.server.ts
T

271 lines
7.0 KiB
TypeScript

import { Webhooks, EmitterWebhookEvent } from "@octokit/webhooks";
import { Octokit } from "@octokit/rest";
import { createAppAuth } from "@octokit/auth-app";
import { OAuthApp } from "@octokit/oauth-app";
import { createUnauthenticatedAuth } from "@octokit/auth-unauthenticated";
import { env } from "~/env.server";
import { Options } from "@octokit/oauth-app/dist-types/types";
import type { Endpoints } from "@octokit/types";
import { z } from "zod";
import { taskQueue } from "../messageBroker.server";
export const octokit = env.GITHUB_APP_PRIVATE_KEY
? new Octokit({
authStrategy: createAppAuth,
auth: {
appId: env.GITHUB_APP_ID,
privateKey: Buffer.from(env.GITHUB_APP_PRIVATE_KEY, "base64").toString(
"utf8"
),
clientId: env.GITHUB_APP_CLIENT_ID,
clientSecret: env.GITHUB_APP_CLIENT_SECRET,
},
log: {
debug: console.log,
info: console.log,
warn: console.warn,
error: console.error,
},
})
: undefined;
export const oauthApp = createOauthApp();
export const webhooks = createWebhooks();
declare global {
var __github_webhooks__:
| Webhooks<EmitterWebhookEvent & { octokit: Octokit }>
| undefined;
var __github_oauth_app__: OAuthApp<Options<"github-app">> | undefined;
}
function createOauthApp() {
if (typeof global.__github_oauth_app__ !== "undefined") {
return global.__github_oauth_app__;
}
if (typeof env.GITHUB_APP_CLIENT_ID === "undefined") {
return;
}
if (typeof env.GITHUB_APP_CLIENT_SECRET === "undefined") {
return;
}
global.__github_oauth_app__ = new OAuthApp({
clientId: env.GITHUB_APP_CLIENT_ID,
clientSecret: env.GITHUB_APP_CLIENT_SECRET,
clientType: "github-app",
});
global.__github_oauth_app__.on("token", async (event) => {});
return __github_oauth_app__;
}
function createWebhooks() {
if (typeof global.__github_webhooks__ !== "undefined") {
return global.__github_webhooks__;
}
if (
typeof env.GITHUB_APP_WEBHOOK_SECRET === "undefined" ||
typeof octokit === "undefined"
) {
return;
}
global.__github_webhooks__ = __webhooks(
octokit,
env.GITHUB_APP_WEBHOOK_SECRET
);
global.__github_webhooks__.on("push", ({ octokit, payload }) => {});
global.__github_webhooks__.on(
"installation.deleted",
async ({ octokit, payload }) => {
await taskQueue.publish("GITHUB_APP_INSTALLATION_DELETED", {
id: payload.installation.id,
});
}
);
global.__github_webhooks__.on(
"repository.created",
async ({ octokit, payload }) => {
await taskQueue.publish(
"GITHUB_APP_REPOSITORY_CREATED",
{
id: payload.repository.id,
},
{},
{ deliverAfter: 1000 * 10 }
);
}
);
global.__github_webhooks__.onAny(({ id, name, payload }) => {
console.log(
`[github-webhook] ${name} event received: ${id}`,
JSON.stringify(payload, null, 2)
);
});
return global.__github_webhooks__;
}
function __webhooks(
appOctokit: Octokit,
secret: string
// Explict return type for better debugability and performance,
// see https://github.com/octokit/app.js/pull/201
): Webhooks<EmitterWebhookEvent & { octokit: Octokit }> {
return new Webhooks({
secret,
transform: async (event) => {
if (
!("installation" in event.payload) ||
typeof event.payload.installation !== "object"
) {
const octokit = new (appOctokit.constructor as typeof Octokit)({
authStrategy: createUnauthenticatedAuth,
auth: {
reason: `"installation" key missing in webhook event payload`,
},
});
return {
...event,
octokit,
};
}
const installationId = event.payload.installation.id;
const octokit = (await appOctokit.auth({
type: "installation",
installationId,
factory(auth: any) {
return new auth.octokit.constructor({
...auth.octokitOptions,
authStrategy: createAppAuth,
...{
auth: {
...auth,
installationId,
},
},
});
},
})) as Octokit;
// set `x-github-delivery` header on all requests sent in response to the current
// event. This allows GitHub Support to correlate the request with the event.
// This is not documented and not considered public API, the header may change.
// Once we document this as best practice on https://docs.github.com/en/rest/guides/best-practices-for-integrators
// we will make it official
/* istanbul ignore next */
octokit.hook.before("request", (options) => {
options.headers["x-github-delivery"] = event.id;
});
return {
...event,
octokit,
};
},
});
}
type GetAppInstallationEndpoint =
Endpoints["GET /app/installations/{installation_id}"];
export async function getAppInstallation({
installation_id,
}: GetAppInstallationEndpoint["parameters"]) {
if (typeof octokit === "undefined") {
return;
}
const response = await octokit.request(
"GET /app/installations/{installation_id}",
{
installation_id,
}
);
return response.data;
}
type CreateRepositoryFromTemplateEndpoint =
Endpoints["POST /repos/{template_owner}/{template_repo}/generate"];
export async function createRepositoryFromTemplate(
parameters: CreateRepositoryFromTemplateEndpoint["parameters"],
{ installationId }: { installationId?: number }
) {
if (typeof octokit === "undefined") {
return;
}
const kit = installationId ? await getOctokit(installationId) : octokit;
const response = await kit.request(
"POST /repos/{template_owner}/{template_repo}/generate",
parameters
);
return response.data;
}
export async function getOctokitRest(installationId: number) {
const installationKit = await getOctokit(installationId);
return installationKit.rest;
}
async function getOctokit(installationId: number): Promise<Octokit> {
return octokit!.auth({
type: "installation",
installationId,
factory(auth: any) {
return new auth.octokit.constructor({
...auth.octokitOptions,
authStrategy: createAppAuth,
...{
auth: {
...auth,
installationId,
},
},
});
},
}) as Promise<Octokit>;
}
export const AccountSchema = z.object({
login: z.string(),
id: z.number(),
node_id: z.string(),
name: z.string().optional(),
email: z.string().optional().nullable(),
avatar_url: z.string(),
gravatar_id: z.string(),
url: z.string(),
html_url: z.string(),
followers_url: z.string(),
following_url: z.string(),
gists_url: z.string(),
starred_url: z.string(),
subscriptions_url: z.string(),
organizations_url: z.string(),
repos_url: z.string(),
events_url: z.string(),
received_events_url: z.string(),
type: z.union([
z.literal("Bot"),
z.literal("User"),
z.literal("Organization"),
]),
site_admin: z.boolean(),
});