Files
hassio-addons/.github/workflows/on_issue_approved.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

200 lines
9.1 KiB
YAML

---
# Destination: .github/workflows/on_issue_approved.yaml
#
# Tier 3 of the AI triage system — the approval executor.
#
# Tier 2 (daily_ai_fix.yaml) posts a full `<!-- ai-plan -->` plan on any issue
# it is not confident enough to fix unattended, and labels it `ai:plan-pending`.
# When @alexbelgium reviews that plan and applies the `ai:approved` label, this
# workflow fires within a minute and executes the plan on Opus, opening a
# ready-for-review pull request — no waiting for the next daily sweep.
#
# Manual precedence: the label is the maintainer's explicit go-ahead, so this
# runs regardless of `no-ai`. Only @alexbelgium may approve; a label applied by
# anyone else is stripped and ignored (revoke job below).
#
# Auth: CR_PAT environment (CLAUDE_CODE_OAUTH_TOKEN) + AI_PR_TOKEN (so the PR
# triggers onpr_check-pr.yaml, which GITHUB_TOKEN-authored PRs would not).
# Kill switch: repo variable AI_DISABLED=true pauses this (and every AI workflow).
name: AI approved-plan executor
on:
issues:
types: [labeled]
workflow_dispatch:
inputs:
issue:
description: "Issue number whose approved plan to execute"
required: true
permissions:
contents: write
issues: write
pull-requests: write
concurrency:
group: ai-approve-${{ github.event.issue.number || inputs.issue }}
cancel-in-progress: false
jobs:
# A non-maintainer applied ai:approved: strip it and do nothing else. Only
# users with triage rights can label at all, so this is belt-and-braces.
revoke:
if: >-
github.event_name == 'issues' &&
github.event.label.name == 'ai:approved' &&
github.event.sender.login != 'alexbelgium'
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Remove unauthorised approval
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.number }}
run: |
set -euo pipefail
gh issue edit "$ISSUE" --repo "$REPO" --remove-label ai:approved || true
gh issue comment "$ISSUE" --repo "$REPO" --body \
"The \`ai:approved\` label only takes effect when applied by @alexbelgium; removing it."
execute:
# workflow_dispatch is a maintainer override, so it must also be gated to
# @alexbelgium — otherwise any collaborator with run-workflow rights could
# execute a Tier 3 plan without the approval label or sender check.
if: >-
vars.AI_DISABLED != 'true' &&
( ( github.event_name == 'workflow_dispatch' &&
github.actor == 'alexbelgium' ) ||
( github.event.label.name == 'ai:approved' &&
github.event.sender.login == 'alexbelgium' ) )
runs-on: ubuntu-latest
timeout-minutes: 60
environment: CR_PAT
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
token: ${{ secrets.AI_PR_TOKEN }}
- name: Configure git
run: |
git config user.name "claude-ai-fix[bot]"
git config user.email "claude-ai-fix[bot]@users.noreply.github.com"
- name: Ensure control labels exist
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:needs-human ai:blocked; do
gh label create "$l" --repo "$REPO" --color ededed --force >/dev/null 2>&1 || true
done
- name: Bundle the approved plan
id: bundle
env:
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.number || inputs.issue }}
run: |
set -euo pipefail
mkdir -p /tmp/ai-exec
gh issue view "$ISSUE" --repo "$REPO" \
--json number,title,body,author,labels,comments > /tmp/ai-exec/issue.json
# The plan is the most recent ai-plan comment FROM A TRUSTED AUTHOR.
# The marker alone is not proof of origin: any reporter can paste
# "<!-- ai-plan -->" into a comment, and picking it by marker+last
# would let them swap in a plan that then executes on approval.
# Tier 2 posts under AI_PR_TOKEN, whose identity is a repo OWNER/
# MEMBER/COLLABORATOR; a reporter is never one of those. (If you
# switch AI_PR_TOKEN to a GitHub App, add its bot login here.)
jq -r '[.comments[]
| select((.body | contains("<!-- ai-plan -->"))
and (.authorAssociation == "OWNER"
or .authorAssociation == "MEMBER"
or .authorAssociation == "COLLABORATOR"))]
| last | .body // ""' /tmp/ai-exec/issue.json > /tmp/ai-exec/plan.md
if [ ! -s /tmp/ai-exec/plan.md ]; then
echo "has_plan=false" >> "$GITHUB_OUTPUT"
echo "::warning::no trusted <!-- ai-plan --> comment on issue #$ISSUE; nothing to execute"
# Consume the approval here too, so a later real plan can be
# re-approved (re-adding ai:approved to an issue that still carries
# it would not fire a fresh labeled event).
gh issue edit "$ISSUE" --repo "$REPO" --remove-label ai:approved >/dev/null 2>&1 || true
gh issue comment "$ISSUE" --repo "$REPO" --body \
"No AI plan (\`<!-- ai-plan -->\`) from the triage bot was found on this issue, so \`ai:approved\` has nothing to execute (removed). Run the tier-2 sweep on it first (\`AI fix sweep\` → issue $ISSUE), then approve the plan it posts."
else
echo "has_plan=true" >> "$GITHUB_OUTPUT"
echo "plan: $(wc -c < /tmp/ai-exec/plan.md) bytes"
fi
- name: Execute the plan
if: steps.bundle.outputs.has_plan == 'true'
uses: anthropics/claude-code-action@be7b93b1907a4abad570368f3c74b6fe3807510b # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever
# github.actor lacks write access. AI_PR_TOKEN, not GITHUB_TOKEN, so
# a PR Claude opens triggers CI.
github_token: ${{ secrets.AI_PR_TOKEN }}
# Only the `issues` path has a comment thread to track progress in.
# On workflow_dispatch there is none, and passing true there fails
# the action's input validation outright.
track_progress: ${{ github.event_name == 'issues' }}
prompt: |
The approved plan is /tmp/ai-exec/plan.md and the issue it belongs
to is /tmp/ai-exec/issue.json. Follow .github/prompts/issue-execute-plan.md
exactly. Do not deviate from the path restrictions under any
circumstances.
claude_args: |
--model claude-opus-5
--effort high
--max-turns 200
--allowedTools "Read,Write,Edit,Glob,Grep,Bash(git:*),Bash(gh:*),Bash(shellcheck:*),Bash(yamllint:*),Bash(docker build:*)"
env:
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
# Consume the approval no matter how the run ended, so a stuck label can
# never re-fire this workflow, and flag a human if nothing shipped.
- name: Guard against repeat processing
if: always() && steps.bundle.outputs.has_plan == 'true'
env:
GH_TOKEN: ${{ secrets.AI_PR_TOKEN }}
REPO: ${{ github.repository }}
ISSUE: ${{ github.event.issue.number || inputs.issue }}
run: |
set -euo pipefail
gh issue edit "$ISSUE" --repo "$REPO" \
--remove-label ai:approved --remove-label ai:plan-pending >/dev/null 2>&1 || true
NOW=$(gh issue view "$ISSUE" --repo "$REPO" --json labels --jq '[.labels[].name]')
if ! echo "$NOW" | jq -e 'index("ai:fixed")!=null' >/dev/null \
&& ! echo "$NOW" | jq -e 'index("ai:upstream")!=null' >/dev/null; then
echo "::warning::issue #$ISSUE produced no PR/upstream outcome, flagging for a human"
gh issue edit "$ISSUE" --repo "$REPO" --add-label ai:needs-human >/dev/null 2>&1 || true
fi
# The plan should never touch shared infra, but enforce it on the PR just
# as the tier-2 sweep does — same script, single source of truth.
- name: Guard forbidden paths
if: always() && steps.bundle.outputs.has_plan == 'true'
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