name: AI Moderator on: issues: types: [opened] issue_comment: types: [created] pull_request_review_comment: types: [created] discussion: types: [created] discussion_comment: types: [created] permissions: issues: write pull-requests: write discussions: write contents: read jobs: moderate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/github-script@v9 env: GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }} with: script: | const fs = require('fs'); const coc = (() => { try { return fs.readFileSync('.github/CODE_OF_CONDUCT.md', 'utf8'); } catch (e) { return ''; } })(); const contributing = (() => { try { return fs.readFileSync('CONTRIBUTING.md', 'utf8'); } catch (e) { return ''; } })(); const content = context.payload.issue?.body || context.payload.discussion?.body || context.payload.comment?.body || ''; if (!content.trim()) { console.log('No content to analyze'); return; } const isComment = context.payload.comment !== undefined; const isDiscussion = context.payload.discussion !== undefined; const prompt = [ 'You are an AI moderator for the Antergos NeXT project.', 'Speak in first person. Be direct, confident, and have some snark.', 'The project has a playful but no-nonsense tone - match it.', '', 'If the content says it is a test or contains phrases like moderator', 'or test or reply here, play along with the joke but still enforce the rules.', '', 'Code of Conduct:', coc, '', 'Contributing Guidelines:', contributing, '', 'Analyze the content below for violations.', 'Respond with ONLY valid JSON - no markdown, no backticks.', '', '{', ' "violation": true/false,', ' "message": "your direct first-person response",', ' "action": "warn",', ' "severity": 1-10', '}', '', '"action" options: "warn" (post warning), "hide" (minimize comment), "lock" (lock issue/discussion)', 'For issues use "warn" or "lock". For comments you may use "hide".', '', 'Content:', content ].join('\n'); const model = 'llama-3.1-8b-instant'; let decision; try { const response = await fetch('https://api.groq.com/openai/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.GROQ_API_KEY}` }, body: JSON.stringify({ model, messages: [ { role: 'system', content: 'You are a helpful community moderator. Respond only with valid JSON as instructed.' }, { role: 'user', content: prompt } ], temperature: 0.1, max_tokens: 250 }) }); const data = await response.json(); const raw = data.choices?.[0]?.message?.content || '{}'; decision = JSON.parse(raw.replace(/```json|```/g, '').trim()); console.log(`Decision: ${JSON.stringify(decision)}`); } catch (err) { console.log(`API error: ${err}`); return; } if (!decision.violation) { console.log('Content deemed acceptable'); return; } const number = context.payload.issue?.number || context.payload.pull_request?.number || context.payload.discussion?.number; if (!number) { console.log('No issue/PR/discussion number found'); return; } const owner = context.repo.owner; const repo = context.repo.repo; try { if (isDiscussion) { await github.request('POST /repos/{owner}/{repo}/discussions/{discussion_number}/comments', { owner, repo, discussion_number: number, body: decision.message }); } else { await github.rest.issues.addLabels({ owner, repo, issue_number: number, labels: ['ai-moderated'] }); await github.rest.issues.createComment({ owner, repo, issue_number: number, body: decision.message }); } if (decision.action === 'hide' && isComment) { const commentId = context.payload.comment.node_id; if (commentId) { await github.graphql(` mutation { minimizeComment(input: {subjectId: "${commentId}", classifier: OFF_TOPIC}) { minimizedComment { isMinimized } } } `); console.log('Comment minimized'); } } if (decision.action === 'lock') { if (isDiscussion) { await github.request('PUT /repos/{owner}/{repo}/discussions/{discussion_number}/lock', { owner, repo, discussion_number: number }); } else { await github.rest.issues.lock({ owner, repo, issue_number: number, lock_reason: 'too_heated' }); } console.log('Locked'); } console.log('Action completed: ' + decision.action); } catch (err) { console.log(`GitHub API error: ${err}`); }