Builds
GitHub Actions: 78 Red Builds, 3 Root Causes, One Commit
78 failed GitHub Actions runs reduced to 3 root causes: a bare GITHUB_ENV write, deploy races, one apostrophe. A CI debugging story with transferable lessons.

Last updated: September 9, 2026 · 6-minute read
The GitHub Actions history for this site's repo had 78 failed runs in it — out of 487 total, spread across three workflows, several of them failing daily for roughly two straight weeks. I pulled all 487 runs and read the job logs, and the mystery did not survive the first screenful: all 78 failures traced back to three root causes, and every fix fit in a single commit.
Some context, because the stakes matter for the lesson. This site runs itself — scheduled workflows publish posts, syndicate them to developer platforms, and run SEO regression checks against the live domain. The three red workflows were exactly those: Syndicate & Share, SEO Regression, and the Daily Content Engine. None of the failures were flaky infrastructure or cloud ghosts; each was deterministic, written down in a log, and repeating on schedule.
Three failure families, one tab
The table names the shape of the problem. What follows is each family in order, because the same debugging arc — symptom, log, boring fix — repeats three times.
The $GITHUBENV trap
Syndicate & Share had failed every day since August 27 — eleven straight runs. Its first step built a list of posts eligible for cross-posting and wrote it to the job environment like this:
$ELIGIBLE was a multi-line list — 57 slugs had piled up in the syndication backlog, spanning posts from January through September 2026. $GITHUBENV expects one KEY=value pair per line, so the parser hit the second slug and died with Invalid format, taking the whole run down before anything else could execute. Two weeks of daily red runs, from one echo.
The fix is the documented heredoc pattern — verbose, explicit, and impossible for a multi-line value to break:
Cost of the bug: about two weeks of missed syndication runs, with 57 posts piled up waiting for Dev.to and Hashnode. Cost of the fix: three lines and a delimiter. The next scheduled run — not any clever engineering — was all the backlog needed.
A regression check with two bad habits
SEO Regression, the workflow that fetches the live site and fails on real problems, had been red since August 26. Its chronic failure was almost embarrassing: the IndexNow key file was absent from the repo, so the live check hit a 404 every single day until the key landed. That one is already resolved — the key file is live, returns HTTP 200, and the secret was updated on September 6.
The subtler failure mode was timing: push-triggered runs raced Netlify's deploy propagation, checking URLs before the deploy had finished rolling out — transient 404s that failed the run and muddied the signal. A third gate, a 76-character post title tripping the 70-character limit, had already been fixed in an earlier commit. The fix for the races is a retry helper on all three live fetches: three attempts, thirty seconds apart, with the final result still strict. A transient blip now costs thirty seconds of waiting instead of a red X, and real regressions still fail the build.
The zero-job failure
Daily Content Engine failed differently: its runs died at validation with zero jobs executed. The cause was invalid YAML in the workflow file itself — a single apostrophe inside a string, which the parser refuses to forgive. That had already been fixed in an earlier commit, and all seven workflow YAMLs now parse cleanly.
Checking that workflow surfaced a latent bug worth its own headline. content/ideas/ — the queue the engine stages posts from — was empty, and the staging script would have exited with an error the next time the cron fired. An empty content queue would have failed the next 16:30 UTC run. The fix now lives in the workflow: the staging, OG, and quality steps skip gracefully when the queue is empty, guarded by a skipqueue env flag, instead of treating "nothing to do" as "something is broken".
Proof the fixes held
I verified every fix the boring way — local syntax checks first, then a manual dispatch of all three workflows using the dry-run and skip flags those workflows accept. Syndicate & Share passed all ten steps with the real 57-slug list flowing through its filter step. SEO Regression came back green. The Daily Content Engine posted its first successful run in its entire existence, with the LLM step deliberately skipped for the verification.
The record since: of the last 12 runs across those workflows, 11 are green and the one failure is pre-fix history. Nothing was weakened to get there — the syndication filters, the SEO gates, and the content pipeline all still fail loudly when something is actually wrong.
Three lessons that transfer
Read the logs, not the red X. Every one of these families looked, from the outside, like flaky CI. The job logs said otherwise within the first screenful — a parser error with the offending slug printed in it, a 404 with the missing path, a validation failure with zero jobs. The distance between "CI is being weird again" and the actual cause was one log download.
The boring fix beats the clever one. A heredoc delimiter instead of a clever one-liner, a retry loop instead of a smarter deployment detector, a graceful skip instead of fake work for an empty queue. None of these fixes are impressive, and all of them eliminated their failure family in one commit.
Empty states are failure modes too. An empty content queue is not "nothing to do" — to a script written for the happy path, it is a crash that has not been scheduled yet. Every cron-driven script needs an opinion about what happens when its input list has zero items, and "exit 1" is rarely the right opinion.
TL;DR
- 78 failed runs across 3 workflows reduced to 3 root causes: a bare multi-line $GITHUBENV write, a live check racing deploys plus a missing key file, and invalid YAML.
- The $GITHUBENV trap: multi-line values need a heredoc delimiter — one bare echo killed the daily syndication run 11 times in a row.
- 57 posts piled up in the syndication backlog while the workflow was broken; the fix was 3 lines long.
- The Daily Content Engine now skips gracefully when the content queue is empty — empty states are failure modes too.
Keep reading
- The automated pipeline that publishes this site
- Free AI code review with GitHub Actions
- Automation & CI/CD — the solo-dev workflow hub
---
Not affiliated with GitHub, Microsoft, or Netlify. Tools used: GitHub Actions, the site's own syndication and SEO scripts, and one well-placed heredoc.