My 'Updated Every 6 Hours' Site Served 45-Day-Old Data. GitHub Never Failed Once.
My site's header said "auto-collected · updated daily · zero API dependencies · fully automated on GitHub." It was serving data from 2026-09-25 17:02 UTC. Today is 2026-11-09. That's 44.7 days of a page that renders pe
My site's header said "auto-collected · updated daily · zero API dependencies · fully automated on GitHub."
It was serving data from 2026-09-25 17:02 UTC.
Today is 2026-11-09. That's 44.7 days of a page that renders perfectly, returns HTTP 200, and lies to every visitor — including me, because I was the one who built it and I never noticed.
Here is the part that should bother you more than the staleness: GitHub never reported a single failure. Not one red X. The 234-run history is 100% green.
The failure was an absence, not an error
The project is github-daily-trending: a workflow that scrapes GitHub Trending every 6 hours, regenerates a static page, and deploys it to Pages. The workflow file is unremarkable:
on:
schedule:
- cron: '0 */6 * * *'
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read # <-- read-only, this matters later
pages: write
id-token: write
I checked the repository API and got a number I did not expect:
GET /repos/Felixwang007/github-daily-trending/actions/workflows
Daily Trending Deploy | state = disabled_inactivity
pages-build-deployment | state = active
state: disabled_inactivity. Not disabled_manually. Not a failed run. The scheduler had switched my workflow off and told exactly one API field about it.
The timeline, to the hour
| Event | UTC timestamp |
|---|---|
Only human commit (init) |
2026-07-27 13:28:34 |
| 60-day inactivity mark | 2026-09-25 13:28:34 |
| Last successful run | 2026-09-25 17:01:50 (+3.6 h) |
| 4 commits pushed | 2026-10-29 12:56 → 0 runs |
| Page served to visitors | stale, 44.7 days |
234 workflow runs at 4/day across 60.1 days lines up almost exactly (≈241 expected, minus the slots that never fired after the cutoff). Since the disable, roughly 179 scheduled invocations were skipped.
GitHub disables schedule triggers in public repositories after 60 days without repository activity. My workflow had contents: read, so it deployed an artifact and wrote nothing back to the repository. Its own activity therefore never counted as activity. The automation was structurally incapable of keeping its own clock alive:
A pipeline that only reads from the repo can never prove the repo is alive.
That is not a GitHub bug. That is a design defect on my side, and it took 45 days of silently wrong data to surface it.
The detail that proves it isn't my code
On 2026-10-29 I pushed four commits to main while adding an unrelated probe script. The workflow has push in its on: block, so those commits should have produced four runs.
They produced zero. Disabling by inactivity doesn't just kill the schedule — it detaches every trigger from the workflow. So the natural first hypothesis, "my scraper broke," was wrong, and I would have wasted hours reading Python tracebacks that never existed.
Why nothing alerted me
Every check I had — and, I suspect, every check most people have — is defined over the set of runs:
- "Did the last run fail?" → there is no last run, so nothing to fail
- "Is the success rate dropping?" → 234/234, perfect score
- "Did the deploy step error?" → the deploy step didn't execute
When the set of executions is empty, every predicate over that set is vacuously true. The green history was a record of past invocations, and I had been reading it as evidence that the trigger still existed. Those are different claims, and only one of them was still being tested.
The signal was there the whole time
You don't need to scrape the UI banner. The state is a first-class API field. Here is the check I now run, ~90 lines, no dependencies beyond stdlib — it reads each workflow's state, parses the cron cadence from the workflow file, and compares the last run's age against that cadence:
wfs = _get(f"{API}/repos/{repo}/actions/workflows", token)
for wf in wfs["workflows"]:
crons = _workflow_crons(repo, wf["path"], token) # from the YAML body
interval = min(f for f in map(_parse_cron_interval_minutes, crons) if f)
runs = _get(f"{API}/repos/{repo}/actions/workflows/{wf['id']}/runs?per_page=1", token)
age_h = hours_since(runs["workflow_runs"][0]["created_at"])
if wf["state"] == "disabled_inactivity":
verdict = "DISABLED_INACTIVITY" # platform stopped calling it
elif wf["state"] == "disabled_manually":
verdict = "DISABLED_MANUALLY"
elif age_h > interval / 60 * 2:
verdict = "STALE" # active, but not firing on time
else:
verdict = "OK"
Real output from the broken repo:
{"repo": "Felixwang007/github-daily-trending", "workflow": "Daily Trending Deploy",
"state": "disabled_inactivity", "cron": ["0 */6 * * *"], "interval_min": 360,
"last_run": "2026-09-25T17:01:50Z", "age_hours": 1073.6,
"verdict": "DISABLED_INACTIVITY"}
And a positive control (home-assistant/core) so the tool isn't just always shouting:
{"workflow": "Build images", "state": "active", "cron": ["0 2 * * *"],
"interval_min": 1440, "age_hours": 8.4, "verdict": "OK"}
Two design notes, because both matter for false-alarm hygiene:
- The cron parser only interprets
*,*/N, and hourly shapes. Expressions it cannot parse (weekly, or with trailing inline comments) degrade to "cannot judge staleness" rather than "alarm". An unreadable schedule is not evidence of a broken one. - Non-scheduled workflows (like the Pages build) have no cadence, so they are never reported as stale.
Exit code is 0 when every schedule is alive, 2 when anything isn't, 3 when it had no repo to check. "I had nothing to look at" is a distinct outcome from "everything is fine" — the same lesson as the 45 missing days, applied one level up.
The fixes, ranked by how much I trust them
1. Make the trigger external. The 60-day clock is computed by GitHub, and GitHub is also the system that would need to tell me the clock expired. Judgement can't live inside the thing being judged. The durable fix is a trigger from outside: an external cron (a VPS, another platform, even a laptop at job) calls POST /repos/{o}/{r}/actions/workflows/{file}/dispatches once a week. Then the schedule's own health stops mattering.
2. Alarm on the artifact's age, not on CI colour. The deployed page writes its own updated_at. Comparing that against wall-clock time is a measurement I control and it would have fired on day 1. CI status was a measurement that disappeared along with the thing it described.
3. Keepalive commits — treat as unproven. Writing a timestamp file back into the repo is the popular trick. I'm not relying on it: the same "is this repo active?" heuristic that decided I was dormant is the one that would have to accept a bot commit as activity, and I can't verify that from the outside.
What I did today, measured: re-enabled the workflow (PUT .../actions/workflows/321419158/enable, HTTP 204 → state: active), dispatched it by hand, run completed successfully in about 2 minutes, and the page timestamp moved:
before: 2026-09-25 17:02 UTC
after : 2026-11-09 10:43 UTC
Re-enabling is not the fix. The clock restarted; in 60 days it will stop again unless the trigger comes from outside. I'm writing that down here mostly so future me can't pretend he didn't know.
The shape of this bug is everywhere
This isn't a GitHub quirk. It's what happens whenever a pipeline's health is inferred from its own executions:
- A market data feed that stops publishing doesn't throw — the last bar just sits there and your indicators keep computing on stale prices. This exact failure mode is why every A-share script I run now asserts the newest date present in the data, instead of asserting that the request returned 200.
- A cron job whose
last_statusisokcan be a job that produced zero rows for three weeks. - A heartbeat written by the same process it monitors proves the process was alive at write time, not that it's alive now.
The general rule I've settled on: every "is it working?" check needs a claim about what should exist now, verified from a vantage point the failing component doesn't control. "No errors" is not that claim. It's the absence of evidence, dressed as evidence.
If you run any scheduled automation, go paste this into a shell right now — it takes one call and it's free:
curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/OWNER/REPO/actions/workflows" \
| jq -r '.workflows[] | "\(.state)\t\(.name)"'
Anything that isn't active has been quietly not running for some number of days you don't know yet.
The aggregator (now actually updating) is at Felixwang007/github-daily-trending, and the reliability tooling from earlier posts lives in awesome-content-tools. The audit scripts I use for CI gates, log checks, and cron hygiene are published on 虾评 (Xiaping) as standalone skill packages — search "cron-reliability-doctor" if you want the packaged version of the checks above.
If you've had a monitor go green while the thing it watched was dead, I'd like to hear which signal you ended up trusting. That's the part I still don't have a general answer for.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.