mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-04 00:43:34 +02:00
chore: add two-tier AI issue triage (workflows, script, prompts)
Install tier-1 classifier (on_issues_ai_triage.yaml) and tier-2 fix sweep (weekly_ai_fix.yaml) plus their helper script and prompts. Pre-merge fixes from verification: - Wait-for-ping sleep 150s -> 60s. on_issues_ping_submitter completes in 6-11s of job time across the last 10 runs; 60s covers runner-queue skew with margin. - Rule 0 rewritten to match the real ownership signal: ping_submitter posts a github-actions[bot] comment with a stable marker `<!-- addon-submitter-ping:<addon> -->`. Rule 0 now keys off that literal marker instead of fuzzy prose, and guards against @<user> == alexbelgium. - Silence one intentional shellcheck SC2016 (literal Markdown backticks) so actionlint runs clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
99
.github/prompts/issue-classify.md
vendored
Normal file
99
.github/prompts/issue-classify.md
vendored
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# Issue classifier — tier 1
|
||||||
|
|
||||||
|
You are triaging a new issue on `alexbelgium/hassio-addons`, a monorepo of
|
||||||
|
100+ Home Assistant add-ons. Each add-on is a thin wrapper (Dockerfile,
|
||||||
|
`run.sh`, s6 services, nginx config, `config.yaml`) around an upstream
|
||||||
|
application that Alex does not maintain.
|
||||||
|
|
||||||
|
Your entire output is one JSON object written to `/tmp/ai-triage/verdict.json`.
|
||||||
|
You do not comment, label, or edit anything.
|
||||||
|
|
||||||
|
## Rule 0 — ownership short-circuit
|
||||||
|
|
||||||
|
Read the existing comments in the context bundle first. The
|
||||||
|
`on_issues_ping_submitter` workflow signals ownership by posting a **comment**
|
||||||
|
(authored by `github-actions[bot]`) that pings the add-on's original submitter.
|
||||||
|
Its exact, machine-stable format is:
|
||||||
|
|
||||||
|
```
|
||||||
|
<!-- addon-submitter-ping:<addon> -->
|
||||||
|
Heads up @<user>: this issue appears to mention `<addon>`.
|
||||||
|
```
|
||||||
|
|
||||||
|
Match it on the literal marker `<!-- addon-submitter-ping:` — that string is
|
||||||
|
the reliable signal; do not infer ownership from prose. If a comment carrying
|
||||||
|
that marker is present **and** the pinged `@<user>` is not `alexbelgium`, stop
|
||||||
|
immediately and emit:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"verdict": "owned", "confidence": "high"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not spend turns on anything else. (The workflow only ever pings a mapped
|
||||||
|
submitter, so in practice `@<user>` is always someone other than `alexbelgium`;
|
||||||
|
the check is a guard, not a common case.)
|
||||||
|
|
||||||
|
## Rule 1 — pick exactly one verdict
|
||||||
|
|
||||||
|
| verdict | when |
|
||||||
|
|---|---|
|
||||||
|
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
|
||||||
|
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
|
||||||
|
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |
|
||||||
|
| `upstream-bug` | The fault is in the upstream application or its image, not in this repo's wrapper. |
|
||||||
|
| `addon-bug` | The fault is in something this repo owns: the Dockerfile, `run.sh`, s6 service files, nginx config, `config.yaml` schema, or an option that is not being passed through. |
|
||||||
|
| `feature-request` | New capability, new add-on, new option. |
|
||||||
|
|
||||||
|
**The `upstream-bug` / `addon-bug` split is the one that matters.** Only
|
||||||
|
`addon-bug` triggers the expensive fix pass. Getting it wrong means the bot
|
||||||
|
opens a pull request against code that does not exist in this repository.
|
||||||
|
|
||||||
|
Test it explicitly: name the file in this repo you would have to change. If you
|
||||||
|
cannot name one, it is not `addon-bug`.
|
||||||
|
|
||||||
|
## Rule 2 — confidence is a real signal
|
||||||
|
|
||||||
|
Set `confidence` to `low` whenever any of these hold:
|
||||||
|
|
||||||
|
- The add-on could not be resolved from the title (`UNRESOLVED` in the bundle).
|
||||||
|
- The issue mixes several unrelated problems.
|
||||||
|
- You are choosing between `upstream-bug` and `addon-bug` and could argue both.
|
||||||
|
- The report is in a language you are not confident reading.
|
||||||
|
|
||||||
|
`low` confidence suppresses the comment entirely and flags a human instead.
|
||||||
|
Prefer that over a fluent guess. A wrong answer on a support issue costs Alex
|
||||||
|
more trust than no answer.
|
||||||
|
|
||||||
|
## Rule 3 — writing the comment
|
||||||
|
|
||||||
|
Only `duplicate`, `needs-info`, and `question` get a comment. The other verdicts
|
||||||
|
are labelled silently and handled later.
|
||||||
|
|
||||||
|
- **duplicate** — one line, link the other issue, no explanation.
|
||||||
|
- **needs-info** — ask only for what is *strictly* required to proceed, as a
|
||||||
|
short checklist. Never more than four items. Say where to find each one
|
||||||
|
(e.g. the add-on log tab, the Configuration tab). Do not ask for anything
|
||||||
|
already present in the issue body.
|
||||||
|
- **question** — answer only from files in the context bundle, and quote the
|
||||||
|
file path you took it from. If the bundle does not contain the answer, this
|
||||||
|
is `needs-info`, not `question`. Never invent option names.
|
||||||
|
|
||||||
|
Never close an issue. Never promise a timeline. Never say a fix is coming.
|
||||||
|
|
||||||
|
## Output schema
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"verdict": "owned|duplicate|needs-info|question|upstream-bug|addon-bug|feature-request",
|
||||||
|
"addon": "birdnet-go",
|
||||||
|
"confidence": "high|medium|low",
|
||||||
|
"duplicate_of": 1234,
|
||||||
|
"labels": ["bug"],
|
||||||
|
"root_cause_hint": "one sentence for the tier-2 pass, or empty",
|
||||||
|
"comment": "markdown, or empty string"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`labels` should contain at most two, from the repo's existing set. Do not
|
||||||
|
invent new label names; the workflow adds `ai-triage` and `ai:classified`
|
||||||
|
on its own.
|
||||||
74
.github/prompts/issue-fix.md
vendored
Normal file
74
.github/prompts/issue-fix.md
vendored
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Issue fix sweep — tier 2
|
||||||
|
|
||||||
|
You are working through a batch of confirmed add-on bugs on
|
||||||
|
`alexbelgium/hassio-addons`. Each add-on is a thin wrapper around an upstream
|
||||||
|
application. You own the wrapper. You do not own the upstream app.
|
||||||
|
|
||||||
|
Read `/tmp/ai-fix/batch.json`. Work add-on by add-on, not issue by issue —
|
||||||
|
grouping is the point of the batch.
|
||||||
|
|
||||||
|
## Hard limits
|
||||||
|
|
||||||
|
These are not guidelines. A workflow step enforces them after you finish, and
|
||||||
|
anything that violates them gets blocked and flagged.
|
||||||
|
|
||||||
|
1. **Never modify `.github/` or `.templates/`.** Those are inherited by every
|
||||||
|
add-on in the repo. A change there is a 100-add-on incident, not a fix.
|
||||||
|
2. **Never touch the `version` or `upstream` fields in `config.yaml`.** The
|
||||||
|
`addons_updater` job owns those. Editing them causes merge conflicts you
|
||||||
|
will not be around to resolve.
|
||||||
|
3. **One add-on per branch, one branch per pull request.** Branch name
|
||||||
|
`ai-fix/<addon>-<issue-number>`.
|
||||||
|
4. **Draft pull requests only.** Never merge, never mark ready for review,
|
||||||
|
never close an issue.
|
||||||
|
5. If the fix requires changing more than roughly 60 lines, or touching more
|
||||||
|
than three files, stop. Post the analysis, open no pull request, and say
|
||||||
|
plainly that the change is too large for an unattended fix.
|
||||||
|
|
||||||
|
## Per add-on, do this in order
|
||||||
|
|
||||||
|
**1. Read before you write.** The add-on's `CLAUDE.md` if it has one, then
|
||||||
|
`DOCS.md`, `config.yaml`, `Dockerfile`, and everything under `rootfs/`. Read
|
||||||
|
`CHANGELOG.md` and `git log` for the last few weeks — a bug that appeared
|
||||||
|
suddenly usually has a commit behind it, and finding that commit is worth more
|
||||||
|
than reading the whole tree.
|
||||||
|
|
||||||
|
**2. Establish the root cause, and be honest about confidence.** Name the exact
|
||||||
|
file and line. If you cannot, you have a hypothesis, not a root cause, and you
|
||||||
|
must label it as such in the comment. Do not dress a guess up as a diagnosis.
|
||||||
|
Alex has to trust these comments without re-deriving them.
|
||||||
|
|
||||||
|
**3. Re-check the upstream/wrapper split.** Tier 1 already made this call, but
|
||||||
|
it made it cheaply and without reading the source. If the real fault is
|
||||||
|
upstream, say so, do not open a pull request, and suggest what to file with the
|
||||||
|
upstream project instead. Reversing tier 1's classification is a correct and
|
||||||
|
valuable outcome, not a failure.
|
||||||
|
|
||||||
|
**4. Fix it.** Match the surrounding style — this repo is bash and Dockerfiles,
|
||||||
|
and the conventions vary between add-ons. Run `shellcheck` on any shell you
|
||||||
|
change. Add a `CHANGELOG.md` entry in the add-on's existing format.
|
||||||
|
|
||||||
|
**5. Open the draft pull request.** Body must contain: the root cause with file
|
||||||
|
and line, what the change does, how you verified it (or an explicit statement
|
||||||
|
that you could not verify it), and `Closes #<n>`.
|
||||||
|
|
||||||
|
**6. Comment on the issue.** Root cause, the fix in one or two sentences, and
|
||||||
|
the pull request link. Plain language — the reader is a Home Assistant user,
|
||||||
|
not a Go developer. If you found no fix, say what you ruled out and what you
|
||||||
|
would need to go further. Close with a note that this is automated analysis
|
||||||
|
pending Alex's review.
|
||||||
|
|
||||||
|
## Meta-findings
|
||||||
|
|
||||||
|
This is the part a per-issue run cannot do, so do not skip it.
|
||||||
|
|
||||||
|
After the batch, look across everything you read. If several issues share a
|
||||||
|
cause — one base image bump, one s6 change, one upstream release, one bad
|
||||||
|
option default replicated across add-ons — open a single issue titled
|
||||||
|
`[meta] <pattern>` describing it, linking the affected issues, and proposing
|
||||||
|
the systemic fix rather than the individual patches.
|
||||||
|
|
||||||
|
Report honestly if the batch produced nothing. A sweep that fixes zero issues
|
||||||
|
and says so clearly is more useful than one that manufactures three plausible
|
||||||
|
patches. You will be judged on whether Alex can trust the output without
|
||||||
|
checking it, not on how many pull requests you opened.
|
||||||
99
.github/scripts/ai_triage_context.sh
vendored
Executable file
99
.github/scripts/ai_triage_context.sh
vendored
Executable file
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Destination: .github/scripts/ai_triage_context.sh
|
||||||
|
#
|
||||||
|
# Builds /tmp/ai-triage/context.md so Claude does not have to explore a
|
||||||
|
# 100-addon, 34k-commit monorepo to answer one question. Everything the
|
||||||
|
# model needs is assembled here by cheap shell instead of by expensive turns.
|
||||||
|
#
|
||||||
|
# Env: GH_TOKEN, ISSUE_NUMBER, REPO
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OUT=/tmp/ai-triage
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
CTX="$OUT/context.md"
|
||||||
|
: > "$CTX"
|
||||||
|
|
||||||
|
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
|
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
|
||||||
|
|
||||||
|
TITLE=$(jq -r '.title' "$OUT/issue.json")
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- addon slug
|
||||||
|
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up".
|
||||||
|
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<<"$TITLE" | head -n1)
|
||||||
|
ADDON=""
|
||||||
|
if [ -n "$RAW" ]; then
|
||||||
|
CAND=$(tr '[:upper:] ' '[:lower:]_' <<<"$RAW")
|
||||||
|
# Directory list without checking out any of them.
|
||||||
|
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
|
||||||
|
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
|
||||||
|
if grep -qxF "$guess" "$OUT/dirs.txt"; then ADDON="$guess"; break; fi
|
||||||
|
done
|
||||||
|
# Last resort: longest directory name contained in the candidate.
|
||||||
|
if [ -z "$ADDON" ]; then
|
||||||
|
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
|
||||||
|
"$OUT/dirs.txt" | sort -rn | head -n1 | cut -f2)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Issue #${ISSUE_NUMBER}"
|
||||||
|
echo
|
||||||
|
echo "Repo: ${REPO}"
|
||||||
|
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
|
||||||
|
echo
|
||||||
|
echo "## Title"
|
||||||
|
echo "$TITLE"
|
||||||
|
echo
|
||||||
|
echo "## Author"
|
||||||
|
jq -r '.author.login' "$OUT/issue.json"
|
||||||
|
echo
|
||||||
|
echo "## Body"
|
||||||
|
echo '```'
|
||||||
|
jq -r '.body // "(empty)"' "$OUT/issue.json"
|
||||||
|
echo '```'
|
||||||
|
echo
|
||||||
|
echo "## Existing comments (in order)"
|
||||||
|
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
|
||||||
|
echo
|
||||||
|
echo "## Existing labels"
|
||||||
|
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
|
||||||
|
} >> "$CTX"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- addon sources
|
||||||
|
if [ -n "$ADDON" ]; then
|
||||||
|
git sparse-checkout set --no-cone .github/prompts .github/scripts "$ADDON" || true
|
||||||
|
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "## Addon files: ${ADDON}/"
|
||||||
|
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
|
||||||
|
[ -f "$ADDON/$f" ] || continue
|
||||||
|
echo
|
||||||
|
echo "### ${ADDON}/${f}"
|
||||||
|
echo '```'
|
||||||
|
head -c 8000 "$ADDON/$f"
|
||||||
|
echo '```'
|
||||||
|
done
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "## Recent commits touching ${ADDON}/"
|
||||||
|
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2>/dev/null || true
|
||||||
|
} >> "$CTX"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# -------------------------------------------------------- possible duplicates
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "## Similar existing issues (candidate duplicates)"
|
||||||
|
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<<"$TITLE" \
|
||||||
|
| tr '[:upper:]' '[:lower:]' \
|
||||||
|
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
|
||||||
|
gh search issues --repo "$REPO" --limit 15 \
|
||||||
|
--json number,title,state,url -- "$KEYWORDS" 2>/dev/null \
|
||||||
|
| jq -r '.[] | "- #\(.number) [\(.state)] \(.title)"' \
|
||||||
|
|| echo "(search unavailable)"
|
||||||
|
} >> "$CTX"
|
||||||
|
|
||||||
|
echo "context bundle: $(wc -c < "$CTX") bytes, addon=${ADDON:-none}"
|
||||||
136
.github/workflows/on_issues_ai_triage.yaml
vendored
Normal file
136
.github/workflows/on_issues_ai_triage.yaml
vendored
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
---
|
||||||
|
# Destination: .github/workflows/on_issues_ai_triage.yaml
|
||||||
|
#
|
||||||
|
# Tier 1. Fires on every new issue, costs cents, finishes in ~2 minutes.
|
||||||
|
# Classifies, de-duplicates, asks for missing info, answers simple questions,
|
||||||
|
# and applies the `ai-triage` label that tier 2 (weekly_ai_fix.yaml) picks up.
|
||||||
|
#
|
||||||
|
# Required secret: ANTHROPIC_API_KEY (or swap to claude_code_oauth_token)
|
||||||
|
|
||||||
|
name: AI issue triage
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ai-triage-${{ github.event.issue.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
MAINTAINER: alexbelgium
|
||||||
|
# Leave "true" for the first couple of weeks. The verdict is printed in the
|
||||||
|
# job log and nothing is written to the issue. Flip when it looks right.
|
||||||
|
DRY_RUN: "true"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
classify:
|
||||||
|
if: >-
|
||||||
|
github.event.issue.user.login != 'alexbelgium' &&
|
||||||
|
!contains(github.event.issue.labels.*.name, 'no-ai')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# on_issues_ping_submitter.yml has to land first: the classifier reads
|
||||||
|
# the existing comments and bails out if someone already owns the issue.
|
||||||
|
# Both workflows fire on the same issues.opened event and race. The
|
||||||
|
# submitter ping completes in 6-11s of job time across recent runs; 60s
|
||||||
|
# leaves a generous margin for runner-queue skew between the two jobs.
|
||||||
|
- name: Wait for ping_submitter
|
||||||
|
run: sleep 60
|
||||||
|
|
||||||
|
- name: Checkout tooling
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
sparse-checkout: |
|
||||||
|
.github/prompts
|
||||||
|
.github/scripts
|
||||||
|
sparse-checkout-cone-mode: false
|
||||||
|
|
||||||
|
- name: Build context bundle
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: bash .github/scripts/ai_triage_context.sh
|
||||||
|
|
||||||
|
- name: Classify
|
||||||
|
uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
prompt: |
|
||||||
|
Read /tmp/ai-triage/context.md, then follow the instructions in
|
||||||
|
.github/prompts/issue-classify.md exactly.
|
||||||
|
|
||||||
|
Write your verdict as a single JSON object to
|
||||||
|
/tmp/ai-triage/verdict.json and write nothing else anywhere.
|
||||||
|
Do NOT comment on or label the issue yourself.
|
||||||
|
claude_args: |
|
||||||
|
--model claude-sonnet-5
|
||||||
|
--effort low
|
||||||
|
--max-turns 12
|
||||||
|
--allowedTools "Read,Write,Glob,Grep,Bash(gh issue list:*),Bash(gh search issues:*)"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Apply verdict
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
ISSUE: ${{ github.event.issue.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
F=/tmp/ai-triage/verdict.json
|
||||||
|
if [ ! -s "$F" ] || ! jq -e . "$F" >/dev/null 2>&1; then
|
||||||
|
echo "::warning::no usable verdict produced, leaving issue untouched"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "--- verdict ---"; jq . "$F"; echo "---------------"
|
||||||
|
|
||||||
|
VERDICT=$(jq -r '.verdict // "unknown"' "$F")
|
||||||
|
CONF=$(jq -r '.confidence // "low"' "$F")
|
||||||
|
COMMENT=$(jq -r '.comment // ""' "$F")
|
||||||
|
mapfile -t LABELS < <(jq -r '.labels[]? // empty' "$F")
|
||||||
|
|
||||||
|
# Someone already owns this one: ping_submitter did its job.
|
||||||
|
if [ "$VERDICT" = "owned" ]; then
|
||||||
|
echo "issue already has an owner, nothing to do"; exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Low confidence never speaks. It just flags for a human.
|
||||||
|
if [ "$CONF" = "low" ]; then
|
||||||
|
LABELS=("ai:needs-human"); COMMENT=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ "$VERDICT" = "addon-bug" ] && LABELS+=("ai-triage")
|
||||||
|
LABELS+=("ai:classified")
|
||||||
|
|
||||||
|
if [ "${DRY_RUN:-true}" = "true" ]; then
|
||||||
|
echo "DRY_RUN: would apply labels: ${LABELS[*]}"
|
||||||
|
echo "DRY_RUN: would post comment:"; printf '%s\n' "$COMMENT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
for l in "${LABELS[@]}"; do
|
||||||
|
gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
gh issue edit "$ISSUE" --repo "$REPO" \
|
||||||
|
"${LABELS[@]/#/--add-label=}"
|
||||||
|
|
||||||
|
if [ -n "$COMMENT" ]; then
|
||||||
|
{
|
||||||
|
printf '%s\n\n' "$COMMENT"
|
||||||
|
printf -- '---\n'
|
||||||
|
printf '<sub>Automated triage. Not verified by a human yet '
|
||||||
|
# shellcheck disable=SC2016 # backticks are literal Markdown, not a subshell
|
||||||
|
printf -- '— @%s will confirm. Add the `no-ai` label to opt out.</sub>\n' "$MAINTAINER"
|
||||||
|
} > /tmp/ai-triage/comment.md
|
||||||
|
gh issue comment "$ISSUE" --repo "$REPO" --body-file /tmp/ai-triage/comment.md
|
||||||
|
fi
|
||||||
114
.github/workflows/weekly_ai_fix.yaml
vendored
Normal file
114
.github/workflows/weekly_ai_fix.yaml
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
---
|
||||||
|
# Destination: .github/workflows/weekly_ai_fix.yaml
|
||||||
|
#
|
||||||
|
# Tier 2. One Opus 4.8 run at xhigh over the whole batch of `ai-triage` issues,
|
||||||
|
# grouped by add-on, so it can spot the cross-issue patterns a per-issue run
|
||||||
|
# never sees ("these four reports are all the same base image bump").
|
||||||
|
#
|
||||||
|
# Required secrets:
|
||||||
|
# ANTHROPIC_API_KEY
|
||||||
|
# AI_PR_TOKEN — GitHub App token or PAT (repo scope). NOT GITHUB_TOKEN:
|
||||||
|
# pull requests created with GITHUB_TOKEN do not trigger
|
||||||
|
# other workflows, so your PR Check Build would never run.
|
||||||
|
|
||||||
|
name: AI fix sweep
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * SUN"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
issue:
|
||||||
|
description: "Single issue number (blank = all ai-triage issues)"
|
||||||
|
required: false
|
||||||
|
limit:
|
||||||
|
description: "Max issues in the batch"
|
||||||
|
required: false
|
||||||
|
default: "8"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ai-fix-sweep
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
sweep:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 180
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
token: ${{ secrets.AI_PR_TOKEN }}
|
||||||
|
|
||||||
|
- name: Collect batch
|
||||||
|
id: batch
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p /tmp/ai-fix
|
||||||
|
if [ -n "${{ inputs.issue }}" ]; then
|
||||||
|
gh issue view "${{ inputs.issue }}" --repo "$REPO" \
|
||||||
|
--json number,title,body,labels,comments \
|
||||||
|
| jq '[.]' > /tmp/ai-fix/batch.json
|
||||||
|
else
|
||||||
|
gh issue list --repo "$REPO" --state open \
|
||||||
|
--label ai-triage --limit "${{ inputs.limit || '8' }}" \
|
||||||
|
--json number,title,body,labels,comments > /tmp/ai-fix/batch.json
|
||||||
|
fi
|
||||||
|
N=$(jq 'length' /tmp/ai-fix/batch.json)
|
||||||
|
echo "count=$N" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "batch size: $N"
|
||||||
|
|
||||||
|
- name: Configure git
|
||||||
|
if: steps.batch.outputs.count != '0'
|
||||||
|
run: |
|
||||||
|
git config user.name "claude-ai-fix[bot]"
|
||||||
|
git config user.email "claude-ai-fix[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
|
- name: Analyse and fix
|
||||||
|
if: steps.batch.outputs.count != '0'
|
||||||
|
uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
prompt: |
|
||||||
|
The batch of issues to work through is /tmp/ai-fix/batch.json.
|
||||||
|
Follow .github/prompts/issue-fix.md exactly. Do not deviate from
|
||||||
|
the path restrictions in that file under any circumstances.
|
||||||
|
claude_args: |
|
||||||
|
--model claude-opus-4-8
|
||||||
|
--effort xhigh
|
||||||
|
--max-turns 300
|
||||||
|
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(shellcheck:*),Bash(yamllint:*),Bash(docker build:*)"
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||||
|
|
||||||
|
# Belt and braces. The prompt forbids these paths; this enforces it.
|
||||||
|
- name: Guard forbidden paths
|
||||||
|
if: always() && steps.batch.outputs.count != '0'
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
PRS=$(gh pr list --repo "$REPO" --state open --limit 50 \
|
||||||
|
--json number,headRefName \
|
||||||
|
--jq '.[] | select(.headRefName|startswith("ai-fix/")) | .number')
|
||||||
|
for pr in $PRS; do
|
||||||
|
BAD=$(gh pr diff "$pr" --repo "$REPO" --name-only \
|
||||||
|
| grep -E '^(\.github/|\.templates/)' || true)
|
||||||
|
if [ -n "$BAD" ]; then
|
||||||
|
echo "::error::PR #$pr touches protected paths:"; echo "$BAD"
|
||||||
|
gh pr ready "$pr" --repo "$REPO" --undo || true
|
||||||
|
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
|
||||||
|
gh pr comment "$pr" --repo "$REPO" --body \
|
||||||
|
"Blocked automatically: this PR modifies shared infrastructure (\`.github/\` or \`.templates/\`), which is inherited by every add-on in the repo. Needs manual review before it goes anywhere."
|
||||||
|
fi
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user