close-issues.ts 2.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. #!/usr/bin/env bun
  2. const repo = "anomalyco/opencode"
  3. const days = 60
  4. const msg =
  5. "To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one."
  6. const token = process.env.GITHUB_TOKEN
  7. if (!token) {
  8. console.error("GITHUB_TOKEN environment variable is required")
  9. process.exit(1)
  10. }
  11. const cutoff = new Date(Date.now() - days * 24 * 60 * 60 * 1000)
  12. type Issue = {
  13. number: number
  14. updated_at: string
  15. }
  16. const headers = {
  17. Authorization: `Bearer ${token}`,
  18. "Content-Type": "application/json",
  19. Accept: "application/vnd.github+json",
  20. "X-GitHub-Api-Version": "2022-11-28",
  21. }
  22. async function close(num: number) {
  23. const base = `https://api.github.com/repos/${repo}/issues/${num}`
  24. const comment = await fetch(`${base}/comments`, {
  25. method: "POST",
  26. headers,
  27. body: JSON.stringify({ body: msg }),
  28. })
  29. if (!comment.ok) throw new Error(`Failed to comment #${num}: ${comment.status} ${comment.statusText}`)
  30. const patch = await fetch(base, {
  31. method: "PATCH",
  32. headers,
  33. body: JSON.stringify({ state: "closed", state_reason: "not_planned" }),
  34. })
  35. if (!patch.ok) throw new Error(`Failed to close #${num}: ${patch.status} ${patch.statusText}`)
  36. console.log(`Closed https://github.com/${repo}/issues/${num}`)
  37. }
  38. async function main() {
  39. let page = 1
  40. let closed = 0
  41. while (true) {
  42. const res = await fetch(
  43. `https://api.github.com/repos/${repo}/issues?state=open&sort=updated&direction=asc&per_page=100&page=${page}`,
  44. { headers },
  45. )
  46. if (!res.ok) throw new Error(res.statusText)
  47. const all = (await res.json()) as Issue[]
  48. if (all.length === 0) break
  49. const stale: number[] = []
  50. for (const i of all) {
  51. const updated = new Date(i.updated_at)
  52. if (updated < cutoff) {
  53. stale.push(i.number)
  54. } else {
  55. console.log(`\nFound fresh issue #${i.number}, stopping`)
  56. if (stale.length > 0) {
  57. await Promise.all(stale.map(close))
  58. closed += stale.length
  59. }
  60. console.log(`Closed ${closed} issues total`)
  61. return
  62. }
  63. }
  64. if (stale.length > 0) {
  65. await Promise.all(stale.map(close))
  66. closed += stale.length
  67. }
  68. page++
  69. }
  70. console.log(`Closed ${closed} issues total`)
  71. }
  72. main().catch((err) => {
  73. console.error("Error:", err)
  74. process.exit(1)
  75. })