Repository navigation
CI Failure Analyst never runs for failed GitHub Actions jobs: the check_run trigger only fires for third-party checks #2235
Description
Problem
CI Failure Analyst is documented as diagnosing failed CI, Lint and Test runs. In practice it never runs for a failed GitHub Actions job, in any repo, on any release. It only runs when a third-party app's check fails.
The standard stub (templates/ci-failure-analyst.yml) triggers on check_run: [completed], and the reusable reads only github.event.check_run.*. GitHub does not start a workflow for an event raised by GitHub Actions' own token, so a check run created by an Actions job never delivers a check_run event to the stub. Check runs from other apps (SonarCloud, for example) do.
Evidence (2026-10-10)
.github-private: more than 50 failed workflow runs today; of the analyst's last 300 runs, 300 were skipped or empty and none ran the analysis.- google-app-scripts: its only two successful analyst runs (37562808488, 36585887034) were both triggered by a SonarCloud check (
DETAILS_URL: https://sonarcloud.io/...). A failed Actions job on 2026-10-09 (ideate / Gather Project Signals) produced no analyst run. - TalkTerm: 8 successful runs in its last 300, the latest on 2026-09-07.
- bmad-bgreat-suite: still carries the older stub that triggers on
workflow_run, which does fire for Actions failures. The reusable cannot read that payload, so every such run fails withjq: error: Cannot iterate over null(run 38052385125). [Fleet Monitor] petry-projects/bmad-bgreat-suite — .github/workflows/ci-failure-analyst.yml bmad-bgreat-suite#479 moves that stub tocheck_runso it stops failing; that does not give it Actions coverage either. - CodeAnt and CodeRabbit both raised this on petry-projects/bmad-bgreat-suite PR 499.
Decision (maintainer, 2026-10-11): option 1
The analyst learns about failed GitHub Actions runs through a workflow_run trigger. The check_run trigger stays, so failed third-party checks (SonarCloud and the like) keep working. Options 2 and 3 are not being pursued.
Delivery order (maintainer, 2026-10-14)
This lands in two PRs. Every stub, including this repo's own caller, pins ci-failure-analyst/v1-stable, and the reusable on that channel (v1.0.0) fails on a workflow_run payload. The trigger can only be added after the channel carries the new reusable.
- PR feat: implement issue #2235 — CI Failure Analyst never runs for failed GitHub Actions jobs: the check_run trigger only fires for third-party checks #2308 "feat: implement issue CI Failure Analyst never runs for failed GitHub Actions jobs: the check_run trigger only fires for third-party checks #2235" — the reusable accepts either event. Criteria 1 and 3, and the docs half of criterion 4. It references this issue and does not close it.
- A second PR, after
ci-failure-analyst/v1-stablehas been promoted to a release containing PR feat: implement issue #2235 — CI Failure Analyst never runs for failed GitHub Actions jobs: the check_run trigger only fires for third-party checks #2308. Criterion 2 (theworkflow_runtrigger intemplates/ci-failure-analyst.yml, in.github/workflows/ci-failure-analyst.mdand in its lock file, with thecaller-stub-freezebaseline and the interaction contract updated to match) and the live proof in criterion 4. That PR closes this issue.
Acceptance criteria
- The reusable accepts either event.
.github/workflows/ci-failure-analyst-reusable.ymlresolves the head SHA, the non-fork PR, and the failing job, step and log from whichever payload it was called with:check_run: unchanged behaviour.workflow_run: head SHA fromgithub.event.workflow_run.head_sha; PR fromworkflow_run.pull_requests(non-fork only), falling back to the commit-to-pulls lookup the step already has; failing jobs and their logs from the run id.- A payload that carries neither (or a run with no associated non-fork PR) ends in the existing "skipping" notice with exit 0. It must never reach
jq: Cannot iterate over null. - Move the payload resolution into a script under
scripts/so it can be tested, and add bats cases for acheck_runpayload, aworkflow_runpayload, aworkflow_runon the default branch with no PR, and an empty payload.
- The stub triggers on both events.
templates/ci-failure-analyst.ymland this repo's own caller (.github/workflows/ci-failure-analyst.mdand its compiled lock file) gain aworkflow_run: types: [completed]trigger next tocheck_run:- the
workflows:list names the workflows to watch (GitHub requires names; there is no wildcard) and the header says each repo may edit that list; - the job
if:passes a failedcheck_runor a failedworkflow_run, and still excludes the analyst's own runs; - the concurrency group uses the head SHA from whichever event fired.
- the
- One analysis per commit, not one per trigger. The existing per-SHA idempotency marker (
<!-- ci-analyst sha=... -->) still prevents a second comment when several workflows fail on the same commit, and a test covers two failures on one SHA. - Docs and live proof.
docs/aw/ci-failure-analyst.mdstates which failures the analyst covers and how to edit the watched-workflow list. After the change is on a channel this repo runs, a failed Actions job on a PR in.github-privateproduces one analyst comment; link it here before closing.
Notes for the implementer
- The change is inert until a channel is cut. Say so in the PR description.
- Do not edit consumer repos' stubs from this issue. They are re-seeded through the normal standards deployment once the template has changed.
- bmad-bgreat-suite was moved to the
check_runstub by its PR 499; it picks up the new trigger with everyone else.
Related
- ci-failure-analyst can never reach stable: its ring1 members pin v1-stable, so the ring1->stable gate has no sample to count (stable frozen on v1.0.0 since 2026-07-10) .github#1270 (
ci-failure-analyst/v1-stablefrozen onv1.0.0). Consumers pinned to stable will not see this change until that promotes. - markets ingress pins ci-failure-analyst by bare commit SHA (DEFECTIVE PIN): move it to a channel once stable is no longer frozen #2176 (markets pins the reusable by bare SHA).
Activity
github-actions commentedon Oct 11, 2026
Thanks for the detailed write-up and evidence, this is a really clear analysis of why the analyst never fires for Actions failures. The three options trade off differently:
workflow_rungives coverage but needs per-stub workflow lists, anif: failure()job needs edits in every CI workflow, and option 3 narrows the docs to third-party checks. Which of the three do you want the analyst to take, and if it's option 3, is dev-lead's CI relay already enough for Actions failures in your view?don-petry commentedon Oct 11, 2026
Dev-Lead Implementation Plan
Issue: #2235: CI Failure Analyst never runs for failed GitHub Actions jobs: the check_run trigger only fires for third-party checks
Scope
Teach the reusable to resolve head SHA / non-fork PR / failing run from either a
check_runor aworkflow_runpayload, via a new tested script, and document the watched-workflow list. The caller stubs' new trigger is sequenced after a channel cut (see "Sequencing" below).Implementation Checklist
scripts/ci-failure-analyst-resolve.sh(new): reads$GITHUB_EVENT_PATH, detectscheck_runvsworkflow_runfrom the payload, resolves head SHA, the first non-fork PR (payload first, then the existingcommits/{sha}/pullsfallback), the run id (fromworkflow_run.id, or parsed fromcheck_run.details_url), and the failed jobs/steps of that run; an empty or unknown payload, or no PR, prints the existing "skipping" notice and exits 0 (neverjq: Cannot iterate over null). It also has anidempotencysubcommand for the<!-- ci-analyst sha=... -->marker check..github/workflows/ci-failure-analyst-reusable.yml: check out this script at the reusable's own commit (github.job_workflow_sha, so there's no skew and no newworkflow_callinput), use it for the PR and idempotency steps, and feed the resolved SHA/name/URL/run id/failed jobs into the prompt in place ofgithub.event.check_run.*.github/workflows/lint.yml: add the new bats file to the suite listdocs/aw/ci-failure-analyst.md: which failures are covered, theworkflow_runstub shape, and how a repo edits itsworkflows:listtests/aw/ci-failure-analyst/scenarios.md: add a workflow_run scenarioTests to Write
tests/ci_failure_analyst_resolve.bats: a check_run payload (unchanged behaviour), a workflow_run payload with a non-fork PR, a workflow_run whose fork PR is ignored and falls back to the lookup, a workflow_run on the default branch with no PR (skip, exit 0), an empty payload /{}/ missing file (skip, exit 0, no jq error), the failed-job resolution from the run id, and two failures on one SHA (the second is an idempotency skip)Sequencing (deviation from AC 2: flagged for maintainer)
Both caller stubs (
ci-failure-analyst.lock.yml,templates/ci-failure-analyst.yml) pin@ci-failure-analyst/v1-stable, which is frozen on v1.0.0 (combsmith/.github#1270). A trigger is read from the default branch as soon as it merges. If a stub gainsworkflow_runbefore the channel carries this reusable, every failed watched workflow runs the old reusable, which fails withjq: Cannot iterate over null(the bmad-bgreat-suite failure). This is the trigger version of the channel-skew rule (#1052/#1034) thatcaller-stub-freezeexists to catch. So this PR lands step 1 (a reusable that accepts both payloads and is safe with today's stubs). The stub trigger, theif:/concurrency changes and the.blockbaseline regeneration follow onceci-failure-analyst/v1-stableis promoted. The docs carry the exact stub snippet, so step 3 is mechanical.Test Command
npx bats tests/ci_failure_analyst_resolve.bats(CI:scripts/run-bats.shin lint.yml) +bash .dev-lead/scripts/dev-lead-lint.shdon-petry commentedon Oct 11, 2026
Dev-Lead: Implementation Complete — PR #2308
The implementation for issue #2235 is durable: commits are pushed and a pull request is open. This record is backed by verifiable artifacts you can confirm in one click.
2eb942a6c0a95b9941ee72b6e7959bfa40f1cd2amain: 7 file(s), +601/-51 linesReview happens on the PR. Acceptance criteria and test results are evidenced by the PR's diff and CI checks — not asserted here. If a later stage fails, this claim is retracted in place.
fix(reviews): fail when the failed-jobs lookup errors