Maintenance & Housekeeping #35
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: Maintenance & Housekeeping | |
| on: | |
| schedule: | |
| # Run daily at 2 AM UTC | |
| - cron: '0 2 * * *' | |
| workflow_dispatch: | |
| jobs: | |
| stale-issues: | |
| runs-on: ubuntu-latest | |
| name: Close Stale Issues | |
| permissions: | |
| issues: write | |
| contents: read | |
| steps: | |
| - name: Close Stale Issues | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const staleLabel = 'on-hold'; | |
| const staleAfterDays = 30; | |
| const warningAfterDays = 28; | |
| const now = new Date(); | |
| const staleDate = new Date(now.getTime() - staleAfterDays * 24 * 60 * 60 * 1000); | |
| const warningDate = new Date(now.getTime() - warningAfterDays * 24 * 60 * 60 * 1000); | |
| // Get all open issues | |
| const { data: issues } = await github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100 | |
| }); | |
| for (const issue of issues) { | |
| // Skip PRs | |
| if (issue.pull_request) continue; | |
| const lastActivity = new Date(issue.updated_at); | |
| const daysSinceUpdate = (now - lastActivity) / (1000 * 60 * 60 * 24); | |
| // Check if issue is stale (30+ days with no activity) | |
| if (daysSinceUpdate > staleAfterDays && !issue.labels.some(l => l.name === 'blocked')) { | |
| // Check if already warned | |
| const hasStaleComment = (await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| per_page: 5 | |
| })).data.some(c => c.body.includes('This issue is stale')); | |
| if (!hasStaleComment) { | |
| // Add warning | |
| await github.rest.issues.createComment({ | |
| issue_number: issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `⚠️ **Stale Issue Warning**\n\nThis issue has been inactive for ${Math.floor(daysSinceUpdate)} days. It will be closed in 2 days if no activity occurs.\n\n**To keep it open:**\n- Update the issue description if anything has changed\n- Leave a comment with progress or blockers\n- Label it as \`on-hold\` if it's blocked\n\nThank you for your contribution!` | |
| }); | |
| // Add stale label if it exists | |
| try { | |
| await github.rest.issues.addLabels({ | |
| issue_number: issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| labels: ['stale'] | |
| }); | |
| } catch (err) { | |
| console.log(`Could not add stale label: ${err.message}`); | |
| } | |
| } | |
| } | |
| // Close if stale for 30+ days without activity | |
| if (daysSinceUpdate > staleAfterDays + 2) { | |
| await github.rest.issues.update({ | |
| issue_number: issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'closed' | |
| }); | |
| await github.rest.issues.createComment({ | |
| issue_number: issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `🔒 **Closed as Stale**\n\nThis issue has been closed due to inactivity. If you'd like to continue working on it:\n\n1. Comment \`/reopen\` and we'll reopen it\n2. Update the issue description with current status\n3. Label it appropriately\n\nStale issues can be reopened anytime by the original author or maintainers.` | |
| }); | |
| } | |
| } | |
| stale-prs: | |
| runs-on: ubuntu-latest | |
| name: Review Stale PRs | |
| permissions: | |
| pull-requests: write | |
| contents: read | |
| steps: | |
| - name: Notify on Stale PRs | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const staleAfterDays = 14; | |
| const now = new Date(); | |
| const staleDate = new Date(now.getTime() - staleAfterDays * 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 | |
| }); | |
| for (const pr of prs) { | |
| const lastActivity = new Date(pr.updated_at); | |
| const daysSinceUpdate = (now - lastActivity) / (1000 * 60 * 60 * 24); | |
| // Notify if PR is stale (14+ days without activity) | |
| if (daysSinceUpdate > staleAfterDays && daysSinceUpdate < staleAfterDays + 1) { | |
| await github.rest.issues.createComment({ | |
| issue_number: pr.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: `⏰ **PR Update Needed**\n\nThis PR has been waiting for ${Math.floor(daysSinceUpdate)} days. Please:\n\n- Address review comments if any\n- Rebase on latest \`test\` branch\n- Resolve any conflicts\n- Push updates\n\nIf this PR is no longer needed, please close it. Thank you!` | |
| }); | |
| } | |
| } | |
| cleanup-merged-branches: | |
| runs-on: ubuntu-latest | |
| name: Cleanup Merged Branches | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Delete merged branches | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const { data: branches } = await github.rest.repos.listBranches({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| per_page: 100 | |
| }); | |
| const protectedBranches = ['main', 'test', 'develop']; | |
| for (const branch of branches) { | |
| // Skip protected branches | |
| if (protectedBranches.includes(branch.name)) continue; | |
| try { | |
| // Check if branch is merged into main or test | |
| const compareMain = await github.rest.repos.compareCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| base: 'main', | |
| head: branch.name | |
| }); | |
| const compareTest = await github.rest.repos.compareCommits({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| base: 'test', | |
| head: branch.name | |
| }); | |
| // Delete if fully merged (behind by 0 commits) | |
| if ((compareMain.status === 'identical' || compareMain.behind_by === 0) || | |
| (compareTest.status === 'identical' || compareTest.behind_by === 0)) { | |
| await github.rest.git.deleteRef({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| ref: `heads/${branch.name}` | |
| }); | |
| console.log(`✅ Deleted merged branch: ${branch.name}`); | |
| } | |
| } catch (error) { | |
| // Branch might not exist or other error, skip it | |
| console.log(`ℹ️ Could not process branch ${branch.name}: ${error.message}`); | |
| } | |
| } | |
| vulnerability-alert: | |
| runs-on: ubuntu-latest | |
| name: Check Dependency Vulnerabilities | |
| permissions: | |
| contents: read | |
| security-events: read | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '18.x' | |
| cache: 'npm' | |
| - name: Run security audit | |
| continue-on-error: true | |
| run: | | |
| npm ci --legacy-peer-deps 2>&1 || true | |
| npm audit --production 2>&1 || echo "Audit completed" | |
| - name: Notify on vulnerabilities | |
| uses: actions/github-script@v7 | |
| if: failure() | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| github.rest.issues.create({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| title: '🔒 Security: Dependency Vulnerabilities Detected', | |
| body: `⚠️ **Automated Security Alert**\n\nDependency vulnerabilities have been detected in the latest scan.\n\n**Action Required:**\n1. Run \`npm audit\` to see details\n2. Run \`npm audit fix\` for automatic fixes\n3. Review high/critical vulnerabilities\n4. Create PR to fix issues\n\nSee [SECURITY.md](https://github.com/${context.repo.owner}/${context.repo.repo}/blob/main/.github/SECURITY.md) for guidelines.`, | |
| labels: ['security', 'priority-high'] | |
| }); |