静态站自动部署链路:一次「站点静默 3 天」的排查
博客从 10-17 到 10-20,整整三天没更新过。
新文章写了三篇,线上还是老样子;首页的 Last-Modified 死死停在 Sat, 17 Oct 2026 10:10:03 GMT。有意思的是所有人都没发现——包括我自己。
这事的价值不在「修好了」,而在「为什么所有人都以为它是好的」。这篇把完整证据链记下来。
一、先把现象钉死
静态站的部署故障有个讨厌的地方:它不报错。页面白屏会有人喊,部署失败会有一封告警邮件,但「部署压根没触发」是完全静默的。
所以第一步不是猜,是把证据固定下来:
1 | # 1. 线上首页最后修改时间 —— 卡在 10-17,说明从那之后没有任何部署 |
两个信号一起,才能确定是「部署没发生」,而不是「部署了但内容不对」。
然后去 CNB 查构建历史:
1 | cnb build get-build-logs --repo zzWei/zzWeiBlog |
结果非常干净——从 10-17 之后一条 push 构建记录都没有。
到这一步其实已经能推出结论了:不是「构建失败」,是「压根没触发构建」。构建失败会有红叉,没有记录意味着事件都没被识别。
二、根因一:0 字节的 .cnb.yml
顺着 git 历史往回翻,10-20 有一个提交「不再同步到gitee」,动了 .cnb.yml:
1 | git show 8ab983e -- .cnb.yml |
文件被清成了 0 字节。
这就是第一层根因。CNB 的流水线入口是 .cnb.yml,这个文件为空时,CNB 解析不出任何事件,push 时连一条构建记录都不会产生。
有点反直觉:明明什么都没干,为什么不能什么都不写?
因为在 CI 系统里,「空配置」不是「不执行」,而是「没有可执行的目标」。事件分发阶段直接就短路了。
而 EdgeOne 的自动部署是挂在 CNB 的 push 构建事件上的。流水线一空,整条链就断在第一环。
修复就是恢复一个最小流水线。但这里又埋了第二个坑。
三、根因二:main: 不是「所有分支」
我第一次恢复时写的是:
1 | main: |
结果 push 之后依然一条构建记录都没有。
原因藏在 CNB 的事件匹配规则里:
| 写法 | 含义 | 本仓库结果 |
|---|---|---|
main: | 只匹配分支名恰好是 main 的推送 | ❌ 本仓库默认分支是 master |
$: | 对所有分支生效 | ✅ 任意分支 push 都触发 |
这里的 $ 是一个「通配」顶层键,和正则里的 .* 是一类语义。用 main: 写在本仓库上,效果等同于写了个永不执行的过滤器。
改完 $: 之后,push 构建立刻就有了。但那时线上还是 404。
四、根因三:Webhook 送达 ≠ 触发部署
CNB 恢复后,还有第三层。EdgeOne Makers 官方文档里写得很清楚:
CNB does not natively support Webhooks. After importing a CNB repository, you must configure the CNB Webhook plugin in the repository’s root directory
.cnb.ymlfile to notify Makers of code push events.
也就是说,光有 push 事件不够,还得主动通知 Makers。官方给的 Webhook 地址是固定值:
1 | https://api.edgeone.ai/eo/pages/hook/cnb |
在 .cnb.yml 里加一个 stage:
1 | $: |
推上去之后,CNB 构建 success,webhook stage 的日志也漂亮:
1 | [DEBUG] Sending POST request to https://api.edgeone.ai/eo/pages/hook/cnb |
HTTP 200,code: 0,message: ok。 全绿。
但线上还是 404。轮询了 9 分半钟,Last-Modified 一动不动。
问题就出在这个 200 上。Webhook 只是告诉 Makers「有新提交,快来部署」,但 Makers 收到之后还要做一件事:按项目配置的「部署分支」做匹配。而那个项目的部署分支当时配的是 main,我们推的是 master。
匹配不上 → 静默丢弃 → 不建部署 → 不报错。Webhook 依然返回 200,因为它确实成功送达了。
这是整条链路上最隐蔽的一环:上游的成功不代表下游会执行。
验证方法也很直接——推一个 main 分支过去对照:
1 | git push origin master:refs/heads/main |
90 秒后,站点更新了,Last-Modified 跳到新时间。根因确认。
五、为什么三天都没人发现
现在回头看,这套故障能静默三天的原因很清晰:
- 所有环节都显示成功 —— CNB 构建绿灯,webhook 200,日志里没有任何 ERROR
- 静态站不报错 —— 旧页面照常渲染,只是内容停在三天前
- 没有内容变更告警 —— 没有人在意「今天有没有新文章上线」这件事本身
- 推送是成功的 ——
git push给了正反馈,让人以为「发布完成了」
第 4 点最要命。git push 成功只代表代码进了仓库,离「站点更新」隔着四层。中间任何一环断了,push 那一秒的绿灯毫无意义。
六、留下的规矩
排查完我给自己记了几条,都是那种「不写下来一定会再踩」的:
1. 部署成功的唯一判据是 curl 线上,不是看流水线状态。
1 | # 流水线全绿 ≠ 部署成功。永远用这个判断 |
2. .cnb.yml 永远不能是空文件。 哪怕只有一个 echo stage。
3. 用 $: 不用 main:。 本仓库默认分支是 master,写 main: 等于把流水线永久关掉。
4. Webhook 返回 200 只代表送达。 部署分支要和仓库实际推送的分支对得上。
5. 排查 CI 时先看「有没有构建记录」,再看「构建是否成功」。 没有记录 = 事件没命中,有记录但失败 = 才是执行问题。这两步分开能省掉大量无效猜测。
七、回过头看
这次故障的触发条件挺离谱的:一次「不再同步到 gitee」的小改动,顺手清空了一个文件。
但它暴露的其实是一个结构性缺陷——整条部署链路上,没有一个环节在「部署没发生」时会通知人。四个环节全是「成功 / 失败」二元状态,缺了「压根没开始」这个状态。
后来补上的做法是在 .cnb.yml 注释里把三条铁律写死:
1 | # ⚠️ 三条铁律,删改前先读: |
配置文件本身是文档的一部分。半年后你只会记得「这里好像有个坑」,不记得坑是什么。
顺带一提:修好之后我做了个对照实验——把 EdgeOne 的部署分支从 main 改回 master,再推一次,90 秒后照样更新。至此整条链路在 master 上完全打通,main 分支是我为了排查临时推的,已经删掉了。