PR Inactivity Closer #7
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: PR Inactivity Closer | |
| on: | |
| schedule: | |
| - cron: '0 0 * * *' # Runs daily at midnight UTC | |
| workflow_dispatch: # Allows manual triggering from the Actions tab | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| jobs: | |
| close-inactive-prs: | |
| name: Close Inactive PRs | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Process Inactive Pull Requests | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const label = 'inactive'; | |
| const closeAfterDays = 5; | |
| const warnAfterDays = 3; | |
| const now = new Date(); | |
| core.info(`Checking PRs for inactivity. Warn after ${warnAfterDays} days, close after ${closeAfterDays} days.`); | |
| // Ensure the inactivity label exists in the repository | |
| try { | |
| await github.rest.issues.getLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| }); | |
| } catch { | |
| core.info(`Creating label '${label}'...`); | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: label, | |
| color: '6b7280', | |
| description: 'This PR was closed due to inactivity.', | |
| }); | |
| } | |
| // Fetch all open PRs | |
| const { data: openPRs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100, | |
| }); | |
| core.info(`Scanning ${openPRs.length} open pull requests...`); | |
| const skipLabels = ['wip', 'blocked', 'do-not-merge']; | |
| for (const pr of openPRs) { | |
| // Draft PRs are intentionally long-lived | |
| if (pr.draft) { | |
| core.info(`PR #${pr.number} is a draft. Skipping.`); | |
| continue; | |
| } | |
| // Maintainer and collaborator PRs should not be auto-closed | |
| if (['COLLABORATOR', 'MEMBER', 'OWNER'].includes(pr.author_association)) { | |
| core.info(`PR #${pr.number} is from a maintainer (@${pr.user.login}). Skipping.`); | |
| continue; | |
| } | |
| // PRs carrying exclusion labels | |
| const prLabelNames = (pr.labels || []).map(l => l.name.toLowerCase()); | |
| if (prLabelNames.some(l => skipLabels.includes(l))) { | |
| core.info(`PR #${pr.number} has a wip/blocked/do-not-merge label. Skipping.`); | |
| continue; | |
| } | |
| const updatedAt = new Date(pr.updated_at); | |
| const daysSinceUpdate = Math.floor((now.getTime() - updatedAt.getTime()) / (1000 * 60 * 60 * 24)); | |
| if (daysSinceUpdate >= closeAfterDays) { | |
| core.info(`PR #${pr.number} (@${pr.user.login}) is inactive. Last updated: ${pr.updated_at}. Closing...`); | |
| // 1. Add inactivity label | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: [label], | |
| }); | |
| // 2. Post explanatory comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: `🤖 Hey @${pr.user.login}, this pull request has been automatically closed because it has been inactive for ${closeAfterDays} days.\n\nIf you are still interested in fixing the issue and contributing, please feel free to pull the latest changes, resolve any conflicts, and reopen this PR at any time! We'd love to review your work. Thank you! ❤️`, | |
| }); | |
| // 3. Close the PR | |
| await github.rest.pulls.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: pr.number, | |
| state: 'closed', | |
| }); | |
| core.info(`Successfully closed PR #${pr.number}.`); | |
| } else if (daysSinceUpdate >= warnAfterDays) { | |
| // Avoid posting multiple warnings on consecutive runs | |
| const { data: existingComments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| per_page: 10, | |
| }); | |
| if (existingComments.some(c => c.body && c.body.includes('<!-- inactivity-warning -->'))) { | |
| core.info(`PR #${pr.number} already has a warning. Skipping.`); | |
| continue; | |
| } | |
| core.info(`PR #${pr.number} (@${pr.user.login}) has been inactive for ${daysSinceUpdate} days. Posting warning...`); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: `⚠️ Hey @${pr.user.login}, this pull request has been inactive for ${daysSinceUpdate} days. It will be automatically closed in ${closeAfterDays - daysSinceUpdate} days if no further activity occurs.\n\nIf you are still working on this, please push your latest changes or leave a comment to keep it active.\n\n<!-- inactivity-warning -->`, | |
| }); | |
| core.info(`Warning posted on PR #${pr.number}.`); | |
| } else { | |
| core.info(`PR #${pr.number} is active. Last updated: ${pr.updated_at}. Skipping.`); | |
| } | |
| } |