Submatter/apps/web/server/lib/forge/adapters.ts

275 lines
7.9 KiB
TypeScript
Raw Normal View History

import type { ForgeType, RepoMetadata } from "@tasks/shared/connected-repos";
export type ForgeAdapterContext = {
apiBaseUrl: string;
owner: string;
repo: string;
token?: string;
};
export type CreatePullRequestInput = {
base: string;
head: string;
title: string;
body: string;
draft: boolean;
};
export type CreatePullRequestResult = {
url: string;
number: number;
};
export interface ForgeAdapter {
readonly forgeType: ForgeType;
fetchRepoMetadata(ctx: ForgeAdapterContext): Promise<RepoMetadata>;
createPullRequest(
ctx: ForgeAdapterContext,
input: CreatePullRequestInput,
): Promise<CreatePullRequestResult>;
}
async function readJsonResponse(response: Response): Promise<unknown> {
const text = await response.text();
if (!text) {
return null;
}
try {
return JSON.parse(text) as unknown;
} catch {
throw new Error(`Forge API returned non-JSON response (${response.status}).`);
}
}
function authHeaders(token: string | undefined): HeadersInit {
if (!token) {
return {};
}
return { Authorization: `Bearer ${token}` };
}
async function createGithubPullRequest(
ctx: ForgeAdapterContext,
input: CreatePullRequestInput,
): Promise<CreatePullRequestResult> {
const url = `${ctx.apiBaseUrl.replace(/\/$/, "")}/repos/${ctx.owner}/${ctx.repo}/pulls`;
const response = await fetch(url, {
method: "POST",
headers: {
Accept: "application/vnd.github+json",
"Content-Type": "application/json",
...authHeaders(ctx.token),
},
body: JSON.stringify({
title: input.title,
body: input.body,
head: input.head,
base: input.base,
draft: input.draft,
}),
});
const body = await readJsonResponse(response);
if (!response.ok) {
const message =
typeof body === "object" &&
body !== null &&
"message" in body &&
typeof (body as { message: unknown }).message === "string"
? (body as { message: string }).message
: `GitHub API error (${response.status})`;
throw new Error(message);
}
const htmlUrl =
typeof body === "object" &&
body !== null &&
"html_url" in body &&
typeof (body as { html_url: unknown }).html_url === "string"
? (body as { html_url: string }).html_url
: typeof body === "object" &&
body !== null &&
"url" in body &&
typeof (body as { url: unknown }).url === "string"
? (body as { url: string }).url
: `${ctx.apiBaseUrl.replace(/\/$/, "")}/${ctx.owner}/${ctx.repo}/pulls/unknown`;
const number =
typeof body === "object" &&
body !== null &&
"number" in body &&
typeof (body as { number: unknown }).number === "number"
? (body as { number: number }).number
: 0;
return { url: htmlUrl, number };
}
async function createForgejoPullRequest(
ctx: ForgeAdapterContext,
input: CreatePullRequestInput,
): Promise<CreatePullRequestResult> {
const url = `${ctx.apiBaseUrl.replace(/\/$/, "")}/repos/${ctx.owner}/${ctx.repo}/pulls`;
const response = await fetch(url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
...authHeaders(ctx.token),
},
body: JSON.stringify({
title: input.title,
body: input.body,
head: input.head,
base: input.base,
draft: input.draft,
}),
});
const body = await readJsonResponse(response);
if (!response.ok) {
const message =
typeof body === "object" &&
body !== null &&
"message" in body &&
typeof (body as { message: unknown }).message === "string"
? (body as { message: string }).message
: `Forge API error (${response.status})`;
throw new Error(message);
}
const htmlUrl =
typeof body === "object" &&
body !== null &&
"html_url" in body &&
typeof (body as { html_url: unknown }).html_url === "string"
? (body as { html_url: string }).html_url
: typeof body === "object" &&
body !== null &&
"url" in body &&
typeof (body as { url: unknown }).url === "string"
? (body as { url: string }).url
: `${ctx.apiBaseUrl.replace(/\/api\/v1$/, "")}/${ctx.owner}/${ctx.repo}/pulls/${typeof body === "object" && body !== null && "number" in body ? String((body as { number: unknown }).number) : "unknown"}`;
const number =
typeof body === "object" &&
body !== null &&
"number" in body &&
typeof (body as { number: unknown }).number === "number"
? (body as { number: number }).number
: 0;
return { url: htmlUrl, number };
}
export const githubForgeAdapter: ForgeAdapter = {
forgeType: "github",
async fetchRepoMetadata(ctx) {
const url = `${ctx.apiBaseUrl.replace(/\/$/, "")}/repos/${ctx.owner}/${ctx.repo}`;
const response = await fetch(url, {
headers: {
Accept: "application/vnd.github+json",
...authHeaders(ctx.token),
},
});
const body = await readJsonResponse(response);
if (!response.ok) {
const message =
typeof body === "object" &&
body !== null &&
"message" in body &&
typeof body.message === "string"
? body.message
: `GitHub API error (${response.status})`;
throw new Error(message);
}
if (
typeof body !== "object" ||
body === null ||
!("default_branch" in body) ||
typeof body.default_branch !== "string"
) {
throw new Error("GitHub API response missing default_branch.");
}
const cloneUrl =
typeof body === "object" &&
body !== null &&
"clone_url" in body &&
typeof body.clone_url === "string"
? body.clone_url
: `https://github.com/${ctx.owner}/${ctx.repo}.git`;
const description =
typeof body === "object" &&
body !== null &&
"description" in body &&
typeof body.description === "string"
? body.description
: undefined;
return {
defaultBranch: body.default_branch,
cloneUrl,
description,
};
},
async createPullRequest(ctx, input) {
return createGithubPullRequest(ctx, input);
},
};
export const forgejoForgeAdapter: ForgeAdapter = {
forgeType: "forgejo",
async fetchRepoMetadata(ctx) {
const url = `${ctx.apiBaseUrl.replace(/\/$/, "")}/repos/${ctx.owner}/${ctx.repo}`;
const response = await fetch(url, {
headers: {
Accept: "application/json",
...authHeaders(ctx.token),
},
});
const body = await readJsonResponse(response);
if (!response.ok) {
const message =
typeof body === "object" &&
body !== null &&
"message" in body &&
typeof body.message === "string"
? body.message
: `Forge API error (${response.status})`;
throw new Error(message);
}
if (
typeof body !== "object" ||
body === null ||
!("default_branch" in body) ||
typeof body.default_branch !== "string"
) {
throw new Error("Forge API response missing default_branch.");
}
const cloneUrl =
typeof body === "object" &&
body !== null &&
"clone_url" in body &&
typeof body.clone_url === "string"
? body.clone_url
: `${ctx.apiBaseUrl.replace(/\/api\/v1$/, "")}/${ctx.owner}/${ctx.repo}.git`;
const description =
typeof body === "object" &&
body !== null &&
"description" in body &&
typeof body.description === "string"
? body.description
: undefined;
return {
defaultBranch: body.default_branch,
cloneUrl,
description,
};
},
async createPullRequest(ctx, input) {
return createForgejoPullRequest(ctx, input);
},
};
const ADAPTERS: Record<ForgeType, ForgeAdapter> = {
github: githubForgeAdapter,
forgejo: forgejoForgeAdapter,
gitlab: forgejoForgeAdapter,
bitbucket: forgejoForgeAdapter,
};
export function getForgeAdapter(forgeType: ForgeType): ForgeAdapter {
return ADAPTERS[forgeType];
}