/** * GitHub REST + GraphQL client on GH_PAT. Hand-rolled fetch, no SDK — * matching the repo's dependency-free automation rule. */ export interface GitHub { repo: string; rest(path: string, init?: RequestInit): Promise; graphql(query: string, variables?: Record): Promise; list(path: string): AsyncGenerator; /** Comment first, then close — a failed close still leaves a visible note. */ closeIssue(issue: number, comment: string): Promise; comment(issue: number, body: string): Promise; react(commentId: number, content: string): Promise; } const PER_PAGE = 100; export function github(token: string, repo: string): GitHub { async function rest(path: string, init: RequestInit = {}): Promise { const res = await fetch(`https://api.github.com${path}`, { ...init, headers: { accept: "application/vnd.github+json", authorization: `Bearer ${token}`, "user-agent": "srs-api", "x-github-api-version": "2022-11-28", ...(init.body ? { "content-type": "application/json" } : {}), }, }); if (!res.ok) { throw new Error(`${init.method ?? "GET"} ${path} -> ${res.status} ${await res.text()}`); } return res.status === 204 ? null : res.json(); } async function graphql( query: string, variables: Record = {}, ): Promise { const res = await fetch("https://api.github.com/graphql", { method: "POST", headers: { authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": "srs-api", }, body: JSON.stringify({ query, variables }), }); if (!res.ok) throw new Error(`graphql -> ${res.status} ${await res.text()}`); const payload = (await res.json()) as { data?: unknown; errors?: { message: string }[] }; if (payload.errors?.length) throw new Error(payload.errors.map((e) => e.message).join("; ")); return payload.data; } async function* list(path: string): AsyncGenerator { const sep = path.includes("?") ? "&" : "?"; for (let page = 1; ; page++) { const batch = await rest(`${path}${sep}per_page=${PER_PAGE}&page=${page}`); if (!Array.isArray(batch)) throw new Error(`unexpected ${path} payload: not an array`); yield* batch; if (batch.length < PER_PAGE) return; } } return { repo, rest, graphql, list, async closeIssue(issue, comment) { await rest(`/repos/${repo}/issues/${issue}/comments`, { method: "POST", body: JSON.stringify({ body: comment }), }); await rest(`/repos/${repo}/issues/${issue}`, { method: "PATCH", body: JSON.stringify({ state: "closed", state_reason: "completed" }), }); }, async comment(issue, body) { await rest(`/repos/${repo}/issues/${issue}/comments`, { method: "POST", body: JSON.stringify({ body }), }); }, async react(commentId, content) { await rest(`/repos/${repo}/issues/comments/${commentId}/reactions`, { method: "POST", body: JSON.stringify({ content }), }); }, }; }