| #!/usr/bin/env bun |
|
|
| const repo = "anomalyco/opencode" |
| const days = 60 |
| const msg = `To stay organized issues are automatically closed after ${days} days of no activity. If the issue is still relevant please open a new one.` |
|
|
| const token = process.env.GITHUB_TOKEN |
| if (!token) { |
| console.error("GITHUB_TOKEN environment variable is required") |
| process.exit(1) |
| } |
|
|
| const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000) |
| const agentLogin = "opencode-agent[bot]" |
| const teamMembers = new Set( |
| (await Bun.file(new URL("../../.github/TEAM_MEMBERS", import.meta.url)).text()) |
| .split("\n") |
| .map((line) => line.trim().toLowerCase()) |
| .filter(Boolean), |
| ) |
| const teamAssociations = new Set(["OWNER", "MEMBER"]) |
|
|
| type Issue = { |
| number: number |
| updated_at: string |
| author_association: string |
| user: { login: string } | null |
| } |
|
|
| const headers = { |
| Authorization: `Bearer ${token}`, |
| "Content-Type": "application/json", |
| Accept: "application/vnd.github+json", |
| "X-GitHub-Api-Version": "2022-11-28", |
| } |
|
|
| function shouldSkip(i: Issue) { |
| const login = i.user?.login.toLowerCase() |
| return login === agentLogin || (login ? teamMembers.has(login) : false) || teamAssociations.has(i.author_association) |
| } |
|
|
| async function close(num: number) { |
| const base = `https://api.github.com/repos/${repo}/issues/${num}` |
|
|
| const comment = await fetch(`${base}/comments`, { |
| method: "POST", |
| headers, |
| body: JSON.stringify({ body: msg }), |
| }) |
| if (!comment.ok) throw new Error(`Failed to comment #${num}: ${comment.status} ${comment.statusText}`) |
|
|
| const patch = await fetch(base, { |
| method: "PATCH", |
| headers, |
| body: JSON.stringify({ state: "closed", state_reason: "not_planned" }), |
| }) |
| if (!patch.ok) throw new Error(`Failed to close #${num}: ${patch.status} ${patch.statusText}`) |
|
|
| console.log(`Closed https://github.com/${repo}/issues/${num}`) |
| } |
|
|
| async function main() { |
| let page = 1 |
| let closed = 0 |
|
|
| while (true) { |
| const res = await fetch( |
| `https://api.github.com/repos/${repo}/issues?state=open&sort=updated&direction=asc&per_page=100&page=${page}`, |
| { headers }, |
| ) |
| if (!res.ok) throw new Error(res.statusText) |
|
|
| const all = (await res.json()) as Issue[] |
| if (all.length === 0) break |
| console.log(`Fetched page ${page} ${all.length} issues`) |
|
|
| const stale: number[] = [] |
| for (const i of all) { |
| const updated = new Date(i.updated_at) |
| if (updated < cutoff) { |
| if (shouldSkip(i)) { |
| console.log(`Skipping stale issue #${i.number}; author ${i.user?.login ?? "unknown"} is exempt`) |
| continue |
| } |
| stale.push(i.number) |
| } else { |
| console.log(`\nFound fresh issue #${i.number}, stopping`) |
| if (stale.length > 0) { |
| for (const num of stale) { |
| await close(num) |
| closed++ |
| } |
| } |
| console.log(`Closed ${closed} issues total`) |
| return |
| } |
| } |
|
|
| if (stale.length > 0) { |
| for (const num of stale) { |
| await close(num) |
| closed++ |
| } |
| } |
|
|
| page++ |
| } |
|
|
| console.log(`Closed ${closed} issues total`) |
| } |
|
|
| main().catch((err) => { |
| console.error("Error:", err) |
| process.exit(1) |
| }) |
|
|