Add health checks - 20260214090007 #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: NullSec Response Bot | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, closed, synchronize] | |
| pull_request_review: | |
| types: [submitted] | |
| issue_comment: | |
| types: [created] | |
| issues: | |
| types: [opened, labeled] | |
| schedule: | |
| - cron: '0 9 * * 1' | |
| permissions: | |
| pull-requests: write | |
| issues: write | |
| contents: read | |
| env: | |
| STALE_DAYS: 14 | |
| jobs: | |
| welcome-issue: | |
| if: github.event_name == 'issues' && github.event.action == 'opened' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Welcome and Classify Issue | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const issue = context.payload.issue; | |
| const author = issue.user.login; | |
| const repoOwner = context.repo.owner; | |
| if (author === repoOwner) return; | |
| const title = issue.title.toLowerCase(); | |
| const body = (issue.body || '').toLowerCase(); | |
| let response = ''; | |
| let labels = []; | |
| let isPriority = false; | |
| // COLLABORATION REQUEST | |
| if (body.includes('collaborate') || body.includes('team up') || | |
| body.includes('work together') || body.includes('partnership') || | |
| body.includes('join the team') || body.includes('contribute to nullsec') || | |
| title.includes('collaboration') || title.includes('team up')) { | |
| response = [ | |
| '## 👋 Hey @' + author + '! Thanks for reaching out about collaborating!', | |
| '', | |
| "I'm **always** excited to connect with fellow security researchers!", | |
| '', | |
| '### 🤝 Ways to Get Involved:', | |
| '', | |
| '| Type | Description |', | |
| '|------|-------------|', | |
| '| 🔧 **Code** | PRs welcome! |', | |
| '| 📖 **Docs** | Help with documentation |', | |
| '| 🧪 **Testing** | Test new features, report bugs |', | |
| '| 💡 **Ideas** | Share feature ideas |', | |
| '| 🔬 **Research** | Security research, CVE hunting |', | |
| '', | |
| '### 📋 Tell me more about:', | |
| '1. Your background/expertise', | |
| '2. Which NullSec tools interest you', | |
| '3. How much time you can dedicate', | |
| '', | |
| "I'll respond personally very soon! 🚀", | |
| '', | |
| '---', | |
| '*🤖 NullSec Bot - Flagged as priority*' | |
| ].join('\n'); | |
| labels = ['collaboration', 'priority']; | |
| isPriority = true; | |
| // BUG REPORT | |
| } else if (title.includes('bug') || title.includes('error') || | |
| title.includes('crash') || title.includes('not working')) { | |
| response = [ | |
| '## 👋 Thanks for the bug report, @' + author + '!', | |
| '', | |
| "I'll investigate this ASAP. To help me fix it faster:", | |
| '', | |
| '### 📋 Please Provide:', | |
| '- **Environment:** OS, kernel version', | |
| '- **Tool Version:** version output', | |
| '- **Steps to Reproduce:** Exact commands', | |
| '- **Expected vs Actual:** What happened', | |
| '- **Logs:** Error messages if any', | |
| '', | |
| "I'll get back to you soon! 🔧", | |
| '', | |
| '---', | |
| '*🤖 NullSec Bot*' | |
| ].join('\n'); | |
| labels = ['bug', 'needs-triage']; | |
| // FEATURE REQUEST | |
| } else if (title.includes('feature') || title.includes('request') || | |
| title.includes('suggestion') || title.includes('add support')) { | |
| response = [ | |
| '## 👋 Thanks for the suggestion, @' + author + '!', | |
| '', | |
| 'I love hearing ideas from users! ✨', | |
| '', | |
| '### 📋 To Help Evaluate:', | |
| '1. **Use Case:** What problem does this solve?', | |
| '2. **Current Workaround:** How do you handle this now?', | |
| '3. **Priority:** Is this blocking your workflow?', | |
| '', | |
| 'Thanks for helping improve NullSec! 💪', | |
| '', | |
| '---', | |
| '*🤖 NullSec Bot*' | |
| ].join('\n'); | |
| labels = ['enhancement']; | |
| // QUESTION | |
| } else if (title.includes('?') || title.includes('how to') || | |
| title.includes('help') || title.includes('question')) { | |
| response = [ | |
| '## 👋 Hey @' + author + '! Happy to help!', | |
| '', | |
| "I'll answer your question soon. In the meantime:", | |
| '', | |
| '### 📚 Resources:', | |
| '- 📖 Check the **README** for documentation', | |
| '- 🔍 Search **existing issues** for similar questions', | |
| '', | |
| "I'll get back to you shortly!", | |
| '', | |
| '---', | |
| '*🤖 NullSec Bot*' | |
| ].join('\n'); | |
| labels = ['question']; | |
| // SECURITY | |
| } else if (title.includes('security') || title.includes('vulnerability') || | |
| title.includes('cve') || title.includes('exploit')) { | |
| response = [ | |
| '## 🔒 Security Report Received', | |
| '', | |
| 'Thanks @' + author + ' for the security report!', | |
| '', | |
| '⚠️ **Important:** If this is a sensitive vulnerability:', | |
| '1. Do NOT post full exploit details publicly', | |
| '2. Consider using GitHub private vulnerability reporting', | |
| '', | |
| "I'll review this with high priority!", | |
| '', | |
| '---', | |
| '*🤖 NullSec Bot - Security issues are top priority*' | |
| ].join('\n'); | |
| labels = ['security', 'priority']; | |
| isPriority = true; | |
| // DEFAULT | |
| } else { | |
| response = [ | |
| '## 👋 Thanks for opening this issue, @' + author + '!', | |
| '', | |
| "I'll review it and respond soon.", | |
| '', | |
| '---', | |
| '*🤖 NullSec Bot*' | |
| ].join('\n'); | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| body: response | |
| }); | |
| if (labels.length > 0) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: issue.number, | |
| labels: labels | |
| }); | |
| } catch (e) { | |
| console.log('Could not add labels:', e.message); | |
| } | |
| } | |
| auto-label: | |
| if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'synchronize') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Auto Label PR | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const prNumber = pr.number; | |
| const labels = []; | |
| const additions = pr.additions || 0; | |
| const deletions = pr.deletions || 0; | |
| const totalChanges = additions + deletions; | |
| if (totalChanges <= 10) labels.push('size/XS'); | |
| else if (totalChanges <= 50) labels.push('size/S'); | |
| else if (totalChanges <= 200) labels.push('size/M'); | |
| else if (totalChanges <= 500) labels.push('size/L'); | |
| else labels.push('size/XL'); | |
| const { data: files } = await github.rest.pulls.listFiles({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber | |
| }); | |
| const filenames = files.map(f => f.filename.toLowerCase()); | |
| if (filenames.some(f => f.includes('readme') || f.endsWith('.md'))) labels.push('documentation'); | |
| if (filenames.some(f => f.includes('.github/workflows'))) labels.push('ci/cd'); | |
| if (filenames.some(f => f.endsWith('.rs'))) labels.push('rust'); | |
| if (filenames.some(f => f.endsWith('.go'))) labels.push('go'); | |
| if (filenames.some(f => f.endsWith('.py'))) labels.push('python'); | |
| const title = pr.title.toLowerCase(); | |
| if (title.startsWith('feat')) labels.push('feature'); | |
| if (title.startsWith('fix')) labels.push('bugfix'); | |
| if (title.startsWith('docs')) labels.push('documentation'); | |
| if (labels.length > 0) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| labels: labels | |
| }); | |
| } catch (e) { | |
| console.log('Could not add labels:', e.message); | |
| } | |
| } | |
| first-time-contributor: | |
| if: github.event_name == 'pull_request_target' && github.event.action == 'opened' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check First Time Contributor | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prAuthor = context.payload.pull_request.user.login; | |
| const repoOwner = context.repo.owner; | |
| if (prAuthor === repoOwner) return; | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'all', | |
| per_page: 100 | |
| }); | |
| const authorPRs = prs.filter(p => p.user.login === prAuthor); | |
| if (authorPRs.length === 1) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.pull_request.number, | |
| labels: ['first-contribution'] | |
| }); | |
| } catch (e) {} | |
| const msg = '🎉 **Welcome to the project, @' + prAuthor + '!** This is your first PR here!\n\nI really appreciate you taking the time to contribute. I will review this carefully and get back to you soon.\n\n---\n*🤖 NullSec Bot*'; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.payload.pull_request.number, | |
| body: msg | |
| }); | |
| } | |
| welcome-new-pr: | |
| if: github.event_name == 'pull_request_target' && (github.event.action == 'opened' || github.event.action == 'reopened') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Welcome PR Author | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prAuthor = context.payload.pull_request.user.login; | |
| const prNumber = context.payload.pull_request.number; | |
| const repoOwner = context.repo.owner; | |
| if (prAuthor === repoOwner) return; | |
| const welcomeMessage = [ | |
| '👋 Hey @' + prAuthor + '! Thanks for opening this PR!', | |
| '', | |
| "I'll review this as soon as I can. Here's what happens next:", | |
| '', | |
| '- 🔍 I will review your changes', | |
| '- 💬 I may leave some comments or suggestions', | |
| '- ✅ Once everything looks good, I will merge it', | |
| '', | |
| '---', | |
| '*🤖 NullSec Bot*' | |
| ].join('\n'); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: welcomeMessage | |
| }); | |
| respond-to-review: | |
| if: github.event_name == 'pull_request_review' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Respond to Review | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const review = context.payload.review; | |
| const prNumber = context.payload.pull_request.number; | |
| const reviewer = review.user.login; | |
| const repoOwner = context.repo.owner; | |
| const prAuthor = context.payload.pull_request.user.login; | |
| if (reviewer !== repoOwner || prAuthor === repoOwner) return; | |
| let responseMessage = ''; | |
| if (review.state === 'approved') { | |
| responseMessage = '✅ **Approved!**\n\nThanks for your contribution @' + prAuthor + '! I will merge this shortly.\n\n---\n*🤖 NullSec Bot*'; | |
| } else if (review.state === 'changes_requested') { | |
| responseMessage = '📝 **Changes Requested**\n\nHey @' + prAuthor + ', I have left some feedback above. Please take a look and update your PR when you get a chance.\n\n---\n*🤖 NullSec Bot*'; | |
| } | |
| if (responseMessage) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: responseMessage | |
| }); | |
| } | |
| handle-commands: | |
| if: github.event_name == 'issue_comment' && github.event.issue.pull_request | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Handle Bot Commands | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const comment = context.payload.comment.body.toLowerCase().trim(); | |
| const commenter = context.payload.comment.user.login; | |
| const prNumber = context.payload.issue.number; | |
| const repoOwner = context.repo.owner; | |
| const commands = { | |
| '/status': '📊 **PR Status**\n\n- **State:** ' + context.payload.issue.state + '\n- **Labels:** ' + (context.payload.issue.labels.map(l => l.name).join(', ') || 'None') + '\n\n---\n*🤖 NullSec Bot*', | |
| '/help': '🤖 **NullSec Bot Commands**\n\n| Command | Description | Access |\n|---------|-------------|--------|\n| `/status` | Check PR status | Anyone |\n| `/help` | Show this help | Anyone |\n| `/ping` | Check if bot active | Anyone |\n| `/lgtm` | Approve PR | Owner |\n| `/merge` | Merge PR | Owner |\n| `/close` | Close PR | Owner |\n\n---\n*🤖 NullSec Bot*', | |
| '/ping': '🏓 **Pong!** Bot is active.\n\n---\n*🤖 NullSec Bot*' | |
| }; | |
| for (const [cmd, response] of Object.entries(commands)) { | |
| if (comment.startsWith(cmd)) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: response | |
| }); | |
| return; | |
| } | |
| } | |
| if (commenter === repoOwner) { | |
| if (comment.includes('/lgtm') || comment.includes('/approve')) { | |
| await github.rest.pulls.createReview({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| event: 'APPROVE', | |
| body: '✅ LGTM! Approved via bot command.' | |
| }); | |
| } | |
| if (comment.includes('/merge')) { | |
| try { | |
| await github.rest.pulls.merge({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| merge_method: 'squash' | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: '🎉 **Merged!** Thanks for your contribution!\n\n---\n*🤖 NullSec Bot*' | |
| }); | |
| } catch (e) { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: '❌ **Merge failed:** ' + e.message + '\n\n---\n*🤖 NullSec Bot*' | |
| }); | |
| } | |
| } | |
| if (comment.includes('/close')) { | |
| await github.rest.pulls.update({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| pull_number: prNumber, | |
| state: 'closed' | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: '🚪 **PR Closed** by maintainer.\n\n---\n*🤖 NullSec Bot*' | |
| }); | |
| } | |
| } | |
| thank-on-merge: | |
| if: github.event_name == 'pull_request_target' && github.event.action == 'closed' && github.event.pull_request.merged == true | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Thank Contributor on Merge | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const author = pr.user.login; | |
| const repoOwner = context.repo.owner; | |
| if (author === repoOwner) return; | |
| const messages = [ | |
| '🎉 **Merged!** Thanks @' + author + ' for this contribution!', | |
| '✨ **Awesome!** Your changes are now part of the project, @' + author + '!', | |
| '🚀 **Shipped!** Thanks for the great work, @' + author + '!' | |
| ]; | |
| const message = messages[Math.floor(Math.random() * messages.length)]; | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: message + '\n\nYour contribution helps make this project better for everyone. 🙏\n\n---\n*🤖 NullSec Bot*' | |
| }); | |
| stale-check: | |
| if: github.event_name == 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Check for Stale PRs | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const { data: prs } = await github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| sort: 'updated', | |
| direction: 'asc' | |
| }); | |
| const now = new Date(); | |
| const staleDays = 14; | |
| for (const pr of prs) { | |
| const updatedAt = new Date(pr.updated_at); | |
| const daysSinceUpdate = Math.floor((now - updatedAt) / (1000 * 60 * 60 * 24)); | |
| if (daysSinceUpdate >= staleDays) { | |
| const hasStaleLabel = pr.labels.some(l => l.name === 'stale'); | |
| if (!hasStaleLabel) { | |
| try { | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| labels: ['stale'] | |
| }); | |
| } catch (e) {} | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: pr.number, | |
| body: '👋 Hey @' + pr.user.login + ', this PR has been inactive for ' + daysSinceUpdate + ' days.\n\nIs this still something you are working on? If so, please update or leave a comment.\n\n---\n*🤖 NullSec Bot*' | |
| }); | |
| } | |
| } | |
| } |