Stale PR Management #67
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: Stale PR Management | |
| on: | |
| schedule: | |
| # Run daily at 00:00 UTC | |
| - cron: '0 0 * * *' | |
| workflow_dispatch: # Allow manual trigger | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| jobs: | |
| stale: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Mark and close stale PRs | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const now = new Date(); | |
| const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); | |
| const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000); | |
| const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); | |
| // Get all open PRs | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100, | |
| }); | |
| console.log(`Found ${prs.length} open PRs`); | |
| for (const pr of prs) { | |
| const updatedAt = new Date(pr.updated_at); | |
| const labels = pr.labels.map(l => l.name); | |
| const prNumber = pr.number; | |
| // Skip draft PRs | |
| if (pr.draft) { | |
| console.log(`PR #${prNumber} is a draft, skipping`); | |
| continue; | |
| } | |
| // 30+ days: Close PR | |
| if (updatedAt < thirtyDaysAgo) { | |
| console.log(`PR #${prNumber} is 30+ days old, closing`); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: '🔒 This PR has been automatically closed due to 30 days of inactivity. If you would like to continue working on this, please reopen the PR and update it.', | |
| }); | |
| await github.rest.pulls.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| state: 'closed', | |
| }); | |
| continue; | |
| } | |
| // 14+ days: Add warning comment (if not already warned) | |
| if (updatedAt < fourteenDaysAgo) { | |
| console.log(`PR #${prNumber} is 14+ days old, adding warning`); | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| }); | |
| const alreadyWarned = comments.some(c => c.body.includes('This PR will be closed in 16 days')); | |
| if (!alreadyWarned) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: '⚠️ This PR has been inactive for 14 days. This PR will be closed in 16 days if there is no activity. Please update the PR or add a comment to keep it active.', | |
| }); | |
| } | |
| } | |
| // 7+ days: Add stale label | |
| if (updatedAt < sevenDaysAgo && !labels.includes('stale')) { | |
| console.log(`PR #${prNumber} is 7+ days old, marking as stale`); | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: ['stale'], | |
| }); | |
| } | |
| // Remove stale label if PR was recently updated | |
| if (updatedAt >= sevenDaysAgo && labels.includes('stale')) { | |
| console.log(`PR #${prNumber} was recently updated, removing stale label`); | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| name: 'stale', | |
| }); | |
| } catch (error) { | |
| console.log(`Could not remove stale label: ${error.message}`); | |
| } | |
| } | |
| } | |
| console.log('Stale PR management completed'); |