mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-15 22:29:08 +02:00
feat: automate issue triage and draft fixes
This commit is contained in:
654
.github/workflows/on_issues_ai.yml
vendored
Normal file
654
.github/workflows/on_issues_ai.yml
vendored
Normal file
@@ -0,0 +1,654 @@
|
|||||||
|
# yamllint disable rule:line-length
|
||||||
|
---
|
||||||
|
name: AI issue triage and draft fix
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, reopened, labeled]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ai-issue-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
TRIAGE_MODEL: ${{ vars.OPENAI_TRIAGE_MODEL || 'gpt-5-mini' }}
|
||||||
|
MIN_CONFIDENCE: ${{ vars.AI_MIN_CONFIDENCE || '0.80' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
detect_submitter:
|
||||||
|
if: >-
|
||||||
|
github.event.action != 'labeled' ||
|
||||||
|
github.event.label.name == 'ai: fix-approved'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
outputs:
|
||||||
|
matched: ${{ steps.submitter.outputs.matched }}
|
||||||
|
addon: ${{ steps.submitter.outputs.addon }}
|
||||||
|
submitter: ${{ steps.submitter.outputs.submitter }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Detect mapped add-on submitter
|
||||||
|
id: submitter
|
||||||
|
env:
|
||||||
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||||
|
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||||
|
run: bash .github/scripts/find_addon_submitter.sh
|
||||||
|
|
||||||
|
triage:
|
||||||
|
if: >-
|
||||||
|
needs.detect_submitter.outputs.matched != 'true' &&
|
||||||
|
(
|
||||||
|
github.event.action == 'opened' ||
|
||||||
|
github.event.action == 'reopened' ||
|
||||||
|
(
|
||||||
|
github.event.action == 'labeled' &&
|
||||||
|
github.event.label.name == 'ai: fix-approved'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
needs: detect_submitter
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
outputs:
|
||||||
|
addon: ${{ steps.result.outputs.addon }}
|
||||||
|
category: ${{ steps.result.outputs.category }}
|
||||||
|
confidence: ${{ steps.result.outputs.confidence }}
|
||||||
|
existing_addon: ${{ steps.result.outputs.existing_addon }}
|
||||||
|
risk: ${{ steps.result.outputs.risk }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Ensure AI labels exist
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gh label create "ai: triaged" --repo "$REPO" --color "1d76db" --description "Issue classified by AI" --force
|
||||||
|
gh label create "ai: answered" --repo "$REPO" --color "0e8a16" --description "Question answered automatically" --force
|
||||||
|
gh label create "ai: needs-info" --repo "$REPO" --color "fbca04" --description "AI requested essential information" --force
|
||||||
|
gh label create "ai: fix-proposed" --repo "$REPO" --color "c5def5" --description "AI recommends repository analysis" --force
|
||||||
|
gh label create "ai: fix-approved" --repo "$REPO" --color "5319e7" --description "Maintainer approved an automated fix attempt" --force
|
||||||
|
gh label create "ai: fixing" --repo "$REPO" --color "0052cc" --description "Automated fix attempt is running" --force
|
||||||
|
gh label create "ai: pr-created" --repo "$REPO" --color "0e8a16" --description "Automated draft pull request created" --force
|
||||||
|
gh label create "ai: maintainer-review" --repo "$REPO" --color "d93f0b" --description "Maintainer review is required" --force
|
||||||
|
gh label create "ai: new-addon-request" --repo "$REPO" --color "ededed" --description "New add-on request; never implemented automatically" --force
|
||||||
|
gh label create "ai-generated" --repo "$REPO" --color "bfdadc" --description "Changes generated with AI assistance" --force
|
||||||
|
|
||||||
|
- name: Triage issue with structured output
|
||||||
|
id: result
|
||||||
|
env:
|
||||||
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
||||||
|
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -n "$OPENAI_API_KEY"
|
||||||
|
|
||||||
|
issue_body="${ISSUE_BODY:0:20000}"
|
||||||
|
addon_catalog="$(
|
||||||
|
find . -mindepth 2 -maxdepth 2 -type f \
|
||||||
|
\( -name config.yaml -o -name config.json \) -printf '%h\n' |
|
||||||
|
sed 's#^./##' | sort -u |
|
||||||
|
jq -Rsc 'split("\n") | map(select(length > 0))'
|
||||||
|
)"
|
||||||
|
|
||||||
|
jq -n \
|
||||||
|
--arg model "$TRIAGE_MODEL" \
|
||||||
|
--arg instructions "$(cat .github/ai/triage-prompt.md)" \
|
||||||
|
--arg author "$ISSUE_AUTHOR" \
|
||||||
|
--arg body "$issue_body" \
|
||||||
|
--arg number "$ISSUE_NUMBER" \
|
||||||
|
--arg title "$ISSUE_TITLE" \
|
||||||
|
--argjson addon_catalog "$addon_catalog" \
|
||||||
|
--slurpfile schema .github/ai/triage-schema.json \
|
||||||
|
'{
|
||||||
|
model: $model,
|
||||||
|
store: false,
|
||||||
|
max_output_tokens: 1200,
|
||||||
|
reasoning: {effort: "low"},
|
||||||
|
instructions: $instructions,
|
||||||
|
input: (
|
||||||
|
"Repository issue #" + $number + "\n" +
|
||||||
|
"Author: " + $author + "\n" +
|
||||||
|
"Title: " + $title + "\n\n" +
|
||||||
|
"Existing add-on directories:\n" +
|
||||||
|
($addon_catalog | tojson) + "\n\n" +
|
||||||
|
"Body:\n" + $body
|
||||||
|
),
|
||||||
|
text: {
|
||||||
|
format: {
|
||||||
|
type: "json_schema",
|
||||||
|
name: "issue_triage",
|
||||||
|
strict: true,
|
||||||
|
schema: $schema[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' > "$RUNNER_TEMP/openai-request.json"
|
||||||
|
|
||||||
|
curl --fail-with-body --retry 3 --max-time 120 \
|
||||||
|
-H "Authorization: Bearer $OPENAI_API_KEY" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
https://api.openai.com/v1/responses \
|
||||||
|
--data-binary "@$RUNNER_TEMP/openai-request.json" \
|
||||||
|
> "$RUNNER_TEMP/openai-response.json"
|
||||||
|
|
||||||
|
jq -r '
|
||||||
|
[
|
||||||
|
.output[]? |
|
||||||
|
select(.type == "message") |
|
||||||
|
.content[]? |
|
||||||
|
select(.type == "output_text") |
|
||||||
|
.text
|
||||||
|
] | join("")
|
||||||
|
' "$RUNNER_TEMP/openai-response.json" > "$RUNNER_TEMP/triage.json"
|
||||||
|
|
||||||
|
jq -e '
|
||||||
|
(.category | type == "string") and
|
||||||
|
(.confidence | type == "number") and
|
||||||
|
(.risk | type == "string") and
|
||||||
|
(.summary | type == "string") and
|
||||||
|
(.response | type == "string") and
|
||||||
|
(.missing_information | type == "array")
|
||||||
|
' "$RUNNER_TEMP/triage.json" > /dev/null
|
||||||
|
|
||||||
|
category="$(jq -r '.category' "$RUNNER_TEMP/triage.json")"
|
||||||
|
addon="$(jq -r '.addon // ""' "$RUNNER_TEMP/triage.json")"
|
||||||
|
existing_addon=false
|
||||||
|
if [[ -n "$addon" && "$addon" != */* && "$addon" != "." && "$addon" != ".." ]] &&
|
||||||
|
[[ -f "$addon/config.yaml" || -f "$addon/config.json" ]]; then
|
||||||
|
existing_addon=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "addon=$addon" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "category=$category" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "confidence=$(jq -r '.confidence' "$RUNNER_TEMP/triage.json")" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "existing_addon=$existing_addon" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "risk=$(jq -r '.risk' "$RUNNER_TEMP/triage.json")" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Publish triage result
|
||||||
|
if: github.event.action == 'opened' || github.event.action == 'reopened'
|
||||||
|
env:
|
||||||
|
EXISTING_ADDON: ${{ steps.result.outputs.existing_addon }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_ACTION: ${{ github.event.action }}
|
||||||
|
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
ISSUE_UPDATED_AT: ${{ github.event.issue.updated_at }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
REPOSITORY_OWNER: ${{ github.repository_owner }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
marker="<!-- ai-triage:${ISSUE_UPDATED_AT} -->"
|
||||||
|
existing="$(
|
||||||
|
gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json comments |
|
||||||
|
jq --arg marker "$marker" \
|
||||||
|
'[.comments[] | select(.body | contains($marker))] | length'
|
||||||
|
)"
|
||||||
|
[[ "$existing" -eq 0 ]] || exit 0
|
||||||
|
|
||||||
|
category="$(jq -r '.category' "$RUNNER_TEMP/triage.json")"
|
||||||
|
confidence="$(jq -r '.confidence' "$RUNNER_TEMP/triage.json")"
|
||||||
|
risk="$(jq -r '.risk' "$RUNNER_TEMP/triage.json")"
|
||||||
|
summary="$(jq -r '.summary' "$RUNNER_TEMP/triage.json")"
|
||||||
|
response="$(jq -r '.response' "$RUNNER_TEMP/triage.json")"
|
||||||
|
|
||||||
|
confident=false
|
||||||
|
if awk -v confidence="$confidence" -v minimum="$MIN_CONFIDENCE" \
|
||||||
|
'BEGIN { exit !(confidence >= minimum) }'; then
|
||||||
|
confident=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
labels=("ai: triaged")
|
||||||
|
heading="### AI triage"
|
||||||
|
body="$response"
|
||||||
|
|
||||||
|
if [[ "$confident" != true ]]; then
|
||||||
|
labels+=("ai: maintainer-review")
|
||||||
|
body="**Low-confidence assessment (${confidence}):** ${summary}
|
||||||
|
|
||||||
|
A maintainer should review this issue before any automated action."
|
||||||
|
else
|
||||||
|
case "$category" in
|
||||||
|
question)
|
||||||
|
if [[ "$(jq -r '.safe_to_answer_automatically' "$RUNNER_TEMP/triage.json")" == true ]]; then
|
||||||
|
labels+=("ai: answered")
|
||||||
|
else
|
||||||
|
labels+=("ai: maintainer-review")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
missing_information)
|
||||||
|
heading="### Additional information required"
|
||||||
|
labels+=("ai: needs-info")
|
||||||
|
missing="$(
|
||||||
|
jq -r '
|
||||||
|
if (.missing_information | length) == 0 then
|
||||||
|
""
|
||||||
|
else
|
||||||
|
"\n\n**Needed:**\n" +
|
||||||
|
(.missing_information | map("- " + .) | join("\n"))
|
||||||
|
end
|
||||||
|
' "$RUNNER_TEMP/triage.json"
|
||||||
|
)"
|
||||||
|
body="${response}${missing}"
|
||||||
|
;;
|
||||||
|
bug)
|
||||||
|
labels+=("bug" "ai: fix-proposed")
|
||||||
|
body="**Assessment:** ${summary}
|
||||||
|
|
||||||
|
**Estimated risk:** ${risk}
|
||||||
|
|
||||||
|
${response}"
|
||||||
|
if [[ "$ISSUE_AUTHOR" == "$REPOSITORY_OWNER" ]]; then
|
||||||
|
labels+=("ai: fixing")
|
||||||
|
body="${body}
|
||||||
|
|
||||||
|
Repository analysis and a draft fix attempt will start automatically because the issue was opened by the repository owner."
|
||||||
|
else
|
||||||
|
body="${body}
|
||||||
|
|
||||||
|
A maintainer can approve repository analysis and an automated draft fix attempt by adding the \`ai: fix-approved\` label."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
improvement)
|
||||||
|
labels+=("enhancement")
|
||||||
|
if [[ "$EXISTING_ADDON" == true ]]; then
|
||||||
|
labels+=("ai: fix-proposed" "ai: fixing")
|
||||||
|
body="**Assessment:** ${summary}
|
||||||
|
|
||||||
|
**Estimated risk:** ${risk}
|
||||||
|
|
||||||
|
${response}
|
||||||
|
|
||||||
|
This targets an existing add-on, so repository analysis and a validated draft fix attempt will start automatically."
|
||||||
|
else
|
||||||
|
labels+=("ai: maintainer-review")
|
||||||
|
body="${response}
|
||||||
|
|
||||||
|
The referenced add-on directory could not be verified, so no automated implementation will start."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
new_addon_request)
|
||||||
|
labels+=("enhancement" "ai: new-addon-request" "ai: maintainer-review")
|
||||||
|
body="${response}
|
||||||
|
|
||||||
|
New add-on requests are never accepted or implemented automatically by this workflow. A maintainer must review the proposal manually."
|
||||||
|
;;
|
||||||
|
unsupported | spam)
|
||||||
|
labels+=("ai: maintainer-review")
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
body="${marker}
|
||||||
|
${heading}
|
||||||
|
|
||||||
|
${body}
|
||||||
|
|
||||||
|
<sub>Automated classification using \`${TRIAGE_MODEL}\`; confidence ${confidence}.</sub>"
|
||||||
|
|
||||||
|
for label in "${labels[@]}"; do
|
||||||
|
if ! gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$label"; then
|
||||||
|
if [[ "$label" == "bug" || "$label" == "enhancement" ]]; then
|
||||||
|
echo "Optional repository label '$label' does not exist; continuing." >&2
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"
|
||||||
|
|
||||||
|
codex_fix:
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
needs.detect_submitter.result == 'success' &&
|
||||||
|
needs.detect_submitter.outputs.matched != 'true' &&
|
||||||
|
needs.triage.result == 'success' &&
|
||||||
|
fromJSON(needs.triage.outputs.confidence) >=
|
||||||
|
fromJSON(vars.AI_MIN_CONFIDENCE || '0.80') &&
|
||||||
|
(
|
||||||
|
(
|
||||||
|
needs.triage.outputs.category == 'improvement' &&
|
||||||
|
needs.triage.outputs.existing_addon == 'true'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
needs.triage.outputs.category == 'bug' &&
|
||||||
|
(
|
||||||
|
(
|
||||||
|
github.event.action == 'labeled' &&
|
||||||
|
github.event.label.name == 'ai: fix-approved'
|
||||||
|
) ||
|
||||||
|
(
|
||||||
|
(github.event.action == 'opened' || github.event.action == 'reopened') &&
|
||||||
|
github.event.issue.user.login == github.repository_owner
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
needs: [detect_submitter, triage]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: read
|
||||||
|
pull-requests: read
|
||||||
|
outputs:
|
||||||
|
addon: ${{ steps.prepare.outputs.addon }}
|
||||||
|
branch: ${{ steps.prepare.outputs.branch }}
|
||||||
|
category: ${{ steps.prepare.outputs.category }}
|
||||||
|
existing_pr: ${{ steps.prepare.outputs.existing_pr }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Check for an existing pull request
|
||||||
|
id: prepare
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
TRIAGE_ADDON: ${{ needs.triage.outputs.addon }}
|
||||||
|
TRIAGE_CATEGORY: ${{ needs.triage.outputs.category }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
branch="ai/issue-${ISSUE_NUMBER}"
|
||||||
|
existing_pr="$(
|
||||||
|
gh pr list --repo "$REPO" --state open --head "$branch" \
|
||||||
|
--json url --jq '.[0].url // ""'
|
||||||
|
)"
|
||||||
|
echo "addon=$TRIAGE_ADDON" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "branch=$branch" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "category=$TRIAGE_CATEGORY" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "existing_pr=$existing_pr" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build isolated issue context
|
||||||
|
if: steps.prepare.outputs.existing_pr == ''
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
TRIAGE_ADDON: ${{ needs.triage.outputs.addon }}
|
||||||
|
TRIAGE_CATEGORY: ${{ needs.triage.outputs.category }}
|
||||||
|
TRIAGE_RISK: ${{ needs.triage.outputs.risk }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--json number,title,body,author,comments,labels,url |
|
||||||
|
jq \
|
||||||
|
--arg addon "$TRIAGE_ADDON" \
|
||||||
|
--arg category "$TRIAGE_CATEGORY" \
|
||||||
|
--arg risk "$TRIAGE_RISK" \
|
||||||
|
'. + {automation_triage: {addon: $addon, category: $category, risk: $risk}}' \
|
||||||
|
> ai-issue-context.json
|
||||||
|
cp .github/ai/fix-prompt.md codex-prompt.md
|
||||||
|
|
||||||
|
- name: Run Codex
|
||||||
|
id: codex
|
||||||
|
if: steps.prepare.outputs.existing_pr == ''
|
||||||
|
uses: openai/codex-action@v1
|
||||||
|
with:
|
||||||
|
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
prompt-file: codex-prompt.md
|
||||||
|
output-file: codex-result.md
|
||||||
|
sandbox: workspace-write
|
||||||
|
safety-strategy: drop-sudo
|
||||||
|
allow-users: "*"
|
||||||
|
model: ${{ vars.OPENAI_FIX_MODEL }}
|
||||||
|
effort: high
|
||||||
|
|
||||||
|
- name: Package proposed patch
|
||||||
|
if: steps.prepare.outputs.existing_pr == ''
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -f codex-result.md
|
||||||
|
cp codex-result.md "$RUNNER_TEMP/codex-result.md"
|
||||||
|
rm -f ai-issue-context.json codex-prompt.md codex-result.md
|
||||||
|
git add -A
|
||||||
|
git diff --cached --binary --full-index > "$RUNNER_TEMP/ai.patch"
|
||||||
|
git reset
|
||||||
|
cp "$RUNNER_TEMP/ai.patch" ai.patch
|
||||||
|
cp "$RUNNER_TEMP/codex-result.md" codex-result.md
|
||||||
|
|
||||||
|
- name: Upload proposed patch
|
||||||
|
if: steps.prepare.outputs.existing_pr == ''
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ai-issue-${{ github.event.issue.number }}-${{ github.run_id }}
|
||||||
|
path: |
|
||||||
|
ai.patch
|
||||||
|
codex-result.md
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 3
|
||||||
|
|
||||||
|
report_codex_failure:
|
||||||
|
if: always() && needs.codex_fix.result == 'failure'
|
||||||
|
needs: codex_fix
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Report failed automated analysis
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--remove-label "$label" || true
|
||||||
|
done
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--add-label "ai: maintainer-review"
|
||||||
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body \
|
||||||
|
"### Automated analysis failed
|
||||||
|
|
||||||
|
The automated repository analysis did not complete, so no pull request was created. A maintainer should review the issue and the [workflow run](${RUN_URL})."
|
||||||
|
|
||||||
|
publish_fix:
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
needs.codex_fix.result == 'success' &&
|
||||||
|
needs.codex_fix.outputs.existing_pr == ''
|
||||||
|
needs: codex_fix
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Download proposed patch
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ai-issue-${{ github.event.issue.number }}-${{ github.run_id }}
|
||||||
|
|
||||||
|
- name: Apply patch
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if [[ -s ai.patch ]]; then
|
||||||
|
git apply --index --3way ai.patch
|
||||||
|
fi
|
||||||
|
rm -f ai.patch codex-result.md
|
||||||
|
|
||||||
|
- name: Restore Codex report
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ai-issue-${{ github.event.issue.number }}-${{ github.run_id }}
|
||||||
|
path: ${{ runner.temp }}/ai-result
|
||||||
|
|
||||||
|
- name: Ensure publication labels exist
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
gh label create "ai: maintainer-review" --repo "$REPO" --color "d93f0b" --description "Maintainer review is required" --force
|
||||||
|
gh label create "ai: pr-created" --repo "$REPO" --color "0e8a16" --description "Automated draft pull request created" --force
|
||||||
|
gh label create "ai-generated" --repo "$REPO" --color "bfdadc" --description "Changes generated with AI assistance" --force
|
||||||
|
|
||||||
|
- name: Validate proposed patch
|
||||||
|
id: validation
|
||||||
|
env:
|
||||||
|
AI_EXPECTED_ADDON: ${{ needs.codex_fix.outputs.addon }}
|
||||||
|
AI_MAX_CHANGED_FILES: ${{ vars.AI_MAX_CHANGED_FILES || '25' }}
|
||||||
|
AI_MAX_CHANGED_LINES: ${{ vars.AI_MAX_CHANGED_LINES || '2000' }}
|
||||||
|
AI_REQUEST_CATEGORY: ${{ needs.codex_fix.outputs.category }}
|
||||||
|
run: |
|
||||||
|
set -o pipefail
|
||||||
|
bash .github/scripts/validate_ai_patch.sh origin/master 2>&1 |
|
||||||
|
tee "$RUNNER_TEMP/ai-validation.log"
|
||||||
|
|
||||||
|
- name: Report validation failure
|
||||||
|
if: failure() && steps.validation.outcome == 'failure'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md" 2>/dev/null || true)"
|
||||||
|
validation="$(tail -n 80 "$RUNNER_TEMP/ai-validation.log" 2>/dev/null || true)"
|
||||||
|
body="### Automated fix blocked by validation
|
||||||
|
|
||||||
|
${report}
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Validation output</summary>
|
||||||
|
|
||||||
|
\`\`\`text
|
||||||
|
${validation}
|
||||||
|
\`\`\`
|
||||||
|
</details>
|
||||||
|
|
||||||
|
[Open the workflow run](${RUN_URL})"
|
||||||
|
for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--remove-label "$label" || true
|
||||||
|
done
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--add-label "ai: maintainer-review"
|
||||||
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"
|
||||||
|
|
||||||
|
- name: Report no safe change
|
||||||
|
if: steps.validation.outputs.has_changes == 'false'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md")"
|
||||||
|
body="### Automated repository analysis
|
||||||
|
|
||||||
|
${report}
|
||||||
|
|
||||||
|
No draft pull request was created because Codex produced no repository change."
|
||||||
|
for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--remove-label "$label" || true
|
||||||
|
done
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--add-label "ai: maintainer-review"
|
||||||
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"
|
||||||
|
|
||||||
|
- name: Commit and push validated patch
|
||||||
|
if: steps.validation.outputs.has_changes == 'true'
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ needs.codex_fix.outputs.branch }}
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git fetch origin "$BRANCH:refs/remotes/origin/$BRANCH" || true
|
||||||
|
git checkout -B "$BRANCH"
|
||||||
|
git add -A
|
||||||
|
git commit -m "fix: address issue #${ISSUE_NUMBER}"
|
||||||
|
gh auth setup-git
|
||||||
|
git push --force-with-lease --set-upstream origin "$BRANCH"
|
||||||
|
|
||||||
|
- name: Create draft pull request
|
||||||
|
id: pr
|
||||||
|
if: steps.validation.outputs.has_changes == 'true'
|
||||||
|
env:
|
||||||
|
BRANCH: ${{ needs.codex_fix.outputs.branch }}
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md")"
|
||||||
|
cat > "$RUNNER_TEMP/pr-body.md" <<EOF
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Automated analysis and validated minimal fix for #${ISSUE_NUMBER}.
|
||||||
|
|
||||||
|
${report}
|
||||||
|
|
||||||
|
## Safety and review
|
||||||
|
|
||||||
|
- The issue content was treated as untrusted input.
|
||||||
|
- Codex had no repository write token.
|
||||||
|
- A fresh job applied and validated the patch.
|
||||||
|
- This pull request is intentionally a draft and will not be merged automatically.
|
||||||
|
|
||||||
|
Closes #${ISSUE_NUMBER}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
pr_url="$(
|
||||||
|
gh pr create \
|
||||||
|
--repo "$REPO" \
|
||||||
|
--base master \
|
||||||
|
--head "$BRANCH" \
|
||||||
|
--draft \
|
||||||
|
--title "fix: address issue #${ISSUE_NUMBER}" \
|
||||||
|
--body-file "$RUNNER_TEMP/pr-body.md"
|
||||||
|
)"
|
||||||
|
echo "url=$pr_url" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
pr_number="${pr_url##*/}"
|
||||||
|
gh pr edit "$pr_number" --repo "$REPO" --add-label "ai-generated"
|
||||||
|
|
||||||
|
- name: Update issue with analysis and pull request
|
||||||
|
if: steps.validation.outputs.has_changes == 'true'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
PR_URL: ${{ steps.pr.outputs.url }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
report="$(cat "$RUNNER_TEMP/ai-result/codex-result.md")"
|
||||||
|
body="### Automated fix prepared
|
||||||
|
|
||||||
|
${report}
|
||||||
|
|
||||||
|
**Draft pull request:** ${PR_URL}
|
||||||
|
|
||||||
|
The pull request remains in draft pending human review and CI."
|
||||||
|
for label in "ai: fix-approved" "ai: fix-proposed" "ai: fixing"; do
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--remove-label "$label" || true
|
||||||
|
done
|
||||||
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--add-label "ai: pr-created"
|
||||||
|
gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$body"
|
||||||
Reference in New Issue
Block a user