Create ai-triage.yml

This commit is contained in:
Alexandre
2025-08-11 08:17:12 +02:00
committed by GitHub
parent 396d37924a
commit 7dd577cf34

208
.github/workflows/ai-triage.yml vendored Normal file
View File

@@ -0,0 +1,208 @@
name: AI triage (comment + optional PR)
on:
issues:
types: [opened]
permissions:
contents: write # needed if the PR job runs
pull-requests: write
issues: write
jobs:
# 1) Always: post a first AI reply to new issues
ai_first_reply:
runs-on: ubuntu-latest
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;
// Pull a bit of repo context (README) to help the model
let readmeText = '';
try {
const readme = await github.rest.repos.getReadme({ owner, repo });
readmeText = Buffer.from(readme.data.content, 'base64').toString('utf8');
// Keep the prompt lean
if (readmeText.length > 18000) readmeText = readmeText.slice(0, 18000) + "\n...[truncated]...";
} catch (e) {
// repo may not have a README; ignore
}
// Craft prompt for the OpenAI Responses API
const system = `
You are an open-source triage assistant.
Goal: write a concise, actionable first reply for a new GitHub issue.
Style: factual, polite, link to existing docs or code where relevant in this repo, propose next steps.
If the issue lacks details, ask for the minimum set of specifics (versions, logs, repro steps).
Avoid making up repo facts; only use whats in the issue and README excerpt.
`.trim();
const user = `
Repository: ${owner}/${repo}
Issue #${issue.number}: ${issue.title}
Issue body:
${issue.body || '(no body provided)'}
README excerpt (context):
${readmeText || '(none)'}
`.trim();
// Call OpenAI Responses API (recommended by OpenAI docs)
// https://platform.openai.com/docs/api-reference
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();
const fallback = `⚠️ AI reply failed (${res.status}). Raw error:\n\n${'```'}\n${txt}\n${'```'}`;
await github.rest.issues.createComment({ owner, repo, issue_number: issue.number, body: fallback });
return;
}
const data = await res.json();
// Official SDKs expose "output_text"; on raw HTTP it may be present too per docs.
// Fallback to common shapes if needed.
const text = data.output_text
|| 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
});
# 2) Optional: create a PR when the issue is labeled auto-pr
ai_autofix_pr:
runs-on: ubuntu-latest
needs: ai_first_reply
if: contains(join(github.event.issue.labels.*.name, ','), 'auto-pr')
steps:
- uses: actions/checkout@v4
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;
// Grab README to guide proposed changes a bit
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 {}
// Ask for a STRICT unified diff with no code fences.
const system = `
You are a code-change generator for Git patches.
Output ONLY a unified diff (git apply compatible), no backticks, no prose.
Keep changes minimal and safe. If unsure, return an empty diff.
`.trim();
const user = `
Repository: ${owner}/${repo}
Issue #${issue.number}: ${issue.title}
Issue body:
${issue.body || '(no body)'}
README excerpt:
${readmeText || '(none)'}
Task:
- Propose the smallest viable fix or improvement addressing the issue.
- Only modify files that almost certainly exist.
- If the request is a question, create either a docs tweak (README/CONTRIBUTING) or add comments.
- Output only a valid unified diff starting with "diff --git".
`.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?.choices?.[0]?.message?.content || '').trim();
return patch;
}
- name: Apply patch (if any)
id: apply
shell: bash
run: |
set -euo pipefail
branch="ai/autofix-${{ github.event.issue.number }}"
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 "Writing patch..."
cat > ai.patch <<'PATCH'
${{ steps.genpatch.outputs.result }}
PATCH
echo "Applying patch..."
git config user.name "ai-triage-bot"
git config user.email "ai-triage-bot@users.noreply.github.com"
git checkout -b "$branch"
git apply --whitespace=fix ai.patch
git add -A
git commit -m "AI autofix for #${{ github.event.issue.number }}"
git push -u origin "$branch"
echo "skip_pr=false" >> $GITHUB_OUTPUT
- name: Open PR
if: steps.apply.outputs.skip_pr == 'false'
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const head = `ai/autofix-${{ github.event.issue.number }}`;
await github.rest.pulls.create({
owner, repo,
head,
base: 'main',
title: `AI autofix for #${{ github.event.issue.number }}`,
body: `This PR was generated automatically from issue #${{ github.event.issue.number }}.\n\n> Label \`auto-pr\` triggered patch creation. Please review carefully.`,
maintainer_can_modify: true
});