The Week Three Sites Broke — What 'Unreliable Platform Defaults' Taught Me
The Week Three Sites Broke — What "Unreliable Platform Defaults" Taught Me
The Trigger: Three Crashes in Three Days
First week of June 2026. Three independent sites broke in a row.
YAMLExceptionThree bugs, three root causes. Looking back, they all shared one uncomfortable pattern — "platform default behavior is unreliable" was the culprit in every single one.
Act 1: frankbot.org — The Ghost Artifact in GitHub Actions
Symptom
June 3rd. Pushed to master, CI went green. But opening frankbot.org showed the old content.
Checked the CI log: typecheck ✅, build ✅, deploy job also ✅. Yet production wasn't updated.
Root Cause
Drilling into the deploy job with gh run view --log revealed an ENOENT on my-blog/out:
Error: ENOENT: no such file or directory, scandir
'/home/runner/work/openclaw-blog-new/openclaw-blog-next/my-blog/out'
Why was out missing? The CI workflow had typecheck→build in one job, deploy in another. The other job re-checks out a fresh worktree, so the build job's my-blog/out directory was never inherited.
I'd assumed actions/checkout@v4 would naturally preserve the working directory. It doesn't. Actions don't share worktrees across jobs. You have to bridge it explicitly with artifact upload + download.
Fix
actions/upload-artifact@v4 at the end of the build jobactions/download-artifact@v4 at the start of the deploy jobdeployments_enabled: false + production_deployments_enabled: false turned off — had to flip them manually in the GUIpermissions: contents: write, deployments: write, pages: write, id-token: write to the deploy job (otherwise createGitHubDeployment returns 403)Lesson from This Act
CI deploys don't trust worktrees; trust artifacts. To pass files across jobs, you must use upload-artifact + download-artifact as a pair. Don't assume "same repo, same files."
Act 2: blog.frank2025.com — YAML Doesn't Accept Nested Quotes
Symptom
Same period. After saving a note in frank-blog's admin editor, the CF Pages build blew up.
YAMLException: expected a single document in the stream
...
tags: [""Life"", ""Time"", ""Future""]
Tags were written as ""Life"" — double quotes inside double quotes. The YAML parser couldn't make sense of it.
Root Cause
Culprit file: src/components/admin/PostEditor.tsx:84
const tags = frontmatter.tags?.length
? `\ntags: [${frontmatter.tags.map(t => `"${t}"`).join(', ')}]`
: ''
It wraps every tag in ". But if a tag string already contains " (dirty data left over from an older save), "${t}" produces two layers of double quotes.
A classic "save once, rot a little more" bug.
Fix
const tags = frontmatter.tags?.length
? `\ntags: [${frontmatter.tags.map(t => `"${t.replace(/"/g, '')}"`).join(', ')}]`
: ''
t.replace(/"/g, '') strips first, then wraps. That way, even if the historical data is dirty, the next save self-heals it.
Lesson from This Act
"Strip, then wrap" is the golden rule of YAML/JSON serialization. You don't know what's in the input; blind wrapping creates poisoned data.
Another takeaway: a build failure isn't always caused by "the file I just edited." I fixed the Chinese locale but left en/ja untouched — the build still failed. You have to sweep every locale.
Act 3: cv.frank2025.com — Cloudflare Workers Lack "Human-ness"
Symptom
June 2nd. The Cloudflare Worker PUTs PDFs to a GitHub repo via the GitHub API, and the upload-image function returns 500.
Image upload failed: Upload failed (500)
Root Cause
Isolating the failure:
wrangler secret list → GITHUB_TOKEN exists ✅public/.write-test.txt → success ✅GitHub's error message: "Requests to this API must include a User-Agent header."
Cloudflare Pages Functions' fetch doesn't set a User-Agent header by default. GitHub's API treats an empty UA as "a rude bot" and returns 403.
Fix
await fetch(githubUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/octet-stream',
'User-Agent': 'frank-blog-uploader/1.0', // ← add this line
},
body: ...
})
One line, and every upload URL recovered.
Lesson from This Act
When calling external APIs from Cloudflare Workers / Functions, always set the User-Agent explicitly. Workers' fetch leaves the UA empty by default. CF's docs mention it in one sentence — miss it, and you'll be stuck forever.
Three Bugs, One Pattern
| Bug | Platform | Default Behavior | Consequence |
|-----|----------|------------------|-------------|
| ENOENT | GitHub Actions | worktrees aren't shared across jobs | deploy fails |
| YAMLException | YAML parser | nested quotes aren't accepted | build fails |
| 403 Forbidden | GitHub API | empty User-Agent gets rejected | upload fails |
Three bugs, three root causes. But a unified pattern emerges: the platform never guarantees your "end-to-end pipeline."
"The default is unusable" should be your design assumption. Build the bridges into the code from day one; don't bolt them on after the outage.
The 4-Step Debug SOP I Learned
Muscle memory from this week:
A 4-Item Prevention Checklist for Indie Hackers
gh run list alone isn't enough; check CF Pages' latest_deployment.created_on or curl the live page)Closing
A platform is a one-sided service partner. Send polite requests (UA, explicit artifacts, stripped data), and it always responds. Send rude ones, and you get either silent rejection — or, worse, a silent fake success (that's the worst kind, and the slowest to debug).
---
Lesson: fixing 1 site teaches you 1 thing. Fixing 3 sites teaches you 1×, and helps you spot patterns across them.
💬 Feedback & Discussion
I read every piece of feedback carefully.
Questions about an article, spotted an error, or just want to chat about tech and life — reach out on Telegram .