close-issues.ts 2.4 KB

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