Files
hassio-addons/.github/workflows/daily_ai_fix.yaml
dependabot[bot] 5f9ecb7b05 chore(deps): bump anthropics/claude-code-action from 1.0.181 to 1.0.183 (#2914)
Bumps [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action) from 1.0.181 to 1.0.183.
- [Release notes](https://github.com/anthropics/claude-code-action/releases)
- [Commits](44423bdec7...be7b93b190)

---
updated-dependencies:
- dependency-name: anthropics/claude-code-action
  dependency-version: 1.0.183
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-27 21:35:07 +02:00

199 lines
9.1 KiB
YAML

---
# Destination: .github/workflows/daily_ai_fix.yaml
#
# Tier 2 of the AI triage system. One Opus 5 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"). Runs daily rather than weekly, so batches (default limit 8) stay
# small and any one day's failure doesn't block a week's worth of issues.
#
# This is the Opus step — root-cause diagnosis and, for anything not clearly a
# small confident fix, the written plan. Per issue it produces one of:
# * high certainty + small change -> a READY-for-review PR (ai:fixed)
# * medium certainty, or too large -> a full plan comment, no PR (ai:plan-pending)
# Alex then adds `ai:approved` and on_issue_approved.yaml (tier 3) executes
# the plan. This is the "create the full plan, ask for approval only when
# not sure" path — cheap to gate, immediate once approved.
# * fault is upstream / no fix -> analysis only (ai:upstream / ai:needs-human)
#
# Full tier map:
# Tier 1 on_issues_ai_triage.yaml Sonnet-low classify on issue open
# Tier 2 daily_ai_fix.yaml (this) Opus 5-xhigh daily fix/plan sweep
# Tier 3 on_issue_approved.yaml Opus 5-high execute an approved plan
# @claude on_claude_mention.yml Sonnet-low maintainer-only interactive
# PR on_pr_coderabbit.yml Sonnet-low one-shot CodeRabbit follow-up
# Kill switch: set repo variable AI_DISABLED=true to pause every AI workflow.
#
# Auth:
# Claude Pro/Max subscription via the CR_PAT GitHub Environment, which
# holds the CLAUDE_CODE_OAUTH_TOKEN secret (generate with `claude setup-token`).
# 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 * * *"
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:
# Global kill switch: set repo variable AI_DISABLED=true to pause the sweep
# (and every other AI workflow) without editing any file.
if: ${{ vars.AI_DISABLED != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 180
environment: CR_PAT
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.AI_PR_TOKEN }}
- name: Collect batch
id: batch
env:
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
REPO: ${{ github.repository }}
# workflow_dispatch inputs land here instead of being interpolated
# directly into the script below — expanding "${{ }}" inline would
# splice attacker/typo-controlled text into the shell source itself
# rather than passing it as data.
ISSUE_INPUT: ${{ inputs.issue }}
LIMIT_INPUT: ${{ inputs.limit || '8' }}
run: |
set -euo pipefail
mkdir -p /tmp/ai-fix
[[ "$LIMIT_INPUT" =~ ^[1-9][0-9]*$ ]] || { echo "::error::limit must be a positive integer, got '$LIMIT_INPUT'"; exit 1; }
if [ -n "$ISSUE_INPUT" ]; then
[[ "$ISSUE_INPUT" =~ ^[0-9]+$ ]] || { echo "::error::issue must be a number, got '$ISSUE_INPUT'"; exit 1; }
gh issue view "$ISSUE_INPUT" --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 "$LIMIT_INPUT" \
--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"
# Created up front so issue-fix.md's per-issue relabel never has to
# improvise a color or retry a "label does not exist" error — that's a
# wasted turn multiplied by every issue in the batch. Also covers
# ai:blocked, which the forbidden-paths guard applies later in this
# same job: with set -euo pipefail, `gh pr edit --add-label` on a
# label that doesn't exist yet fails and aborts that step's loop
# entirely, silently skipping every remaining PR behind it.
- name: Ensure control labels exist
if: steps.batch.outputs.count != '0'
env:
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
for l in ai:fixed ai:plan-pending ai:approved ai:upstream ai:needs-human ai:blocked; do
gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true
done
- name: Analyse and fix
if: steps.batch.outputs.count != '0'
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange. The scheduled path
# happens to pass it (github.actor is the maintainer), but
# workflow_dispatch by anyone else would 401. AI_PR_TOKEN, not
# GITHUB_TOKEN, so a PR Claude opens triggers CI.
github_token: ${{ secrets.AI_PR_TOKEN }}
# No track_progress here. It needs an issue or PR to hang its sticky
# comment on, and the action hard-fails validation without one; this
# workflow only ever runs on schedule/workflow_dispatch. Per-issue
# progress still gets reported — issue-fix.md has Claude comment on
# each issue directly via gh.
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-5
--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. issue-fix.md instructs Claude to drop the ai-triage
# label off every issue it finishes with (hard limit 6), so tomorrow's
# sweep never re-selects and re-spends a full read-and-fix pass on
# work that's already done. Enforce it here in case a turn or timeout
# budget runs out before the relabel step of the last issue or two.
- name: Guard against repeat processing
if: always() && steps.batch.outputs.count != '0'
env:
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
mapfile -t ISSUES < <(jq -r '.[].number' /tmp/ai-fix/batch.json)
for n in "${ISSUES[@]}"; do
STILL=$(gh issue view "$n" --repo "$REPO" --json labels \
--jq '[.labels[].name] | index("ai-triage") != null' 2>/dev/null) || {
echo "::warning::could not re-check issue #$n (deleted or transferred?), skipping"
continue
}
if [ "$STILL" = "true" ]; then
echo "::warning::issue #$n still carries ai-triage after the sweep, forcing it out of tomorrow's batch"
gh issue edit "$n" --repo "$REPO" \
--remove-label ai-triage --add-label ai:needs-human
fi
done
# Belt and braces. The prompt forbids .github/ and .templates/; this
# enforces it over every open ai-fix/ PR (shared by tiers 2, 3 and the
# CodeRabbit follow-up, so the rule lives in exactly one place).
- 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
# Run the guard from the trusted default-branch copy, never the in-tree
# copy the model could have modified in this same job.
DEFAULT=$(gh api "repos/${REPO}" --jq '.default_branch' 2>/dev/null || echo master)
if git fetch --depth=1 origin "$DEFAULT" >/dev/null 2>&1 \
&& git cat-file -e "FETCH_HEAD:.github/scripts/ai_guard_paths.sh" 2>/dev/null; then
git show "FETCH_HEAD:.github/scripts/ai_guard_paths.sh" | bash
else
echo "::warning::trusted guard copy unavailable, using in-tree copy"
bash .github/scripts/ai_guard_paths.sh
fi