Skip to content

Commit 79e9d19

Browse files
committed
Add close-issues script and GitHub Action
- Create script/github/close-issues.ts to close stale issues after 60 days - Add GitHub Action workflow to run daily at 2 AM - Remove old stale-issues workflow to avoid conflicts
1 parent 958a80c commit 79e9d19

3 files changed

Lines changed: 115 additions & 34 deletions

File tree

.github/workflows/close-issues.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
name: close-issues
2+
3+
on:
4+
schedule:
5+
- cron: "0 2 * * *" # Daily at 2:00 AM
6+
workflow_dispatch:
7+
8+
jobs:
9+
close:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
issues: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: oven-sh/setup-bun@v2
17+
with:
18+
bun-version: latest
19+
20+
- name: Close stale issues
21+
env:
22+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23+
run: bun script/github/close-issues.ts

.github/workflows/stale-issues.yml

Lines changed: 0 additions & 34 deletions
This file was deleted.

script/github/close-issues.ts

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

0 commit comments

Comments
 (0)