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; createPullRequest( ctx: ForgeAdapterContext, input: CreatePullRequestInput, ): Promise; } async function readJsonResponse(response: Response): Promise { 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 { 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 { 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 = { github: githubForgeAdapter, forgejo: forgejoForgeAdapter, gitlab: forgejoForgeAdapter, bitbucket: forgejoForgeAdapter, }; export function getForgeAdapter(forgeType: ForgeType): ForgeAdapter { return ADAPTERS[forgeType]; }