mirror of
https://github.com/prdlk/leetcode.git
synced 2026-09-17 07:26:27 +00:00
94 lines
3.3 KiB
TypeScript
94 lines
3.3 KiB
TypeScript
/**
|
|
* GitHub REST plumbing shared by the issue reconcilers (close-solved,
|
|
* close-topics).
|
|
*
|
|
* Importing this module is side-effect free — nothing resolves credentials or
|
|
* touches the network until github() is awaited — so a script can import it
|
|
* without inheriting another script's pipeline.
|
|
*
|
|
* Repo: GITHUB_REPOSITORY, else the `origin` remote.
|
|
* Auth: GH_TOKEN / GITHUB_TOKEN, else `gh auth token`.
|
|
*/
|
|
import { $ } from "bun";
|
|
import { join } from "node:path";
|
|
|
|
const ROOT = join(import.meta.dir, "..", "..");
|
|
|
|
/** Page size used for every list endpoint; also the "more pages" threshold. */
|
|
const PER_PAGE = 100;
|
|
|
|
export interface GitHub {
|
|
/** `owner/name`. */
|
|
repo: string;
|
|
/** One authenticated request against api.github.com; throws on non-2xx. */
|
|
api(path: string, init?: RequestInit): Promise<unknown>;
|
|
/** Every page of a list endpoint, flattened into one stream of elements. */
|
|
list(path: string): AsyncGenerator<unknown, void, void>;
|
|
/** Comment on an issue, then close it as completed. */
|
|
closeIssue(issue: number, comment: string): Promise<void>;
|
|
}
|
|
|
|
async function resolveRepo(): Promise<string> {
|
|
if (process.env.GITHUB_REPOSITORY) return process.env.GITHUB_REPOSITORY;
|
|
const url = (await $`git -C ${ROOT} remote get-url origin`.text()).trim();
|
|
const m = url.match(/github\.com[:/](.+?)(?:\.git)?$/);
|
|
if (!m) throw new Error(`cannot derive owner/repo from remote: ${url}`);
|
|
return m[1]!;
|
|
}
|
|
|
|
async function resolveToken(): Promise<string> {
|
|
const env = process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
if (env) return env;
|
|
const token = (await $`gh auth token`.text()).trim();
|
|
if (!token) throw new Error("no credentials: set GH_TOKEN or run `gh auth login`");
|
|
return token;
|
|
}
|
|
|
|
export async function github(): Promise<GitHub> {
|
|
const repo = await resolveRepo();
|
|
const token = await resolveToken();
|
|
|
|
async function api(path: string, init: RequestInit = {}): Promise<unknown> {
|
|
const res = await fetch(`https://api.github.com${path}`, {
|
|
...init,
|
|
headers: {
|
|
accept: "application/vnd.github+json",
|
|
authorization: `Bearer ${token}`,
|
|
"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.json();
|
|
}
|
|
|
|
async function* list(path: string): AsyncGenerator<unknown, void, void> {
|
|
const sep = path.includes("?") ? "&" : "?";
|
|
for (let page = 1; ; page++) {
|
|
const batch = await api(`${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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Comment before closing: if the PATCH fails, the issue still carries a
|
|
* visible note of what the automation decided, instead of failing silently.
|
|
*/
|
|
async function closeIssue(issue: number, comment: string): Promise<void> {
|
|
await api(`/repos/${repo}/issues/${issue}/comments`, {
|
|
method: "POST",
|
|
body: JSON.stringify({ body: comment }),
|
|
});
|
|
await api(`/repos/${repo}/issues/${issue}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify({ state: "closed", state_reason: "completed" }),
|
|
});
|
|
}
|
|
|
|
return { repo, api, list, closeIssue };
|
|
}
|