close-issues.ts 2.4 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  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: "completed" }),
  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. console.log(`Fetched page ${page} ${all.length} issues`)
  50. const stale: number[] = []
  51. for (const i of all) {
  52. const updated = new Date(i.updated_at)
  53. if (updated < cutoff) {
  54. stale.push(i.number)
  55. } else {
  56. console.log(`\nFound fresh issue #${i.number}, stopping`)
  57. if (stale.length > 0) {
  58. for (const num of stale) {
  59. await close(num)
  60. closed++
  61. }
  62. }
  63. console.log(`Closed ${closed} issues total`)
  64. return
  65. }
  66. }
  67. if (stale.length > 0) {
  68. for (const num of stale) {
  69. await close(num)
  70. closed++
  71. }
  72. }
  73. page++
  74. }
  75. console.log(`Closed ${closed} issues total`)
  76. }
  77. main().catch((err) => {
  78. console.error("Error:", err)
  79. process.exit(1)
  80. })