💤 Auto Unassign Inactive Issues #201
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
| name: 💤 Auto Unassign Inactive Issues | |
| on: | |
| schedule: | |
| - cron: '0 */12 * * *' | |
| workflow_dispatch: | |
| permissions: | |
| issues: write | |
| pull-requests: read | |
| contents: read | |
| jobs: | |
| auto-unassign: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Auto unassign inactive contributors | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const INACTIVE_DAYS = 3; | |
| const now = new Date(); | |
| const cutoff = new Date(now.getTime() - INACTIVE_DAYS * 24 * 60 * 60 * 1000); | |
| const issues = await github.paginate( | |
| github.rest.issues.listForRepo, | |
| { owner, repo, state: 'open', assignee: '*', per_page: 100 } | |
| ); | |
| // Fetch open PRs once outside the loop | |
| const openPRs = await github.paginate( | |
| github.rest.pulls.list, | |
| { owner, repo, state: 'open', per_page: 100 } | |
| ); | |
| for (const issue of issues) { | |
| if (issue.pull_request) continue; // skip PRs | |
| // Allow the script to fetch comments to accurately check per-assignee activity | |
| const comments = await github.paginate( | |
| github.rest.issues.listComments, | |
| { owner, repo, issue_number: issue.number, per_page: 100 } | |
| ); | |
| const events = await github.paginate( | |
| github.rest.issues.listEventsForTimeline, | |
| { owner, repo, issue_number: issue.number, per_page: 100 } | |
| ); | |
| const assignees = (issue.assignees || []).map(a => a.login); | |
| for (const assignee of assignees) { | |
| const assignedEvent = events | |
| .filter(e => e.event === 'assigned' && e.assignee && e.assignee.login === assignee) | |
| .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))[0]; | |
| if (!assignedEvent) continue; | |
| const assignedAt = new Date(assignedEvent.created_at); | |
| if (assignedAt > cutoff) continue; | |
| // Check for activity after assignment | |
| const contributorComments = comments.filter(c => | |
| c.user && c.user.login === assignee && new Date(c.created_at) > assignedAt | |
| ); | |
| const prs = openPRs; | |
| const linkedPR = prs.find(pr => { | |
| const body = (pr.body || '').toLowerCase(); | |
| return pr.user && pr.user.login === assignee && ( | |
| body.includes(`#${issue.number}`) || | |
| body.includes(`closes #${issue.number}`) || | |
| body.includes(`fixes #${issue.number}`) | |
| ); | |
| }); | |
| if (contributorComments.length > 0 || linkedPR) continue; | |
| await github.rest.issues.removeAssignees({ | |
| owner, repo, issue_number: issue.number, assignees: [assignee] | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner, repo, issue_number: issue.number, | |
| body: `⚠️ Auto-unassigned @${assignee} due to inactivity.\n\n` + | |
| `Per the contribution guidelines, assigned contributors are expected to show progress within ${INACTIVE_DAYS} days.\n\n` + | |
| `You may re-request assignment later if you intend to continue working on this issue.` | |
| }); | |
| } | |
| } |