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.
The CI Health Check cron reports a transient failure of its own
ghcommand as a CI incident. Fired 2026-08-14 02:30Z with: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:When the command fails, its error text becomes
output, which is non-empty, sooutput != ''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
exitCodeto 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
One line. The alert then fires only when the query genuinely ran and genuinely found failing workflows.
Worth doing in the same pass:
.af-cron/for the same shape (team-update.yamluses the same runner and deserves a look).exitCode !== 0and the condition does not mentionexitCode, 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 -ireporting "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.