Skip to content

ci-failure-analyst caller: the self-exclusion guard never matches a real check name, and no-PR failures still start the analyst #2175

Open

Description

@don-petry

Problem

The CI Failure Analyst caller's self-exclusion guard never matches, and failed check runs that belong to no pull request still start the analyst. Both are pre-existing (the pre-collapse stub had them), found while reviewing the markets ADR-0007 collapse (combsmith/markets#513). The collapse deliberately preserved them to stay behavior-equivalent, and the cascade review flagged the guard as a minor finding.

Evidence

  1. Dead loop guard. The caller's if: is github.event.check_run.conclusion == 'failure' && !startsWith(github.event.check_run.name, 'CI Failure Analyst'). A reusable-workflow check run is named <caller job> / <reusable job>; the real name is analyze in the pre-collapse markets stub (job analyze, reusable job analyze) and ci-failure-analyst / analyze after the collapse. Neither starts with CI Failure Analyst (that is a workflow display name, not a check name). Observed on markets: the analyst's check runs are named analyze. So a failed analyst run can start another analyst run on the same head SHA. It is bounded by the job's per-SHA non-cancelling concurrency group and the reusable's non-fork filter, but it is not prevented. The new markets bats test pins the dead string.
  2. No-PR failures. The job fires on every failure check run, including CI runs from pushes to main that have no pull request, so the reusable starts with no PR to comment on. Both the stub and (now) the markets ingress behave this way.

Required

  1. Replace the guard with one that matches the analyst's real check names (both analyze and <role> / analyze, case-sensitive), and prove it with a test fed realistic check-run payloads, including a failing analyst run (must be excluded) and a failing ordinary CI run (must still start the analyst).
  2. Decide, and document, the no-PR behavior: either require an associated PR in the if: (payload-only, so it stays a pure event filter) or accept parity and say why. If the if: changes, it must still pass validate-ingress-if.sh.
  3. The fix lands in the standard caller template (petry-projects/.github standards/workflows/ci-failure-analyst.yml) and in every collapsed ingress (markets agent-ingress.yml job ci-failure-analyst, and the markets collapse package §3). Keep the ingress job and the ratified package in step (see Sync the markets collapse package §3/§4b to the deployed ingress (pr-auto-review group + ci-failure-analyst pin) #2171).

Out of scope

The pin of the analyst's reusable (separate issue).

Activity

  1. github-actions commented on Oct 10, 2026

    @github-actions
    Contributor

    Thanks for the thorough write-up and evidence on this guard and the no-PR behavior. The details on the real check names (analyze and <role> / analyze) make the problem clear. One question to settle before a fix: for failed check runs with no associated pull request, such as CI runs from pushes to main, do you want the analyst to be skipped by requiring an associated PR in the if:, or kept for parity with the current behavior?

  2. don-petry commented on Oct 10, 2026

    @don-petry
    CollaboratorAuthor

    @petry-projects/solution-architect ruling requested on the no-PR question here.

    The caller if: starts ci-failure-analyst for any failing check_run, including ones with no PR. The reusable already skips those at runtime (ci-failure-analyst-reusable.yml lines 32-57). It falls back to commits/{sha}/pulls when check_run.pull_requests is empty, which catches the race where the payload omits a PR that exists.

    Options:

    • (a) Add github.event.check_run.pull_requests[0] to the caller if:. This avoids a runner for every no-PR failure, but loses the race-fallback case.
    • (b) Keep caller parity and rely on the runtime skip. That costs one short runner per no-PR failure.

    Which one? Separately, the loop guard (startsWith(name, 'CI Failure Analyst')) never matches the real check name, ci-failure-analyst / analyze, and will be corrected either way. The fix spans templates/ci-failure-analyst.yml, .github/workflows/ci-failure-analyst.lock.yml, docs/aw/ci-failure-analyst.md and the markets package, with the canonical template in petry-projects/.github.


    Generated by Claude Code

  3. don-petry commented on Oct 10, 2026

    @don-petry
    CollaboratorAuthor

    Solution Architect: structural advisory

    Governing ADR: ADR-0007 (One agent-ingress stub per repo), which extends ADR-0001 (Thin-caller / reusable two-tier). ADR-0010 sets the matching bounds for concurrency.group.

    Alignment: aligned if you pick (b). Option (a) is a legal event filter, but it gives up correctness that only the reusable can provide.

    What I'd shore up (highest leverage first):

    • Choose (b): keep caller parity and rely on the runtime skip. The commits/{sha}/pulls fallback reads repo state. ADR-0007 says an if: that "reaches for repo state … belongs in the reusable", so (a) can't keep that fallback. It would drop it. ADR-0007's Consequences already accepted this exact cost: "a small per-event runner cost" in exchange for a filter that needs state. The ADRs have already settled this trade-off. Don't reopen it to save a short runner on pushes to main.
    • Loop guard: use exact equality, not startsWith. Match check_run.name == 'analyze' || check_run.name == 'ci-failure-analyst / analyze'. That is a pure payload predicate, so it passes validate-ingress-if.sh. The trade-off: a consumer whose own CI job is literally named analyze gets excluded. Say so in the template header.
    • Write the no-PR decision down where the next reader will look: docs/aw/ci-failure-analyst.md and the template header, citing ADR-0007. A new ADR isn't needed because nothing new is being decided.
    • Order of landing: the canonical template in petry-projects/.github first, then the .lock.yml, the markets ingress job and the §3 package together (per Sync the markets collapse package §3/§4b to the deployed ingress (pr-auto-review group + ci-failure-analyst pin) #2171). Update the caller_stub_freeze baseline in the same change, or the freeze check will flag it.

    Escalate? No.

    Advisory only. I comment; I don't change code. Ground: BMAD Method architect (bmad-agent-architect v6.8.0). To opt out on this item, add the solution-architect:hands-off label.

  4. don-petry commented on Oct 12, 2026

    @don-petry
    CollaboratorAuthor

    Status update after combsmith/markets#523 (merged as 9310b6e).

    Required 1 (guard): done for the collapsed ingress. The markets ci-failure-analyst job now has !startsWith(github.event.check_run.name, 'ci-failure-analyst'). That excludes the analyst's real post-collapse check name, ci-failure-analyst / analyze. A markets bats test reads the prefix out of the if: and asserts that ci-failure-analyst / analyze matches it. The canonical template, combsmith/.github#1316 (head 4cdd886), carries the same block byte-for-byte.

    This is a role prefix, not the exact-equality match the architect advised. Inside an ingress the two behave the same, because the only check runs whose names start with ci-failure-analyst are the analyst's own. Neither form covers the legacy bare analyze name, which only a non-collapsed per-role stub produces.

    Required 2 (no-PR): ruled (b), parity. The caller if: stays a pure event filter, and the reusable's commits/{sha}/pulls runtime skip handles failures with no pull request. The ingress if: already matches this ruling, so it needed no change.

    Still open on this issue:


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugBug reports

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions