name: AI triage (comment + optional PR) on: issues: types: [opened] issue_comment: types: [created] permissions: contents: write pull-requests: write issues: write jobs: ai_first_reply: runs-on: ubuntu-latest # Run when: issue opened OR a '/codex' comment on an issue (not PR) if: > (github.event_name == 'issues' || (github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.comment.body == '/codex' && github.event.issue.pull_request == null)) && contains(github.event.issue.labels.*.name, 'bug') steps: - name: Compose and post AI reply uses: actions/github-script@v7 env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} with: script: | const issue = context.payload.issue; const { owner, repo } = context.repo; // Optional repo context let readmeText = ''; try { const readme = await github.rest.repos.getReadme({ owner, repo }); readmeText = Buffer.from(readme.data.content, 'base64').toString('utf8'); if (readmeText.length > 18000) readmeText = readmeText.slice(0, 18000) + "\n...[truncated]..."; } catch {} const system = ` You are an open-source triage assistant. Write a concise, actionable first reply for a new bug report. Focus on diagnosing the problem and suggesting potential fixes. Minimize follow-up questions and avoid inventing repo facts. Propose next steps and link to repo files only when certain. `.trim(); const user = ` Repository: ${owner}/${repo} Issue #${issue.number}: ${issue.title} Issue body: ${issue.body || '(no body provided)'} README excerpt: ${readmeText || '(none)'} `.trim(); const res = await fetch('https://api.openai.com/v1/responses', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: 'gpt-4o-mini', input: [ { role: 'system', content: system }, { role: 'user', content: user } ], }), }); if (!res.ok) { const txt = await res.text(); await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: `⚠️ AI reply failed (${res.status}).\n\n\`\`\`\n${txt}\n\`\`\`` }); return; } const data = await res.json(); const text = data.output_text || data.output?.map(p => p.content?.map(c => c.text ?? '').join('')).join('\n') || data?.choices?.[0]?.message?.content || 'Thanks for opening this issue — we will take a look!'; await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: text }); ai_autofix_pr: runs-on: ubuntu-latest needs: ai_first_reply # Same trigger conditions as above AND issue has label 'auto-pr' if: > (github.event_name == 'issues' || (github.event_name == 'issue_comment' && github.event.action == 'created' && github.event.comment.body == '/codex' && github.event.issue.pull_request == null)) && contains(github.event.issue.labels.*.name, 'auto-pr') && contains(github.event.issue.labels.*.name, 'bug') steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Generate patch with OpenAI (unified diff) id: genpatch uses: actions/github-script@v7 env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} with: result-encoding: string script: | const { owner, repo } = context.repo; const issue = context.payload.issue; let readmeText = ''; try { const readme = await github.rest.repos.getReadme({ owner, repo }); readmeText = Buffer.from(readme.data.content, 'base64').toString('utf8'); if (readmeText.length > 12000) readmeText = readmeText.slice(0, 12000) + "\n...[truncated]..."; } catch {} const system = ` You are a code-change generator. Output ONLY a git unified diff (git apply compatible). No prose, no backticks. Keep changes minimal and safe; if unsure, output an empty diff. `.trim(); const user = ` Repo: ${owner}/${repo} Issue #${issue.number}: ${issue.title} Issue body: ${issue.body || '(no body)'} README excerpt: ${readmeText || '(none)'} `.trim(); const res = await fetch('https://api.openai.com/v1/responses', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`, }, body: JSON.stringify({ model: 'gpt-4o-mini', input: [ { role: 'system', content: system }, { role: 'user', content: user } ], }), }); if (!res.ok) { const txt = await res.text(); core.setFailed(`OpenAI error ${res.status}: ${txt}`); } else { const data = await res.json(); const patch = ( data.output_text || data.output?.map(p => p.content?.map(c => c.text ?? '').join('')).join('\n') || data?.choices?.[0]?.message?.content || '' ).trim(); return patch; } - name: Apply patch (if any) id: apply shell: bash run: | set -euo pipefail patch="${{ steps.genpatch.outputs.result }}" if [ -z "$patch" ]; then echo "No patch produced; skipping PR." echo "skip_pr=true" >> "$GITHUB_OUTPUT" exit 0 fi echo "$patch" > ai.patch git apply --whitespace=fix ai.patch echo "skip_pr=false" >> "$GITHUB_OUTPUT" - name: Create Pull Request if: steps.apply.outputs.skip_pr == 'false' uses: peter-evans/create-pull-request@v7 with: branch: ai/autofix-${{ github.event.issue.number }} title: AI autofix for #${{ github.event.issue.number }} commit-message: AI autofix for #${{ github.event.issue.number }} body: | This PR was generated automatically from issue #${{ github.event.issue.number }}. Label `auto-pr` triggered patch creation. Please review carefully. labels: auto-pr, ai-generated delete-branch: true