Issue: Close stale issues #24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Auto-close stale issues that have no response from the author. | |
| # | |
| # Runs daily. Adds a "waiting-for-response" label when a maintainer | |
| # replies. If the author doesn't follow up within 14 days, the issue | |
| # is closed automatically with a polite message. | |
| name: "Issue: Close stale issues" | |
| on: | |
| schedule: | |
| # Run daily at 09:00 UTC | |
| - cron: "0 9 * * *" | |
| workflow_dispatch: # Allow manual trigger | |
| permissions: | |
| issues: write | |
| jobs: | |
| label-responded: | |
| # Label issues where a maintainer (repo member/owner) has replied | |
| # but the original author hasn't responded yet. | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Label issues awaiting author response | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const LABEL = 'waiting-for-response'; | |
| const STALE_DAYS = 14; | |
| // Ensure the label exists | |
| try { | |
| await github.rest.issues.getLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: LABEL, | |
| }); | |
| } catch { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: LABEL, | |
| color: 'fbca04', | |
| description: 'Awaiting response from the issue author', | |
| }); | |
| } | |
| // Get all open issues (not PRs) | |
| const issues = await github.paginate( | |
| github.rest.issues.listForRepo, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100, | |
| } | |
| ); | |
| for (const issue of issues) { | |
| // Skip pull requests | |
| if (issue.pull_request) continue; | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| per_page: 100, | |
| } | |
| ); | |
| if (comments.length === 0) continue; | |
| // Find the last comment by someone other than the author | |
| // (i.e., a maintainer response) | |
| const maintainerComments = comments.filter( | |
| c => c.user.login !== issue.user.login | |
| ); | |
| if (maintainerComments.length === 0) continue; | |
| const lastMaintainer = | |
| maintainerComments[maintainerComments.length - 1]; | |
| // Check if the author replied after the maintainer | |
| const authorRepliesAfter = comments.filter( | |
| c => | |
| c.user.login === issue.user.login && | |
| new Date(c.created_at) > new Date(lastMaintainer.created_at) | |
| ); | |
| const hasLabel = issue.labels.some(l => l.name === LABEL); | |
| if (authorRepliesAfter.length === 0) { | |
| // Maintainer replied, author hasn't followed up | |
| if (!hasLabel) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: [LABEL], | |
| }); | |
| } | |
| // Check if stale (14 days since last maintainer comment) | |
| const daysSince = Math.floor( | |
| (Date.now() - new Date(lastMaintainer.created_at)) / | |
| (1000 * 60 * 60 * 24) | |
| ); | |
| if (daysSince >= STALE_DAYS) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: [ | |
| `This issue has been automatically closed after ${STALE_DAYS} days of inactivity.`, | |
| ``, | |
| `If you still need help, feel free to reopen this issue or create a new one. We're happy to assist! π`, | |
| ].join('\n'), | |
| }); | |
| await github.rest.issues.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| state: 'closed', | |
| state_reason: 'not_planned', | |
| }); | |
| } | |
| } else { | |
| // Author replied β remove label if present | |
| if (hasLabel) { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| name: LABEL, | |
| }); | |
| } | |
| } | |
| } |