diff --git a/.github/workflows/close-issues.yml b/.github/workflows/close-issues.yml new file mode 100644 index 0000000000..3ee97ee535 --- /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 c265281203..0000000000 --- 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/stale@v10.2.0 - 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 0000000000..d373b4ca15 --- /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) +})