架构记讲了三端同步的 happy path,进阶篇讲了冲突与回滚。这篇补最底层的课:Git 本身怎么组织多仓库——refs 写法、orphan 分支、force push 的语义,以及 Gitee Pages 在国内的真实表现。

仓库拓扑:三个仓库,两种身份

仓库/分支身份写入方
gjsvip/blog @ main唯一事实源人 + 本地生成器
gjsvip/blog @ gh-pages孤儿分支,Pages 部署产物Actions(force_orphan)
dujie_nb_admin/blog @ main只读镜像Actions 强推
dujie_nb_admin/blog @ gh-pagesGitee Pages 源Actions 强推

关键认知:gh-pages 是孤儿分支,与 main 没有共同祖先。所以镜像它必须 --force——非强推的 fast-forward 检查对两个无共同历史的分支永远失败。这不是权限问题,是 Git 对象模型问题。理解这一点,才知道哪些操作必须带 force、哪些场景下带 force 是犯罪。

三端能力对比摆在一起,分工就一目了然:

能力CF PagesGitHub PagesGitee Pages
国内访问一般差优
国际访问优优一般
分支预览部署有无无
一键回滚有无无
更新延迟秒级分钟级分钟级
前置条件无无实名认证

结论直接写进架构:CF Pages 是主力(预览 + 回滚 + 国际),GitHub Pages 是规范镜像(.html 形态兼容搜索引擎),Gitee Pages 是国内兜底。没有哪端是不可替代的,这正是每端都能被单独放弃的原因。

CI 双跑:两个工作流,一条主线

博客仓库有两个工作流,职责切干净:

  • deploy-cf-pages.yml:主部署。内部两个 job——deploy 用 cloudflare/wrangler-action 推 CF Pages,update-gh-pages 用 peaceiris/actions-gh-pages 以 force_orphan 生成 gh-pages,后者 needs: deploy 串成主线
  • mirror-to-gitee.yml:独立镜像。等 gh-pages 就绪(带 2 分钟超时的等待循环),把 main 与 gh-pages 强推到 Gitee

为什么拆两个工作流,而不是一个 job 串到底?三个理由:失败隔离——Gitee 抽风不该让「CF Pages 部署成功」变红;重跑粒度——镜像失败只重跑镜像,不动主部署;跳过语义——Gitee Secret 未配置时镜像步骤显式 exit 0 跳过,主链路无感:

- name: Mirror to Gitee
  run: |
    if [ -z "$GITEE_USER" ] || [ -z "$GITEE_TOKEN" ]; then
      echo "未配置 Secret,跳过 Gitee 镜像"; exit 0
    fi
    git remote add gitee "https://$GITEE_USER:$GITEE_TOKEN@gitee.com/$GITEE_USER/blog.git"
    git push --force gitee main:main
    if git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then
      git push --force gitee origin/gh-pages:gh-pages
    else
      echo "仓库无 gh-pages 分支,跳过"
    fi
  env:
    GITEE_USER: ${{ secrets.GITEE_USER }}
    GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }}
    GIT_TERMINAL_PROMPT: 0

令牌纪律单独说:Gitee 令牌只进 Actions Secrets,工作流里只有变量引用;GIT_TERMINAL_PROMPT=0 防交互卡死;本地 remote 里若存了令牌,那个文件永远别进仓库——文档与 workflow 里一律写占位符。

refspec:显式写法是保命符

早期 git push gitee main:gh-pages 被 Gitee 以 deny updating a hidden ref 拒绝——Gitee 对隐式 refspec 有额外校验。显式写全两侧 ref 名即可:

git push --force gitee origin/gh-pages:refs/heads/gh-pages
#                    ~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
#                    本地 remote-tracking ref   远端完整 ref 名

镜像侧同理:等待 gh-pages 就绪的循环里,fetch 也要显式 refspec——+refs/heads/gh-pages:refs/remotes/origin/gh-pages——否则默认 fetch 配置下可能取不到刚生成的分支。纪律一句话:多仓库操作,ref 永远写全,不赌默认值。平台对「猜出来」的 refspec 各有各的洁癖。

Gitee Pages:国内加速的真实画像

三端并存的核心目的是国内可达性:GitHub Pages 在国内的间歇性超时是祖传技能,CF Pages 主域国际优秀。Gitee Pages 的国内访问是另一个世界——同网络环境下首字节稳定在几十毫秒级,首页完全加载一秒内,这是把博客放 Gitee 镜像的全部理由。渡劫code 的落地页还做了国内外分流提示:Worker 通过 /api/region 读 CF 的 country 字段,向国内访客推荐 Gitee/Pages 入口。

但坑一样真实:

  • 实名认证前置:Gitee 账号要完成实名认证并手动开启仓库的 Pages 服务,缺一不可——缺了页面 404 且没有任何提示,排查全靠猜
  • 更新延迟:push 成功后 Pages 刷新有分钟级延迟,验证时别把「没生效」当故障,先看 Gitee 的部署记录
  • 无预览机制:Gitee Pages 没有 CF Pages 那样的分支预览部署,所以镜像的只能是已验证过的 gh-pages——它不配当第一道防线,只配当国内备份
  • 不做服务端跳转:国内访客分流靠落地页提示,不按地域 302——跳转在国内网络下反而制造二次故障,多一次 DNS 就多一次失败概率

本地手动同步:断电应急通道

CI 挂了也要能发文章。本地双 remote 是最后保险:

git remote -v   # origin=GitHub 主仓,gitee=镜像仓
git push origin main && git push gitee main:main
git ls-remote origin main   # 必须等于本地 HEAD,防「以为推了其实没推」

顺序永远是先主仓后镜像——GitHub 是事实源,镜像仓的任何状态都可以随时被覆盖重建。永远不要直接往镜像仓推内容,那是三端同步的第一条铁律。推送收尾绝不用 git push | tail 之类的管道——退出码被管道吞掉,失败会显示成「成功」,这个坑咬过所有迷信命令行管道的人。

还有个只在多仓库场景出现的细节:镜像 job 的 checkout 必须 fetch-depth: 0。默认浅克隆只有最近一次提交,孤儿分支的历史不在其中,rev-parse origin/gh-pages 直接失败。一行 fetch-depth: 0 换来完整历史,是多仓库 CI 的隐形门票。

小结

多仓库同步的成熟度标准:每条 ref 都显式、每端都可重建、本地有应急通道。Git 的对象模型决定了 orphan + force 是镜像的唯一正确姿势,CI 的 job 划分决定了故障时的爆炸半径。把这些钉死,剩下的交给 Actions——它们从不需要睡觉。下一篇聊聊支撑这一切的 Cloudflare 免费额度怎么做到月账单 0 元。