三个独立网站同周崩溃 ——「平台默认行为不可信」教给我的事
三个独立网站同周崩溃 ——「平台默认行为不可信」教给我的事
起因:三连崩溃的一周
2026 年 6 月第一周。3 天内 3 个独立站点连续崩溃。
YAMLException3 个 bug,3 个根因。回过头看,3 个里只有 1 个共性——所有问题都出在「平台默认行为不可信」。
第一幕:frankbot.org — GitHub Actions 的「幽灵 artifact」
症状
6 月 3 日。push 到 master,CI 跑完是绿色。但打开 frankbot.org 看到的还是旧内容。
检查 CI 日志:typecheck ✅、build ✅、deploy job 也 ✅。但生产就是没更新。
根因
gh run view --log 挖 deploy job,看到 my-blog/out 的 ENOENT:
Error: ENOENT: no such file or directory, scandir
'/home/runner/work/openclaw-blog-new/openclaw-blog-next/my-blog/out'
为什么 out 不见了?CI workflow 里 typecheck→build 跑在同一个 job,deploy 是另一个 job。另一个 job 重新 checkout worktree,build job 写到 my-blog/out 的文件完全没传过来。
我以前以为 actions/checkout@v4 默认会把工作目录整个保留。不是。Actions 跨 job 不共享 worktree。必须靠 artifact upload + download 显式桥接。
修复
actions/upload-artifact@v4actions/download-artifact@v4deployments_enabled: false + production_deployments_enabled: false 是关的,得去 GUI 手动开permissions: contents: write, deployments: write, pages: write, id-token: write(否则 createGitHubDeployment 报 403)这一幕学到的
CI 部署不要信 worktree,要信 artifact。跨 job 传文件必须 upload + download 配对用,不能用「反正都是同一个 repo」来推断。
第二幕:blog.frank2025.com — YAML 不接受嵌套引号
症状
同期。frank-blog 的 admin editor 保存笔记后,CF Pages build 炸了。
YAMLException: expected a single document in the stream
...
tags: [""Life"", ""Time"", ""Future""]
tag 被写成 ""Life""——双引号套双引号。YAML 解析器直接看不懂。
根因
罪魁:src/components/admin/PostEditor.tsx:84
const tags = frontmatter.tags?.length
? `\ntags: [${frontmatter.tags.map(t => `"${t}"`).join(', ')}]`
: ''
它会给每个 tag 包一层 "。但如果 tag 字符串里已经含有 "(早期 save 留下的脏数据),"${t}" 就会生成两层双引号。
经典的「保存一次腐一次」型 bug。
修复
const tags = frontmatter.tags?.length
? `\ntags: [${frontmatter.tags.map(t => `"${t.replace(/"/g, '')}"`).join(', ')}]`
: ''
t.replace(/"/g, '') 先 strip,再 wrap。这样即使历史数据脏,下次 save 也会自动清洗。
这一幕学到的
YAML/JSON 序列化「先 strip 再 wrap」是铁律。你不知道输入里有什么,盲目 wrap 就会生成毒数据。
另一个教训:build 失败不一定只有「我刚改的文件」是凶手。我修了 zh 语言的,但 en/ja 没动——build 照样挂。所有 locale 都要扫一遍。
第三幕:cv.frank2025.com — Cloudflare Worker 缺「人味儿」
症状
6 月 2 日。Cloudflare Worker 走 GitHub API 把 PDF PUT 进 repo,upload-image 函数返回 500。
Image upload failed: Upload failed (500)
根因
拆分:
wrangler secret list → GITHUB_TOKEN 在 ✅public/.write-test.txt 写个空文件试试 → 成功 ✅GitHub 报错原文:「Requests to this API must include a User-Agent header」。
Cloudflare Pages Functions 的 fetch 默认不带 User-Agent 头。GitHub API 把空 UA 当作「不懂礼貌的 bot」,直接 403。
修复
await fetch(githubUrl, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/octet-stream',
'User-Agent': 'frank-blog-uploader/1.0', // ← 加这一行
},
body: ...
})
1 行修复,所有 URL 的 upload 全部恢复。
这一幕学到的
Cloudflare Worker / Function 调外部 API,永远显式设 User-Agent。Worker 的 fetch UA 默认是空,CF 文档里就那么一句话,不知道就永远卡。
3 个 bug,1 个共性
| Bug | 平台 | 默认行为 | 后果 |
|-----|------|----------|------|
| ENOENT | GitHub Actions | job 间不共享 worktree | 部署失败 |
| YAMLException | YAML parser | 不接受嵌套引号 | build 失败 |
| 403 Forbidden | GitHub API | 拒绝空 User-Agent | upload 失败 |
3 个 bug,3 个根因。但有个统一规律:平台从不替你的「完整流程」打包票。
「默认是不可用的」做设计假设。一开始就把 bridge 写进代码,不要等挂了再补。
我悟出的 4 步调试 SOP
这一周养成的肌肉记忆:
给 indie hacker 的 4 条预防清单
gh run list 不够,要看 CF Pages 的 latest_deployment.created_on 或 curl 线上页面验证)结尾
平台是单方面服务你的合作伙伴。你发礼貌的请求(UA、显式 artifact、strip 过的数据),它就一定响应;你发粗鲁的请求,要么被静默拒绝,要么更糟——静默地假装成功(这种最坑人,排查起来最耗时)。
---
教训:修 1 个站,长 1 分经验;修 3 个站,长 1 倍经验,且能发现跨站点的共性。
💬 交流与反馈
我认真阅读每一条反馈。
如果你对文章有疑问、发现错误、或者想交流技术与生活话题,欢迎通过 Telegram 联系我。