| |
| |
|
|
| import type { Issue, Label, PullRequest } from "./types.js"; |
|
|
| |
| |
| |
|
|
| |
| export interface GitHubApi { |
| |
| getIssue(number: number): Promise<Issue>; |
| |
| getPullRequest(number: number): Promise<PullRequest>; |
| |
| |
| listIssuesWithLabelPrefix(prefix: string): Promise<Issue[]>; |
| |
| addLabels(target: number, labels: string[]): Promise<void>; |
| |
| removeLabel(target: number, label: string): Promise<void>; |
| |
| addComment(target: number, body: string): Promise<void>; |
| } |
|
|
| |
| |
| |
|
|
| |
| export class GitHubRestApi implements GitHubApi { |
| private readonly base: string; |
| private readonly headers: Record<string, string>; |
|
|
| constructor( |
| private readonly owner: string, |
| private readonly repo: string, |
| token: string |
| ) { |
| this.base = `https://api.github.com/repos/${owner}/${repo}`; |
| this.headers = { |
| Authorization: `Bearer ${token}`, |
| Accept: "application/vnd.github+json", |
| "X-GitHub-Api-Version": "2022-11-28", |
| "User-Agent": "bounty-bot/v2", |
| "Content-Type": "application/json", |
| }; |
| } |
|
|
| private async request<T>(method: string, path: string, body?: unknown): Promise<T> { |
| const res = await fetch(`${this.base}${path}`, { |
| method, |
| headers: this.headers, |
| body: body !== undefined ? JSON.stringify(body) : undefined, |
| }); |
|
|
| if (!res.ok) { |
| const text = await res.text().catch(() => ""); |
| throw new Error(`GitHub API ${method} ${path} → ${res.status}: ${text}`); |
| } |
|
|
| |
| if (res.status === 204) return {} as T; |
|
|
| return res.json() as Promise<T>; |
| } |
|
|
| async getIssue(number: number): Promise<Issue> { |
| return this.request<Issue>("GET", `/issues/${number}`); |
| } |
|
|
| async getPullRequest(number: number): Promise<PullRequest> { |
| return this.request<PullRequest>("GET", `/pulls/${number}`); |
| } |
|
|
| async listIssuesWithLabelPrefix(prefix: string): Promise<Issue[]> { |
| const results: Issue[] = []; |
| let page = 1; |
| while (true) { |
| const batch = await this.request<Issue[]>( |
| "GET", |
| `/issues?state=open&per_page=100&page=${page}` |
| ); |
| if (batch.length === 0) break; |
| for (const issue of batch) { |
| |
| if (issue.pull_request !== undefined) continue; |
| if (issue.labels.some((l) => l.name.startsWith(prefix))) { |
| results.push(issue); |
| } |
| } |
| if (batch.length < 100) break; |
| page++; |
| } |
| return results; |
| } |
|
|
| async addLabels(target: number, labels: string[]): Promise<void> { |
| await this.request("POST", `/issues/${target}/labels`, { labels }); |
| } |
|
|
| async removeLabel(target: number, label: string): Promise<void> { |
| await this.request("DELETE", `/issues/${target}/labels/${encodeURIComponent(label)}`); |
| } |
|
|
| async addComment(target: number, body: string): Promise<void> { |
| await this.request("POST", `/issues/${target}/comments`, { body }); |
| } |
| } |
|
|