3 つの独立サイトが同週に崩壊した日 — 『プラットフォームのデフォルトは信用ならない』
3 つの独立サイトが同週に崩壊した日 — 『プラットフォームのデフォルトは信用ならない』
発端:3 連クラッシュの 1 週間
2026 年 6 月 1 週目。3 日間で 3 つの独立サイトが連続して壊れた。
YAMLException3 つのバグ、3 つの根本原因。振り返ると、3 つに 1 つの共通項があった — すべて「プラットフォームのデフォルト動作が信頼できない」が犯人だった。
第 1 幕:frankbot.org — GitHub Actions の「幽霊 artifact」
症状
6 月 3 日。master に push して CI が green になる。なのに 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 までが 1 job、deploy が別 job。別 job は新しい worktree を checkout するので、build job が my-blog/out に書いたファイルは引き継がれない。
actions/checkout@v4 のデフォルト動作を当然と思っていた。でも Actions は job 間で worktree を共有しない。artifact upload + download で明示的に橋渡しする必要がある。
修正
actions/upload-artifact@v4 を追加actions/download-artifact@v4 で取得deployments_enabled: false + production_deployments_enabled: false が GUI から手動でオンにする必要があったpermissions: contents: write, deployments: write, pages: write, id-token: write を deploy job に追加(createGitHubDeployment の 403 対策)この幕から学んだこと
CI デプロイは worktree を信用するな、artifact を契約にしろ。 別 job 間でファイルを受け渡すには upload-artifact + download-artifact を必ずペアで使う。パスが同じだからといって、ファイルがそのままあるとは限らない。
第 2 幕:blog.frank2025.com — YAML はネスト引用符を許さない
症状
同じ頃。frank-blog の admin editor でノートを保存すると、CF Pages の build がこける。
YAMLException: expected a single document in the stream
...
tags: [""Life"", ""Time"", ""Future""]
タグが ""Life"" という形で二重引用符で囲まれている。YAML パーサは引用符の中の引用符を理解できない。
根因
該当:src/components/admin/PostEditor.tsx:84
const tags = frontmatter.tags?.length
? `\ntags: [${frontmatter.tags.map(t => `"${t}"`).join(', ')}]`
: ''
既に各タグを " で囲っている。でも tags 配列に元から " を含んだ値が入っていると(古い 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 すると毒入りデータができる。
もう 1 つ:ビルドエラーは「いま直したファイル」だけが犯人とは限らない。 1 ロケール(zh)だけ直して en/ja を放置していたら、build は相変わらず失敗する。すべてのロケールを横断チェックする必要がある。
第 3 幕:cv.frank2025.com — Cloudflare Worker に「人っぽさ」がない
症状
6 月 2 日。Cloudflare Worker 経由で PDF を GitHub リポジトリに PUT する upload-image 関数が 500 を返す。
Image upload failed: Upload failed (500)
根因
切り分け:
wrangler secret list → GITHUB_TOKEN 存在する ✅public/.write-test.txt を PUT → 成功 ✅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 行で全 upload URL が復旧した。
この幕から学んだこと
Cloudflare Worker/Function から外部 API を叩くときは、必ず User-Agent を明示的に設定する。 Worker の fetch は UA を空にしてしまう。これは CF のドキュメントにも小さく書いてあるが、知らないと永遠にハマる。
3 つの bug、1 つの共通項
| バグ | プラットフォーム | デフォルト動作 | 結果 |
|------|-----------------|---------------|------|
| ENOENT | GitHub Actions | job 間で worktree を共有しない | デプロイ失敗 |
| YAMLException | YAML parser | ネスト引用符を許さない | build 失敗 |
| 403 Forbidden | GitHub API | 空 User-Agent を拒否 | upload 失敗 |
3 つのバグ、3 つの根因。でも、プラットフォームは「あなたの全パイプラインを面倒見ます」とは言っていない。
「デフォルトは不便なものだ」と決め打って設計する。 そうすれば最初から bridging を組み込める。
私の 4 ステップ・デバッグ SOP
この 1 週間で身についた習慣:
Indie hacker への 4 つの予防チェックリスト
gh run list だけじゃなく、CF Pages の latest_deployment.created_on か curl で実ページを確認する)締めの 1 文
プラットフォームは契約の片務的パートナー。 礼儀正しいリクエスト(UA、明示的 artifact、strip 済みデータ)を送れば、確実に応えてくれる。 礼儀を欠いたリクエストを送れば、サイレントに拒まれる — もしくは、もっと悪い、サイレントに成功したフリをされる(これがいちばんたちが悪い)。
---
教訓:1 つのサイトを直すより、3 サイトを直したあとのほうが強くなれる。
💬 交流とフィードバック
すべてのフィードバックを真剣に読んでいます。
記事について質問がある、誤りを見つけた、技術や生活について交流したい場合は、お気軽に Telegram でご連絡ください。