mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-01-22 20:46:28 +01:00
209 lines
7.9 KiB
YAML
209 lines
7.9 KiB
YAML
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 what’s 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
|
||
});
|