现象:部署"成功",站点消失

一次例行的内容更新,跑完 hexo g 和 hexo deploy,日志最后几行是熟悉的:

1
INFO  Deploy done: git

刷新线上地址——404。换几个文章页,全是 404。首页偶尔返回 200 但是空白。

登录 GitHub 看部署仓库,最新 commit 是:

1
Site updated: 2026-10-23 15:06:59

时间戳是最新的,看起来推成功了。但点进去看文件树——空的。没有 index.html,没有 posts/,只有一个 .git。

根因:工作区被清空,然后推了个空提交

hexo-deployer-git 的机制是:把 .deploy_git 当作一个独立的 git 工作区,每次部署时把 public/ 的内容同步进去,然后 commit + push。

关键点在于同步的方式:它不是增量覆盖,而是先清理工作区(相当于 git rm -r 全部内容 + 复制新文件),再提交。这个设计本身没问题——它保证删除的文章不会残留。

问题出在中断。

如果进程在「清空工作区」之后、「复制新文件」完成之前被杀,.deploy_git 就处于一个中间状态:文件全没了,但没有产生新的 commit。此时仓库里躺着的还是上一次的内容,看起来正常。

然后下一次(或者被中断的那次恢复后)执行 git add -A && git commit——如果此时工作区确实是空的,就会生成一个「删除所有文件」的提交,推上去。

线上就空了。

什么会杀掉它:管道

最容易被忽略的诱因是这一条命令:

1
npx hexo deploy | grep -E "Deploy done"

或者任何 | head、| tail 的变体。

管道的工作方式:grep 匹配到第一个结果后如果退出(grep -m1、或者 head -n5 读够行数就关闭 stdin),管道读端关闭,hexo 进程后续再往 stdout 写就会收到 SIGPIPE(信号 13),默认行为是终止进程。

hexo deploy 是个长流程:生成 → 复制到 .deploy_git → git add → git commit → git push → (如果配置了)其他部署插件。你只关心最后一行 Deploy done,于是 grep 前面的进度输出一出现就立即匹配退出,hexo 在复制文件的中途被杀。

你以为只是少看了几行日志,实际上是给了进程一刀。

这个问题在长时间部署里更容易触发——文件多、网络慢、push 耗时长,中途被杀的概率更高。

修复:日志重定向到文件

不要用管道看部署日志。重定向到文件,跑完再读:

1
2
3
npx hexo deploy > .workbuddy/tmp/deploy.log 2>&1
echo "exit=$?"
tail -20 .workbuddy/tmp/deploy.log

同样地,任何可能长时间运行的构建命令都适用这条规则:先落盘,再看盘。

部署后的三项核验

日志会说谎。Deploy done: git 这行是在 git push 成功后打印的,但如果 push 的内容本身就是空的,它照样打印。

真正的判断要看仓库状态。三项都过才算部署成功:

1
2
3
4
5
6
7
8
9
10
cd .deploy_git

# 1) 有新 commit,且时间戳是刚才
git log --oneline -1

# 2) 工作区干净(有未提交变更说明复制没完成)
git status --short | wc -l # 必须是 0

# 3) 关键文件确实在仓库根
ls index.html posts/ | head

任何一项不对,就重跑 hexo deploy。

这三项检查值得写进部署脚本,作为硬性门槛而不是靠人肉记忆。

一个会掩盖真实错误的干扰项

部署日志末尾常见这个:

1
2
3
4
'firebase' is not recognized as an internal or external command
FATAL Something's wrong.
Error: spawn firebase ENOENT
at notFoundError (D:\blog\node_modules\cross-spawn\lib\enoent.js:6:26)

如果 _config.yml 里配置了 hexo-deployer-firebase 但本机没装 firebase CLI,每次部署都会在最后报这个错,进程退出码是 2。

这会造成两种误判:

  1. 看到 FATAL 和 exit code 2,以为整体失败——实际上 git 部分可能已经成功
  2. 反过来,因为「这个错误每次都出现、是已知问题」而忽略整个日志——那么真正的中断错误也被一起忽略了

处理方式是:明确知道哪些错误是可忽略的,然后只核验那三项客观指标。不要用「日志里有没有 ERROR」来判断成败,要用「仓库状态对不对」来判断。

如果不再用 firebase 部署,把 _config.yml 里对应的 deployer 删掉,让日志干净,比留着习惯性忽略更好。

恢复步骤

站点已经空了,怎么救:

  1. 确认 public/ 目录还在且完整——构建产物没丢,只是没推上去
  2. **重跑 npx hexo deploy**(不用 hexo g,产物是好的)
  3. 重跑三项核验

hexo-deployer-git 会把 public/ 重新复制进 .deploy_git 并提交,空的提交会被新的覆盖。线上约 1 分钟内恢复(GitHub Pages 的生效时间)。

如果 public/ 也丢了(比如 hexo clean 之后没重新生成),就 hexo g && hexo deploy 走完整流程。

收口

这件事的本质不是 hexo 的 bug,而是用管道消费长任务的输出这个习惯性操作。

三条规则:

  1. 构建/部署类命令,输出重定向到文件,不用管道
  2. 成败看仓库状态,不看日志关键词(新 commit + 工作区干净 + 关键文件存在)
  3. 已知的噪声错误要清掉,不要训练自己忽略日志

第 3 条容易被当成洁癖。但「习惯性忽略某类报错」和「错过真正的事故信号」之间,只隔一次判断。


同期的部署链路整理见 记录一次把文章、音乐、抖音日更压进 watchdog,域名迁移期间的部署问题见 Hexo 换域名后 canonical 和 sitemap 还指着旧域。