Stale Issue Management #41
  
    
      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
    
  
  
    
  | # Stale Issue Management | |
| # Automatically manage inactive issues and pull requests | |
| name: Stale Issue Management | |
| on: | |
| schedule: | |
| # Run daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| workflow_dispatch: | |
| # Allow manual triggering | |
| permissions: | |
| issues: write | |
| pull-requests: write | |
| contents: read | |
| jobs: | |
| stale: | |
| name: Mark Stale Issues | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Mark stale issues and PRs | |
| uses: actions/stale@v9 | |
| with: | |
| # Authentication | |
| repo-token: ${{ secrets.GITHUB_TOKEN }} | |
| # Stale issue configuration | |
| days-before-issue-stale: 30 | |
| days-before-issue-close: 7 | |
| stale-issue-label: 'stale' | |
| stale-issue-message: | | |
| 👋 This issue has been automatically marked as stale because it has had no activity for 30 days. | |
| **What happens next:** | |
| - This issue will be closed in 7 days if no further activity occurs | |
| - To keep this issue open, simply add a comment or reaction | |
| - If this is still relevant, please provide an update | |
| **Remove stale status:** | |
| - Comment on this issue | |
| - Add a reaction to this issue | |
| - Push commits to a linked PR | |
| - Remove the `stale` label | |
| Thank you for your contribution! 🙏 | |
| close-issue-message: | | |
| 🔒 This issue has been automatically closed due to inactivity. | |
| **Why was this closed?** | |
| - No activity for 37 days (30 days + 7 day warning) | |
| - Marked as stale with no follow-up | |
| **This issue can be reopened:** | |
| - If you're still experiencing this problem | |
| - If you have additional information to share | |
| - Simply comment and the issue will be reopened | |
| Thanks for understanding! 🙏 | |
| # Stale PR configuration | |
| days-before-pr-stale: 14 | |
| days-before-pr-close: 7 | |
| stale-pr-label: 'stale' | |
| stale-pr-message: | | |
| 👋 This pull request has been automatically marked as stale because it has had no activity for 14 days. | |
| **What happens next:** | |
| - This PR will be closed in 7 days if no further activity occurs | |
| - To keep this PR open, push new commits or add a comment | |
| **Remove stale status:** | |
| - Push new commits to this PR | |
| - Add a comment explaining the status | |
| - Request review from maintainers | |
| - Remove the `stale` label | |
| Thank you for your contribution! 🙏 | |
| close-pr-message: | | |
| 🔒 This pull request has been automatically closed due to inactivity. | |
| **Why was this closed?** | |
| - No activity for 21 days (14 days + 7 day warning) | |
| - Marked as stale with no follow-up | |
| **This PR can be reopened:** | |
| - Push new commits to the branch | |
| - Reopen the PR and add a comment | |
| - Create a fresh PR with updated changes | |
| Thanks for understanding! 🙏 | |
| # Labels that prevent stale marking | |
| exempt-issue-labels: 'priority:high,priority:critical,pinned,enhancement:approved,bug:confirmed,in-progress,has-pr' | |
| exempt-pr-labels: 'priority:high,priority:critical,pinned,work-in-progress,needs-review,approved' | |
| # Labels to remove when stale | |
| labels-to-remove-when-stale: 'needs-triage,needs-review' | |
| labels-to-remove-when-unstale: 'stale' | |
| # Additional options | |
| remove-stale-when-updated: true | |
| delete-branch: false | |
| start-date: '2024-01-01T00:00:00Z' | |
| ascending: true | |
| # Debug mode (set to true for testing) | |
| debug-only: false | |
| # Clean up closed issues | |
| cleanup-closed-issues: | |
| name: Cleanup Closed Issues | |
| runs-on: ubuntu-latest | |
| needs: stale | |
| if: always() | |
| steps: | |
| - name: Remove labels from closed issues | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Get all closed issues that still have certain labels | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'closed', | |
| labels: 'in-progress,has-pr,needs-review,needs-triage', | |
| per_page: 100 | |
| }); | |
| console.log(`Found ${issues.length} closed issues with cleanup labels`); | |
| for (const issue of issues) { | |
| const labelsToRemove = ['in-progress', 'has-pr', 'needs-review', 'needs-triage']; | |
| const currentLabels = issue.labels.map(l => l.name); | |
| for (const label of labelsToRemove) { | |
| if (currentLabels.includes(label)) { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| name: label | |
| }); | |
| console.log(`Removed ${label} from closed issue #${issue.number}`); | |
| } catch (error) { | |
| console.log(`Failed to remove ${label} from issue #${issue.number}: ${error.message}`); | |
| } | |
| } | |
| } | |
| // Add completed label if not present | |
| if (!currentLabels.includes('completed')) { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: ['completed'] | |
| }); | |
| console.log(`Added completed label to issue #${issue.number}`); | |
| } | |
| } | |
| # Generate stale issues report | |
| stale-report: | |
| name: Generate Stale Report | |
| runs-on: ubuntu-latest | |
| needs: stale | |
| if: github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Generate stale issues report | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| // Get stale issues | |
| const { data: staleIssues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'stale', | |
| per_page: 100 | |
| }); | |
| // Get stale PRs | |
| const { data: stalePRs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100 | |
| }); | |
| const stalePRsFiltered = stalePRs.filter(pr => | |
| pr.labels.some(label => label.name === 'stale') | |
| ); | |
| console.log(`📊 Stale Items Report:`); | |
| console.log(`- Stale Issues: ${staleIssues.length}`); | |
| console.log(`- Stale PRs: ${stalePRsFiltered.length}`); | |
| if (staleIssues.length > 0) { | |
| console.log('\n🐛 Stale Issues:'); | |
| staleIssues.forEach(issue => { | |
| console.log(` - #${issue.number}: ${issue.title} (created: ${issue.created_at})`); | |
| }); | |
| } | |
| if (stalePRsFiltered.length > 0) { | |
| console.log('\n🔄 Stale PRs:'); | |
| stalePRsFiltered.forEach(pr => { | |
| console.log(` - #${pr.number}: ${pr.title} (created: ${pr.created_at})`); | |
| }); | |
| } | |
| // Create summary issue if there are many stale items | |
| if (staleIssues.length + stalePRsFiltered.length >= 5) { | |
| const reportBody = [ | |
| '# 📊 Stale Items Report', | |
| '', | |
| '## Summary', | |
| `- **Stale Issues:** ${staleIssues.length}`, | |
| `- **Stale PRs:** ${stalePRsFiltered.length}`, | |
| `- **Report Date:** ${new Date().toISOString().split('T')[0]}`, | |
| '' | |
| ]; | |
| if (staleIssues.length > 0) { | |
| reportBody.push('## 🐛 Stale Issues'); | |
| staleIssues.forEach(issue => { | |
| reportBody.push(`- #${issue.number}: ${issue.title}`); | |
| }); | |
| reportBody.push(''); | |
| } | |
| if (stalePRsFiltered.length > 0) { | |
| reportBody.push('## 🔄 Stale Pull Requests'); | |
| stalePRsFiltered.forEach(pr => { | |
| reportBody.push(`- #${pr.number}: ${pr.title}`); | |
| }); | |
| reportBody.push(''); | |
| } | |
| reportBody.push( | |
| '## Actions Taken', | |
| '- Items marked as stale will be closed in 7 days without activity', | |
| '- Repository maintainers should review and take action on important items', | |
| '', | |
| '---', | |
| '*This report was generated automatically by the stale issue management workflow.*' | |
| ); | |
| const reportBodyString = reportBody.join('\n'); | |
| await github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: `📊 Stale Items Report - ${new Date().toISOString().split('T')[0]}`, | |
| body: reportBodyString, | |
| labels: ['maintenance', 'report'] | |
| }); | |
| console.log('📄 Created stale items report issue'); | |
| } |