mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-16 18:12:29 +02:00
fix(ci): allow the github-actions bot actor, stop quarantining on systemic failure (#2948)
* fix(ci): allow the github-actions bot actor, stop quarantining on systemic failure Follow-up to #2947, from watching it run in production. The catch-up dispatched for the first time (the 403 is gone), but all five dispatched runs then failed: Actor type: Bot ##[error]Workflow initiated by non-human actor: github-actions (type: Bot). checkHumanActor (src/github/validation/actor.ts) is a SEPARATE gate from the write-permission one, and rejects any actor whose account type is not User. allowed_non_write_users does not cover it — that is only consulted for User accounts. Switching the catch-up to GITHUB_TOKEN in #2947 made those runs arrive as github-actions[bot], so it traded the 403 for this. Fixed with `allowed_bots: "github-actions"` — named rather than "*", since only this repo's own workflows dispatch as that actor. Scheduled runs are unaffected either way: they arrive as actor=alexbelgium, a User, which is also why the tier-2 sweep never hit this. The same run exposed a design error in #2947's bounded retry. It quarantined an issue with ai:needs-human when the ACTION failed — but an action failure is systemic, hitting every issue identically, so a workflow-level fault silently buried a batch a day. It is the opposite case that is issue-specific: the action ran fine and the model still produced no usable verdict. Inverted: * action failed -> fail red, touch no labels, let the catch-up retry * ran but no verdict -> one retry, then ai:needs-human on the second look Five issues (#2847 #2850 #2852 #2896 #2918) were quarantined by the old rule and need their ai:needs-human removed once this lands, so they re-enter the queue. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): allow the coderabbitai bot actor on the PR follow-up tier Same gate, latent instance. on_pr_coderabbit.yml fires on a review submitted by coderabbitai[bot], so github.actor is a Bot-type account and checkHumanActor rejects it. Every run so far skipped on the `ai-fix/*` branch guard before ever reaching the action, so this has never surfaced — it would have failed on the first genuine invocation, taking the whole CodeRabbit follow-up tier with it. Note this is NOT covered by the write-permission check returning early for [bot] actors: checkHumanActor is a separate gate consulted independently. Audited all five claude-code-action call sites. The other three need nothing: on_claude_mention and on_issue_approved are gated to alexbelgium, and the daily_ai_fix schedule runs as actor=alexbelgium — confirmed from run metadata, not assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): clear retry triggers when escalating to a human; correct a comment CodeRabbit: the no-verdict escalation added ai:needs-human but left ai-triage and ai:needs-info in place. The catch-up search excludes both, so the automated path never reaches it — but a MANUAL re-triage of an already-queued issue does, and there it matters: ai-triage would keep an issue we just handed to a human sitting in tier 2's unattended fix queue, and ai:needs-info would let a reporter reply silently re-trigger classification behind the human's back. The normal verdict path already clears stale control labels; this makes the escalation path consistent with it. Copilot: the action-failure comment claimed "touch no labels", but the ai:needs-info restore above may already have run on the issue_comment path. Reworded to say it adds no labels of its own, and why the restore still stands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ci): check the action outcome before trusting its structured output CodeRabbit caught the silent-failure mode sneaking back in. The shape check ran first, so if the action failed AFTER writing a valid structured output, the object sailed through, labels and a comment were applied, and the step exited 0 — a green run on a failed action, which is the precise thing this workflow was rebuilt to eliminate. Reproduced: valid verdict + CLASSIFY_OUTCOME=failure applied bug/ai-triage/ai:classified and posted the comment at exit 0. A failed action means its output is not trustworthy, full stop, so the outcome check now runs before the payload is read at all. That also reads better as two sequential gates rather than nested branches: gate 1 action failed -> restore ai:needs-info, fail red, add nothing gate 2 payload unusable -> restore ai:needs-info, warn, escalate on 2nd look otherwise -> normal verdict handling The ai:needs-info restore is now a function rather than being repeated at each exit, since both gates need it. Re-tested all 12 paths: the two newly-corrected cases plus a full regression sweep over empty/array/valid payloads across issues, issue_comment and workflow_dispatch, and the owned / low-confidence / label-grab branches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
110
.github/workflows/on_issues_ai_triage.yaml
vendored
110
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -175,6 +175,20 @@ jobs:
|
||||
# contents:read + issues:write, the model gets no credentials and no
|
||||
# Bash, and every value it produces is validated in Apply verdict.
|
||||
allowed_non_write_users: "*"
|
||||
# Separate gate from the one above, and it bit the catch-up path in
|
||||
# production: checkHumanActor (src/github/validation/actor.ts)
|
||||
# rejects any actor whose account type is not User. The catch-up
|
||||
# dispatches with GITHUB_TOKEN, so those runs arrive as
|
||||
# github-actions[bot] and died with "Workflow initiated by non-human
|
||||
# actor". allowed_non_write_users does NOT cover this — it is only
|
||||
# consulted for User accounts.
|
||||
# Named rather than "*": only this repo's own workflows can dispatch
|
||||
# as github-actions, whereas "*" would also admit any other App that
|
||||
# can reach a trigger. The matcher lowercases and strips a trailing
|
||||
# [bot], so this entry matches the github-actions[bot] actor.
|
||||
# Scheduled runs are unaffected either way — they arrive as
|
||||
# actor=alexbelgium, a User.
|
||||
allowed_bots: "github-actions"
|
||||
show_full_output: true
|
||||
prompt: |
|
||||
Read /tmp/ai-triage/context.md, then follow the instructions in
|
||||
@@ -220,46 +234,72 @@ jobs:
|
||||
set -euo pipefail
|
||||
mkdir -p /tmp/ai-triage
|
||||
F=/tmp/ai-triage/verdict.json
|
||||
|
||||
# A reporter reply consumed ai:needs-info in the claim step above, so
|
||||
# every early exit below has to restore it or the next reply could
|
||||
# never re-trigger. Defined once here rather than repeated per exit.
|
||||
restore_needs_info() {
|
||||
[ "${EVENT_NAME:-}" = "issue_comment" ] || return 0
|
||||
gh issue edit "$ISSUE" --repo "$REPO" --add-label ai:needs-info >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
# GATE 1 — did the action itself run? This is checked BEFORE looking
|
||||
# at the payload, because the action can fail *after* having written
|
||||
# a valid structured output: the object would sail through the shape
|
||||
# check below, labels and a comment would be applied, and the step
|
||||
# would exit 0 — a green run on a failed action, which is the exact
|
||||
# silent-failure mode this workflow was rebuilt to eliminate.
|
||||
# A failed action means its output is not trustworthy, full stop.
|
||||
#
|
||||
# This branch is also deliberately label-neutral. An action failure
|
||||
# (auth, config, an outage) is systemic — it hits every issue the
|
||||
# same way — so quarantining here would silently bury a batch a day
|
||||
# while the real fault sits in the workflow. Fail red, add nothing,
|
||||
# let the catch-up retry once it's fixed. Classify carries
|
||||
# continue-on-error so this step still runs at all; without the
|
||||
# explicit exit 1 the job would report success.
|
||||
if [ "${CLASSIFY_OUTCOME:-}" = "failure" ]; then
|
||||
restore_needs_info
|
||||
echo "::error::the Classify action failed for #$ISSUE — this is usually a workflow-level fault affecting every issue, so the issue is left untouched for a retry. See the Classify step."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The verdict arrives as the action's schema-validated structured
|
||||
# output rather than a file the model wrote — see the Classify step.
|
||||
# Materialise it here so the validation below is unchanged.
|
||||
printf '%s' "${STRUCTURED:-}" > "$F"
|
||||
# `jq -e .` alone accepts any truthy JSON, so a verdict of `[1,2]` or
|
||||
# `"hi"` passes here and then dies on `.verdict` below with "Cannot
|
||||
# index array with string" — set -e kills the step before the
|
||||
# ai:needs-info restore below, stranding the issue so no later reply
|
||||
# can ever re-trigger it. Require an object.
|
||||
|
||||
# GATE 2 — the action ran, but is the payload usable? `jq -e .` alone
|
||||
# accepts any truthy JSON, so a verdict of `[1,2]` or `"hi"` would
|
||||
# pass and then die on `.verdict` below with "Cannot index array with
|
||||
# string", killing the step under set -e before the restore. Require
|
||||
# an object.
|
||||
#
|
||||
# Reaching here means the failure is specific to THIS issue — the
|
||||
# model looked at it and produced nothing usable — so a retry is
|
||||
# worth exactly one attempt. A workflow_dispatch is the catch-up or
|
||||
# a manual re-triage, i.e. the second look, so hand it to a human
|
||||
# rather than re-dispatching the same issue every day forever;
|
||||
# ai:needs-human is in the catch-up exclusion search, so it drops out
|
||||
# of the queue instead of starving newer issues behind it.
|
||||
if [ ! -s "$F" ] || ! jq -e 'type == "object"' "$F" >/dev/null 2>&1; then
|
||||
# A reporter reply consumed ai:needs-info in the claim step above.
|
||||
# With no verdict we would otherwise leave the issue with the flag
|
||||
# gone, so the next reply could never re-trigger — restore it.
|
||||
# Do this FIRST, before any exit path below.
|
||||
if [ "${EVENT_NAME:-}" = "issue_comment" ]; then
|
||||
gh issue edit "$ISSUE" --repo "$REPO" --add-label ai:needs-info >/dev/null 2>&1 || true
|
||||
restore_needs_info
|
||||
echo "::warning::no usable verdict produced for #$ISSUE"
|
||||
if [ "${EVENT_NAME:-}" = "workflow_dispatch" ]; then
|
||||
echo "::warning::second attempt produced no verdict, handing #$ISSUE to a human"
|
||||
gh label create ai:needs-human --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||
# Drop the retry triggers in the same call. The catch-up search
|
||||
# already excludes both, so this only bites on a MANUAL
|
||||
# re-triage of an issue that still carries them — but there it
|
||||
# matters: leaving ai-triage would keep an issue we just handed
|
||||
# to a human sitting in tier 2's unattended fix queue, and
|
||||
# leaving ai:needs-info would let a reporter reply silently
|
||||
# re-trigger classification behind the human's back. The normal
|
||||
# verdict path below already clears stale control labels; this
|
||||
# keeps the escalation path consistent with it.
|
||||
gh issue edit "$ISSUE" --repo "$REPO" \
|
||||
--add-label ai:needs-human \
|
||||
--remove-label ai-triage --remove-label ai:needs-info >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Classify carries continue-on-error so the restore above always
|
||||
# gets to run. That also means a hard failure inside the action
|
||||
# reports the job green — which is exactly how tier 1 stayed dead
|
||||
# for weeks while every run showed success. Re-raise it here.
|
||||
if [ "${CLASSIFY_OUTCOME:-}" = "failure" ]; then
|
||||
echo "::error::classification failed (see the Classify step); no verdict produced for #$ISSUE"
|
||||
# A workflow_dispatch here is the daily catch-up (or a manual
|
||||
# re-triage) — i.e. this issue has already had at least one shot
|
||||
# and failed again. Hand it to a human rather than letting the
|
||||
# catch-up re-dispatch the same issue every day forever; the
|
||||
# label is in catchup's exclusion search, so it drops out of the
|
||||
# queue instead of starving the newer issues behind it.
|
||||
if [ "${EVENT_NAME:-}" = "workflow_dispatch" ]; then
|
||||
gh label create ai:needs-human --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||
gh issue edit "$ISSUE" --repo "$REPO" --add-label ai:needs-human >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Action succeeded but produced nothing usable: leave the issue
|
||||
# untriaged so tomorrow's catch-up picks it up, and stay green.
|
||||
echo "::warning::no usable verdict produced, leaving issue untouched"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
11
.github/workflows/on_pr_coderabbit.yml
vendored
11
.github/workflows/on_pr_coderabbit.yml
vendored
@@ -87,6 +87,17 @@ jobs:
|
||||
# coderabbitai[bot], the review submitter. AI_PR_TOKEN, not
|
||||
# GITHUB_TOKEN, so the pushed fixes re-trigger CI on the PR.
|
||||
github_token: ${{ secrets.AI_PR_TOKEN }}
|
||||
# Latent until now only because this job has never reached the action:
|
||||
# every run so far skipped on the `ai-fix/*` branch guard. On the
|
||||
# first real firing github.actor is coderabbitai[bot], and
|
||||
# checkHumanActor (src/github/validation/actor.ts) rejects any actor
|
||||
# whose account type is not User — a different gate from the write
|
||||
# check above, which does return early for a [bot] actor. Without
|
||||
# this the whole CodeRabbit follow-up tier would fail on its first
|
||||
# genuine invocation. Named, not "*": the job `if` already requires
|
||||
# the review to come from coderabbitai[bot], so nothing else can get
|
||||
# here anyway, and "*" would only widen it if that guard changed.
|
||||
allowed_bots: "coderabbitai"
|
||||
prompt: |
|
||||
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
|
||||
on ${{ github.repository }}. You are on that PR's branch. Follow
|
||||
|
||||
Reference in New Issue
Block a user