summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-03-24 23:50:35 -0400
committerDax Raad <[email protected]>2026-03-24 23:50:35 -0400
commit79e9d19019e4edd43c6001545a2cbbbd37d8c14f (patch)
treed0b2fe9406a59e4f2e8020769e88803ba8c2d5b5
parent958a80cc052b9b342dfa2a92c0a4caf1c4418fa9 (diff)
downloadopencode-79e9d19019e4edd43c6001545a2cbbbd37d8c14f.tar.gz
opencode-79e9d19019e4edd43c6001545a2cbbbd37d8c14f.zip
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
-rw-r--r--.github/workflows/close-issues.yml23
-rw-r--r--.github/workflows/stale-issues.yml34
-rwxr-xr-xscript/github/close-issues.ts92
3 files changed, 115 insertions, 34 deletions
diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml
new file mode 100644
index 000000000..3ee97ee53
--- /dev/null
+++ b/.github/workflows/close-issues.yml
@@ -0,0 +1,23 @@
+name: close-issues
+
+on:
+ schedule:
+ - cron: "0 2 * * *" # Daily at 2:00 AM
+ workflow_dispatch:
+
+jobs:
+ close:
+ runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+
+ - name: Close stale issues
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: bun script/github/close-issues.ts
diff --git a/.github/workflows/stale-issues.yml b/.github/workflows/stale-issues.yml
deleted file mode 100644
index c26528120..000000000
--- a/.github/workflows/stale-issues.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-name: stale-issues
-
-on:
- schedule:
- - cron: "30 1 * * *" # Daily at 1:30 AM
- workflow_dispatch:
-
-env:
- DAYS_BEFORE_STALE: 90
- DAYS_BEFORE_CLOSE: 7
-
-jobs:
- stale:
- runs-on: ubuntu-latest
- permissions:
- issues: write
- steps:
- - uses: actions/[email protected]
- with:
- operations-per-run: 1000
- days-before-stale: ${{ env.DAYS_BEFORE_STALE }}
- days-before-close: ${{ env.DAYS_BEFORE_CLOSE }}
- stale-issue-label: "stale"
- close-issue-message: |
- [automated] Closing due to ${{ env.DAYS_BEFORE_STALE }}+ days of inactivity.
-
- Feel free to reopen if you still need this!
- stale-issue-message: |
- [automated] This issue has had no activity for ${{ env.DAYS_BEFORE_STALE }} days.
-
- It will be closed in ${{ env.DAYS_BEFORE_CLOSE }} days if there's no new activity.
- remove-stale-when-updated: true
- exempt-issue-labels: "pinned,security,feature-request,on-hold"
- start-date: "2025-12-27"
diff --git a/script/github/close-issues.ts b/script/github/close-issues.ts
new file mode 100755
index 000000000..d373b4ca1
--- /dev/null
+++ b/script/github/close-issues.ts
@@ -0,0 +1,92 @@
+#!/usr/bin/env bun
+
+const repo = "anomalyco/opencode"
+const days = 60
+const msg =
+ "To stay organized issues are automatically closed after 90 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)
+
+type Issue = {
+ number: number
+ updated_at: string
+}
+
+const headers = {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ Accept: "application/vnd.github+json",
+ "X-GitHub-Api-Version": "2022-11-28",
+}
+
+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
+
+ const stale: number[] = []
+ for (const i of all) {
+ const updated = new Date(i.updated_at)
+ if (updated < cutoff) {
+ stale.push(i.number)
+ } else {
+ console.log(`\nFound fresh issue #${i.number}, stopping`)
+ if (stale.length > 0) {
+ await Promise.all(stale.map(close))
+ closed += stale.length
+ }
+ console.log(`Closed ${closed} issues total`)
+ return
+ }
+ }
+
+ if (stale.length > 0) {
+ await Promise.all(stale.map(close))
+ closed += stale.length
+ }
+
+ page++
+ }
+
+ console.log(`Closed ${closed} issues total`)
+}
+
+main().catch((err) => {
+ console.error("Error:", err)
+ process.exit(1)
+})