瀏覽代碼

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
Dax Raad 3 周之前
父節點
當前提交
79e9d19019
共有 3 個文件被更改,包括 115 次插入34 次删除
  1. 23 0
      .github/workflows/close-issues.yml
  2. 0 34
      .github/workflows/stale-issues.yml
  3. 92 0
      script/github/close-issues.ts

+ 23 - 0
.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

+ 0 - 34
.github/workflows/stale-issues.yml

@@ -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"

+ 92 - 0
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)
+})