name: AI Bot on: issues: types: [opened] issue_comment: types: [created] pull_request_review_comment: types: [created] pull_request: types: [opened, edited, synchronize] jobs: respond-to-commands: runs-on: ubuntu-latest if: | (github.actor == 'f') && ((github.event_name == 'issues' && contains(github.event.issue.body, '/ai')) || (github.event_name == 'issue_comment' && contains(github.event.comment.body, '/ai')) || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/ai')) || (github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/ai'))) permissions: contents: write pull-requests: write issues: write steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v3 with: node-version: '18' - name: Install dependencies run: npm install openai@^4.0.0 @octokit/rest@^19.0.0 - name: Process command id: process env: GH_SECRET: ${{ secrets.GH_SECRET }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} run: | node << 'EOF' const OpenAI = require('openai'); const { Octokit } = require('@octokit/rest'); async function main() { const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); const octokit = new Octokit({ auth: process.env.GH_SECRET }); const eventName = process.env.GITHUB_EVENT_NAME; const eventPath = process.env.GITHUB_EVENT_PATH; const event = require(eventPath); // Double check user authorization const actor = event.sender?.login || event.pull_request?.user?.login || event.issue?.user?.login; if (actor !== 'f') { console.log('Unauthorized user attempted to use the bot:', actor); return; } // Get command and context let command = ''; let issueNumber = null; let isPullRequest = false; if (eventName === 'issues') { command = event.issue.body; issueNumber = event.issue.number; } else if (eventName === 'issue_comment') { command = event.comment.body; issueNumber = event.issue.number; isPullRequest = !!event.issue.pull_request; } else if (eventName === 'pull_request_review_comment') { command = event.comment.body; issueNumber = event.pull_request.number; isPullRequest = true; } else if (eventName === 'pull_request') { command = event.pull_request.body; issueNumber = event.pull_request.number; isPullRequest = true; } if (!command.startsWith('/ai')) { return; } // Extract the actual command after /ai const aiCommand = command.substring(3).trim(); // Handle resolve conflicts command if (aiCommand === 'resolve' || aiCommand === 'fix conflicts') { if (!isPullRequest) { await octokit.issues.createComment({ owner: event.repository.owner.login, repo: event.repository.name, issue_number: issueNumber, body: '❌ The resolve command can only be used on pull requests.' }); return; } try { // Get PR details const { data: pr } = await octokit.pulls.get({ owner: event.repository.owner.login, repo: event.repository.name, pull_number: issueNumber }); // Get the PR diff to extract the new prompt const { data: files } = await octokit.pulls.listFiles({ owner: event.repository.owner.login, repo: event.repository.name, pull_number: issueNumber }); // Extract prompt from changes let newPrompt = ''; let actName = ''; let contributorInfo = ''; for (const file of files) { if (file.filename === 'README.md') { const patch = file.patch || ''; const addedLines = patch.split('\n') .filter(line => line.startsWith('+')) .map(line => line.substring(1)) .join('\n'); const promptMatch = addedLines.match(/## Act as (?:a |an )?([^\n]+)\n(?:Contributed by:[^\n]*\n)?(?:> )?([^#]+?)(?=\n\n|$)/); if (promptMatch) { actName = `Act as ${promptMatch[1].trim()}`; newPrompt = promptMatch[2].trim(); const contributorLine = addedLines.match(/Contributed by: \[@([^\]]+)\]\(https:\/\/github\.com\/([^\)]+)\)/); if (contributorLine) { contributorInfo = `Contributed by: [@${contributorLine[1]}](https://github.com/${contributorLine[2]})`; } } } } if (!actName || !newPrompt) { await octokit.issues.createComment({ owner: event.repository.owner.login, repo: event.repository.name, issue_number: issueNumber, body: '❌ Could not extract prompt information from changes' }); return; } // Get README from main branch const { data: readmeFile } = await octokit.repos.getContent({ owner: event.repository.owner.login, repo: event.repository.name, path: 'README.md', ref: 'main' }); // Get CSV from main branch const { data: csvFile } = await octokit.repos.getContent({ owner: event.repository.owner.login, repo: event.repository.name, path: 'prompts.csv', ref: 'main' }); // Prepare new README content let readmeContent = Buffer.from(readmeFile.content, 'base64').toString('utf-8'); const newSection = `\n## ${actName}\n${contributorInfo ? contributorInfo + '\n' : ''}\n> ${newPrompt}\n`; readmeContent += newSection; // Prepare new CSV content let csvContent = Buffer.from(csvFile.content, 'base64').toString('utf-8'); csvContent += `\n"${actName.replace(/"/g, '""')}","${newPrompt.replace(/"/g, '""')}"`; // Update README in PR branch await octokit.repos.createOrUpdateFileContents({ owner: event.repository.owner.login, repo: event.repository.name, path: 'README.md', message: `feat: Add "${actName}" to README`, content: Buffer.from(readmeContent).toString('base64'), branch: pr.head.ref, sha: readmeFile.sha }); // Update CSV in PR branch await octokit.repos.createOrUpdateFileContents({ owner: event.repository.owner.login, repo: event.repository.name, path: 'prompts.csv', message: `feat: Add "${actName}" to prompts.csv`, content: Buffer.from(csvContent).toString('base64'), branch: pr.head.ref, sha: csvFile.sha }); await octokit.issues.createComment({ owner: event.repository.owner.login, repo: event.repository.name, issue_number: issueNumber, body: `✨ Updated both files:\n1. Added "${actName}" to README.md\n2. Added the prompt to prompts.csv` }); } catch (error) { console.error('Error:', error); await octokit.issues.createComment({ owner: event.repository.owner.login, repo: event.repository.name, issue_number: issueNumber, body: `❌ Error while trying to update files: ${error.message}` }); } return; } // Handle rename command specifically if (aiCommand.startsWith('rename') || aiCommand === 'suggest title') { if (!isPullRequest) { await octokit.issues.createComment({ owner: event.repository.owner.login, repo: event.repository.name, issue_number: issueNumber, body: '❌ The rename command can only be used on pull requests.' }); return; } // Get PR details for context const { data: pr } = await octokit.pulls.get({ owner: event.repository.owner.login, repo: event.repository.name, pull_number: issueNumber }); // Get the list of files changed in the PR const { data: files } = await octokit.pulls.listFiles({ owner: event.repository.owner.login, repo: event.repository.name, pull_number: issueNumber }); // Process file changes const fileChanges = await Promise.all(files.map(async file => { if (file.status === 'removed') { return `Deleted: ${file.filename}`; } // Get file content for added or modified files if (file.status === 'added' || file.status === 'modified') { const patch = file.patch || ''; return `${file.status === 'added' ? 'Added' : 'Modified'}: ${file.filename}\nChanges:\n${patch}`; } return `${file.status}: ${file.filename}`; })); const completion = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: "You are a helpful assistant that generates clear and concise pull request titles. Follow these rules:\n1. Use conventional commit style (feat:, fix:, docs:, etc.)\n2. Focus on WHAT changed, not HOW or WHERE\n3. Keep it short and meaningful\n4. Don't mention file names or technical implementation details\n5. Return ONLY the new title, nothing else\n\nGood examples:\n- feat: Add \"Act as a Career Coach\"\n- fix: Correct typo in Linux Terminal prompt\n- docs: Update installation instructions\n- refactor: Improve error handling" }, { role: "user", content: `Based on these file changes, generate a concise PR title:\n\n${fileChanges.join('\n\n')}` } ], temperature: 0.5, max_tokens: 60 }); const newTitle = completion.choices[0].message.content.trim(); // Update PR title await octokit.pulls.update({ owner: event.repository.owner.login, repo: event.repository.name, pull_number: issueNumber, title: newTitle }); // Add comment about the rename await octokit.issues.createComment({ owner: event.repository.owner.login, repo: event.repository.name, issue_number: issueNumber, body: `✨ Updated PR title to: "${newTitle}"\n\nBased on the following changes:\n\`\`\`diff\n${fileChanges.join('\n')}\n\`\`\`` }); return; } // Handle other commands const completion = await openai.chat.completions.create({ model: "gpt-3.5-turbo", messages: [ { role: "system", content: "You are a helpful AI assistant that helps with GitHub repositories. You can suggest code changes, fix issues, and improve code quality." }, { role: "user", content: aiCommand } ], temperature: 0.7, max_tokens: 2000 }); const response = completion.choices[0].message.content; // If response contains code changes, create a new branch and PR if (response.includes('```')) { const branchName = `ai-bot/fix-${issueNumber}`; // Create new branch const defaultBranch = event.repository.default_branch; const ref = await octokit.git.getRef({ owner: event.repository.owner.login, repo: event.repository.name, ref: `heads/${defaultBranch}` }); await octokit.git.createRef({ owner: event.repository.owner.login, repo: event.repository.name, ref: `refs/heads/${branchName}`, sha: ref.data.object.sha }); // Extract code changes and file paths from response const codeBlocks = response.match(/```[\s\S]*?```/g); for (const block of codeBlocks) { const [_, filePath, ...codeLines] = block.split('\n'); const content = Buffer.from(codeLines.join('\n')).toString('base64'); await octokit.repos.createOrUpdateFileContents({ owner: event.repository.owner.login, repo: event.repository.name, path: filePath, message: `AI Bot: Apply suggested changes for #${issueNumber}`, content, branch: branchName }); } // Create PR await octokit.pulls.create({ owner: event.repository.owner.login, repo: event.repository.name, title: `AI Bot: Fix for #${issueNumber}`, body: `This PR was automatically generated in response to #${issueNumber}\n\nChanges proposed:\n${response}`, head: branchName, base: defaultBranch }); } // Add comment with response await octokit.issues.createComment({ owner: event.repository.owner.login, repo: event.repository.name, issue_number: issueNumber, body: `AI Bot Response:\n\n${response}` }); } main().catch(error => { console.error('Error:', error); process.exit(1); }); EOF - name: Handle errors if: failure() uses: actions/github-script@v6 with: script: | const issueNumber = context.issue.number || context.payload.pull_request.number; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: '❌ Sorry, there was an error processing your command. Please try again or contact the repository maintainers.' });