Skip to content

af-cron: CI Health Check reports its own command failure as 'CI failing on main' (condition ignores exitCode) #1456

Description

@amrmelsayed

The CI Health Check cron reports a transient failure of its own gh command as a CI incident. Fired 2026-08-14 02:30Z with:

CI Alert: currently failing on main: Command failed: gh run list --branch main --limit 10 --json workflowName,conclusion --jq '...'

CI on main was green at the time, and the identical command succeeded on re-run seconds later, returning empty. The alert was purely a transient API/network failure wearing an incident's clothes.

Root cause: our own condition, not the framework

.af-cron/ci-health.yaml:

command: gh run list --branch main --limit 10 --json workflowName,conclusion --jq '... | join(", ")'
condition: "output != ''"
message: "CI Alert: currently failing on main: ${output}..."

When the command fails, its error text becomes output, which is non-empty, so output != '' is true and the alert fires with the error interpolated into a sentence asserting CI is broken. A command failure and a genuine CI failure are indistinguishable to this condition.

The framework already provides the discriminator. #1142 added exitCode to the condition sandbox (tower-cron.ts:259, :312-315), and the code comment states the intent plainly: "With a condition, the condition alone decides (a non-zero exit is data the condition can inspect via exitCode, not noise)." This task simply never inspects it.

Fix

condition: "exitCode === 0 && output != ''"

One line. The alert then fires only when the query genuinely ran and genuinely found failing workflows.

Worth doing in the same pass:

  • Audit the other tasks in .af-cron/ for the same shape (team-update.yaml uses the same runner and deserves a look).
  • Consider a runner-side guard so this cannot recur per-task: when exitCode !== 0 and the condition does not mention exitCode, either suppress delivery or prefix the message with a clear "the check itself failed" marker. The current default is arguably the wrong way round — a task author who writes a naive condition gets false incidents rather than silence.

Why this is worth more than its size

A false "CI is failing on main" is not a harmless nuisance: it asserts a specific broken state, and the correct response to a real one is to stop merging. This is the third instance in twenty-four hours of a tool returning a confidently wrong answer that invited a wrong action — alongside afx cleanup -i reporting "builder not found" for builders that exist (#1445), which led to two worktrees being deleted by hand. The pattern is worth naming: our tooling's failure modes tend to present as findings rather than as failures.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/towerArea: Tower server / agent farm CLI

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions