The alarm for a backup that did not run at all

Every signal the nightly backup produced was a step inside its own job, so the one state none of them could describe was the job never starting — which is what happened on 2026-08-17.

GitHub refused to start any job on the repo over billing, the nightly backup did not run, and nothing in the repository said so. The bucket is never pruned, so a glance the next morning showed a bucket full of backups.

A detector inside Actions cannot report Actions not running. So the last thing a successful run does is write a heartbeat, and the alarm is raised by the absence of that write — by the control plane, which is not GitHub, does not scale to zero, and already mails the operators on five other conditions.

Two arms, pulling opposite ways. “The newest heartbeat is older than 26 hours” is true over an empty table, so a single staleness arm pages on its own debut, which is exactly when a new alarm gets muted. An arm that treats never-heard-from-it as quiet is silent forever if the ping is never wired up — this issue’s own failure mode rebuilt inside its fix. So the table is stamped as armed by the migration and the two arms carry different sentences and different remedies.

26 hours, and deliberately not the schedule: nine of nine nightly runs started between 03:52Z and 05:02Z against a 03:17 cron, because GitHub queues cron jobs. The heartbeat step warns rather than failing the job, because a job that fails only there has the exact shape the workflow reads as a failed restore proof, and it would print an explanation of a failure over a run whose proof passed.