上一篇讲了三端同步的「happy path」:一次 push,CF Pages、GitHub Pages、Gitee 全部更新。但真实世界里,同步系统的难点从来不是搭建,而是冲突发生时谁说了算、坏了怎么退回去。这篇记录我们把三端同步从「能跑」打磨到「敢睡」的完整过程。
铁律:main 是唯一事实源
所有冲突的根源都是多写点。我们的纪律:
- Gitee 仓库不接收任何直接推送与 PR,只做
--force镜像目标,它的 gh-pages 仅作为 Gitee Pages 的源 - Cloudflare Pages 与 GitHub Pages 都由 GitHub Actions 从 main 部署,本地
wrangler pages deploy只用于调试 - 内容只进
posts.json与posts/*.body.html,HTML 产物由gen_posts.py生成——消灭「手改页面与元数据不一致」这类冲突
产物可重新生成,就没有「保留哪一份」的争吵。这是三端同步里最值钱的一条架构决策。
冲突场景与对策
1. gh-pages 的孤儿分支竞态
部署 job 用 peaceiris/actions-gh-pages 以 force_orphan 推 gh-pages,而镜像 job 需要 刚刚生成 的 gh-pages。首次推送时 gh-pages 尚不存在,镜像 job 直接失败。解法是带退避的等待循环,最多等 2 分钟,超时则跳过而非报错——镜像失败不该阻塞主链路:
for i in $(seq 1 12); do
if git fetch origin '+refs/heads/gh-pages:refs/remotes/origin/gh-pages' 2>/dev/null; then
echo "gh-pages 已就绪"; exit 0
fi
echo "等待 gh-pages 生成… ($i/12)"; sleep 10
done
echo "gh-pages 等待超时,跳过"
2. Gitee 拒绝镜像推送
早期 git push gitee main:gh-pages 报 deny updating a hidden ref。Gitee 对隐式 refspec 有额外校验,显式写成 refs/remotes/origin/gh-pages:refs/heads/gh-pages 保活推送即可。同时镜像必须 --force:orphan 分支历史与 main 无共同祖先,非强推永远被拒。
3. 令牌明文被 push protection 拦截
GitHub secret scanning 会拦截含 CF 令牌的提交。纪律:文档与 workflow 里一律占位符(cfat_xxx),真实值只进 Secrets;本地误提交用 git filter-repo 清历史并立即轮换令牌——扫描拦截只是提醒,进了历史就等于公开。
4. wrangler 缓存旧账号 ID
Pages 部署报 AuthenticationError,真凶是 node_modules/.cache/wrangler/pages.json 缓存了上一个账号的 ID。CI 里每次 fresh checkout 无此问题,本地复现就删 node_modules/.cache/wrangler/ 与 ~/.config/.wrangler/。
回滚:每一端都要有独立的后悔药
| 端 | 回滚方式 | 耗时 |
|---|---|---|
| Cloudflare Pages | Dashboard → Deployments → 选旧版本 Rollback | 秒级,无需 git 操作 |
| Cloudflare Workers | wrangler rollback --version-id <旧版本> | 秒级 |
| GitHub Pages | 重新 push 旧 commit 到 main,Actions 重跑 | 分钟级 |
| Gitee Pages | 跟随 GitHub 重推,无需单独操作 | 分钟级 |
Git 层面的回滚优先 git revert 而非 reset --hard + force push:revert 保留历史且对协作者安全;只有确认远端无其他依赖时才 force。D1 的 schema 变更同理——迁移只加不改,回滚代码不回滚表结构,用「新代码兼容旧表」保证可退(评论系统的表结构见评论系统实战)。回滚的目标不是「回到过去」,而是「用最短时间恢复服务」:先止损,再复盘,顺序不能反。
推送后的强制验证
git push 成功不等于部署成功。我们的收尾检查是硬规矩:
git push origin main && git push gitee main:main
git ls-remote origin main # 必须等于本地 HEAD
curl -sI https://www.520691.xyz/ | head -1 # 200
curl -sI https://gjsvip.github.io/blog/ | head -1 # 200(可能 404:Pages 部署中)
- 绝不用
git push | tail之类管道收尾——退出码被管道吞掉,失败也会显示「成功」 git ls-remote比对远端 SHA 与本地 HEAD,防「以为推了其实没推」- github.com 间歇超时是已知问题,重试最多 6 次、间隔 15 秒,不做无限重试
- 验证脚本本身进仓库,新人第一次推送就能照着跑,不依赖口口相传
5. CF Pages 缓存导致的「改了没生效」
推完站点刷新却还是旧页面,九成是边缘缓存。HTML 缓存短,但 assets/ 下的静态资源带长缓存头——改完 CSS 不换文件名,用户就会持续读到旧版。对策是资源文件名带内容哈希,或部署后 curl -sI 核对 cf-cache-status 与 ETag。这类问题本地永远复现不了,因为它只发生在边缘。静态资源的缓存策略与 sitemap 部署的完整流程,见静态博客 SEO 全流程。
预览部署与灰度
Cloudflare Pages 会为每个分支与每次 PR 生成独立预览部署(<hash>.gjsvip-blog.pages.dev)。改样式、改模板先用预览链接自测,再合 main——把「合完才发现样式炸了」消灭在评审阶段。Workers 端没有分支概念,灰度靠 wrangler versions upload 上传新版本后按百分比逐步切流量,出问题一键回退旧版本,比直接 deploy 覆盖安全得多。
Gitee Pages 没有预览机制,也不需要有:它只镜像已验证过的 gh-pages。三端各用适合自己的灰度手段,但放行标准只有一条:main 上的版本。每次部署结束,Actions 把本次 commit、三端地址与耗时写进 Step Summary——看不见的部署等于没部署。
把回滚当成功能测试
没演练过的回滚等于没有回滚。我们定期做一次「故障演习」:故意往 main 合入一个明显错误的提交,然后走一遍三条回滚路径——CF Pages Dashboard 回滚、wrangler rollback、git revert——记录每条的耗时与副作用。早前一次 rollback 之后 D1 迁移没有同步退,新旧代码短暂不兼容,于是定下「迁移只加不退、新代码兼容旧表」的规矩。回滚的信心是排练出来的,不是文档写出来的。
小结
三端同步的成熟度不看搭建速度,看两件事:冲突时有没有唯一事实源,出事时每端有没有独立回滚路径。做到这两点,剩下交给 Actions——它们从不需要睡觉。