静态站自动部署链路:一次「站点静默 3 天」的排查

博客从 10-17 到 10-20,整整三天没更新过。

新文章写了三篇,线上还是老样子;首页的 Last-Modified 死死停在 Sat, 17 Oct 2026 10:10:03 GMT。有意思的是所有人都没发现——包括我自己。

这事的价值不在「修好了」,而在「为什么所有人都以为它是好的」。这篇把完整证据链记下来。

静态站自动部署链路与三个断点

一、先把现象钉死

静态站的部署故障有个讨厌的地方:它不报错。页面白屏会有人喊,部署失败会有一封告警邮件,但「部署压根没触发」是完全静默的。

所以第一步不是猜,是把证据固定下来:

1
2
3
4
5
6
# 1. 线上首页最后修改时间 —— 卡在 10-17,说明从那之后没有任何部署
curl -sI https://www.zzwei.cn/ | grep -i last-modified

# 2. 新文章 URL 是不是 404 —— 确认是「没部署」而不是「路由变了」
curl -so /dev/null -w "%{http_code}\n" \
https://www.zzwei.cn/2026/10/21/2026-10-21-footer-hierarchy-refactor/

两个信号一起,才能确定是「部署没发生」,而不是「部署了但内容不对」。

然后去 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
2
3
main:
push:
- name: keep-alive

结果 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.yml file to notify Makers of code push events.

也就是说,光有 push 事件不够,还得主动通知 Makers。官方给的 Webhook 地址是固定值:

1
https://api.edgeone.ai/eo/pages/hook/cnb

在 .cnb.yml 里加一个 stage:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$:
push:
- name: notify-edgeone
stages:
- name: webhook
image: cnbcool/webhook
settings:
urls:
- https://api.edgeone.ai/eo/pages/hook/cnb
method: POST
content_type: application/json
template: |
{
"repoName": "{{ CNB_REPO_NAME }}",
"repoUrl": "{{ CNB_REPO_URL_HTTPS }}",
"branch": "{{ CNB_BRANCH }}",
"commit": "{{ CNB_COMMIT_SHORT }}",
"commitMessage": "{{ CNB_COMMIT_MESSAGE }}",
"committer": "{{ CNB_COMMITTER }}",
"event": "{{ CNB_EVENT }}",
"buildId": "{{ CNB_BUILD_ID }}",
"buildWebUrl": "{{ CNB_BUILD_WEB_URL }}",
"buildUser": "{{ CNB_BUILD_USER }}",
"pipelineName": "{{ CNB_PIPELINE_NAME }}"
}

推上去之后,CNB 构建 success,webhook stage 的日志也漂亮:

1
2
3
4
[DEBUG] Sending POST request to https://api.edgeone.ai/eo/pages/hook/cnb
[DEBUG] Payload: { "repoName": "zzWeiBlog", "branch": "master", "commit": "74967d09", ... }
[DEBUG] Received response status: 200
[DEBUG] Response body: { "code": 0, "data": {}, "message": "ok" }

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 跳到新时间。根因确认。

五、为什么三天都没人发现

现在回头看,这套故障能静默三天的原因很清晰:

  1. 所有环节都显示成功 —— CNB 构建绿灯,webhook 200,日志里没有任何 ERROR
  2. 静态站不报错 —— 旧页面照常渲染,只是内容停在三天前
  3. 没有内容变更告警 —— 没有人在意「今天有没有新文章上线」这件事本身
  4. 推送是成功的 —— git push 给了正反馈,让人以为「发布完成了」

第 4 点最要命。git push 成功只代表代码进了仓库,离「站点更新」隔着四层。中间任何一环断了,push 那一秒的绿灯毫无意义。

六、留下的规矩

排查完我给自己记了几条,都是那种「不写下来一定会再踩」的:

1. 部署成功的唯一判据是 curl 线上,不是看流水线状态。

1
2
# 流水线全绿 ≠ 部署成功。永远用这个判断
curl -so /dev/null -w "%{http_code}\n" https://www.zzwei.cn/2026/10/21/2026-10-21-footer-hierarchy-refactor/

2. .cnb.yml 永远不能是空文件。 哪怕只有一个 echo stage。

3. 用 $: 不用 main:。 本仓库默认分支是 master,写 main: 等于把流水线永久关掉。

4. Webhook 返回 200 只代表送达。 部署分支要和仓库实际推送的分支对得上。

5. 排查 CI 时先看「有没有构建记录」,再看「构建是否成功」。 没有记录 = 事件没命中,有记录但失败 = 才是执行问题。这两步分开能省掉大量无效猜测。

七、回过头看

这次故障的触发条件挺离谱的:一次「不再同步到 gitee」的小改动,顺手清空了一个文件。

但它暴露的其实是一个结构性缺陷——整条部署链路上,没有一个环节在「部署没发生」时会通知人。四个环节全是「成功 / 失败」二元状态,缺了「压根没开始」这个状态。

后来补上的做法是在 .cnb.yml 注释里把三条铁律写死:

1
2
3
4
5
6
7
8
9
10
# ⚠️ 三条铁律,删改前先读:
#
# 1. .cnb.yml 不可为空文件!
# 即使 stage 什么也不做,CNB 也必须能解析出 push 事件……
#
# 2. 这里必须用 `$:` 而不是 `main:`——`$` 表示对所有分支生效。
# 用 `main:` 时 CNB 只会匹配名为 main 的分支,而本仓库默认分支是 master。
#
# 3. EdgeOne Makers 不监听 CNB 仓库事件,CNB 也没有原生 Webhook。
# 必须在 .cnb.yml 里用 cnbcool/webhook 插件主动通知 Makers。

配置文件本身是文档的一部分。半年后你只会记得「这里好像有个坑」,不记得坑是什么。


顺带一提:修好之后我做了个对照实验——把 EdgeOne 的部署分支从 main 改回 master,再推一次,90 秒后照样更新。至此整条链路在 master 上完全打通,main 分支是我为了排查临时推的,已经删掉了。