与买桂花同载酒 🌙

博客双仓库迁移实录

前言

博客跑了半年多,某天审视仓库时发现两个问题:

一是 hexo-blog-encrypt 加密的文章,明文 markdown 就躺在公共仓库的 main 分支里。加密只保护了渲染后的站点,任何人在 GitHub 上点开仓库就能直接读原文——加密了个寂寞。

二是 _config.yml 里有一段祖传的 deploy: 配置,指向源码所在的 main 分支。hexo-deployer-git 是 force push,哪天手滑跑了 npm run deploy,生成的 HTML 会直接把源码分支覆盖掉。这是从「main 放产物、源码只放本地」的旧教程架构带过来的遗产,在我「源码住 main」的新架构下变成了一颗地雷。

于是决定做一次彻底的架构改造:双仓库——源码进私有仓库,公共仓库只留构建产物,顺便把旧历史全部清掉。

目标架构

1
2
3
4
5
6
7
Zebin-Blog-Source(私有)                ZebinGao.github.io(公共)
├── markdown 源码、草稿 └── gh-pages 分支:只有生成的
├── 主题、配置 HTML/CSS/RSS 静态产物
└── Actions workflow
│ git push 触发

自动构建 → 推 gh-pages → 网站更新

日常写作流程从「push 源码等 CI」变成……其实没变,还是 git push,只是推的对象换了仓库。部署彻底变成 push 的副产品,不再有任何手动发布命令。

关键技术点

1)GITHUB_TOKEN 跨不了仓库。 第一个念头是用 Actions 自带的 secrets.GITHUB_TOKEN——免费、自动、免配置。但它在单仓库架构下能用,双仓库下物理上走不通

1
2
作用域:只属于「运行 workflow 的那个仓库」(私库)
有效期:本次 job 结束即作废

GitHub 刻意如此设计:即使某个仓库的 workflow 被塞了恶意代码,偷走的 token 也只够得着自己这个仓库——一道防横向渗透的护栏。所以跨仓库推送必须换凭证,候选有三个:Deploy Key(推荐)、fine-grained PAT、GitHub App。个人博客用 Deploy Key 最合适:只对一个仓库有效、无过期时间、权限窄到不能更窄

2)Deploy Key 的方向性:公钥挂门上,私钥揣身上。 配置时最容易懵的就是两张密钥各放哪。记住方向就行——谁访问谁

  • 私钥是身份凭证,谁发起访问谁持有 → 存进私库的 Settings → Secrets and variables → Actions(新建名为 DEPLOY_KEY 的 repository secret)
  • 公钥是验证用的「锁芯」,装在被访问的仓库上 → 挂到公共库的 Deploy keys(勾选 write access)

和你平时 push 到 GitHub 的原理一模一样:本机的 id_ed25519 私钥留在电脑里,对应的公钥贴在 GitHub 账号上。钥匙在访问者手里,锁芯在目的地。「公钥配公共库、私钥配私有库」听着像按名字配对,纯属巧合。

顺带把这把钥匙的「出身」说清楚——它来自本地一条标准命令:

1
ssh-keygen -t ed25519 -C "blog-deploy-key" -f ~/.ssh/blog_deploy_key -N ""

-t ed25519 指定椭圆曲线签名算法(现代首选:密钥短、验证快),-C 是给人看的注释标签,-f 指定私钥的保存路径(公钥自动存为同名 .pub 文件),-N "" 设置空密码短语——CI 环境没法交互式输密码,所以钥匙文件本身不能再加锁。

而「产生」在密码学上分两步:先向操作系统申请 256 位密码学安全的随机数作为种子,私钥由种子确定;公钥再由私钥经椭圆曲线单向运算派生。注意公私钥不是两个独立的随机数,而是数学上绑定的一对:从私钥算公钥瞬间完成,从公钥反推私钥在计算上不可行——这正是整套验证的根基:GitHub 只需持有公钥,就能验证来者是否握有对应私钥,而它自己永远推不出私钥。生成过程完全在本地完成,在公钥挂上 GitHub 之前,这对钥匙只存在于我这台电脑上。

workflow 里核心就这几行:

1
2
3
4
5
6
7
8
- name: Deploy to public repo
uses: peaceiris/actions-gh-pages@v4
with:
deploy_key: ${{ secrets.DEPLOY_KEY }}
external_repository: ZebinGao/ZebinGao.github.io # 跨仓库的关键参数
publish_dir: ./public
publish_branch: gh-pages
allow_empty_commit: true # 内容无变化也产生新提交,便于外部验证部署落地

3)删了库,钥匙没释放。 公共仓库删除重建后,把同一把公钥挂到新库,报错 key is already in use——Deploy Key 是单仓库绑定,而删除仓库的解绑有传播延迟。用旧钥匙试探新库确认写不进去后果断换方案:直接铸一把新钥匙(重新 ssh-keygen,公钥挂新库、私钥更新 secret),比赌 GitHub 的解绑速度确定性强得多。

顺带学到一个诊断技巧——ssh -T 能让 GitHub 亲口告诉你一把钥匙的身份:

1
2
$ ssh -i ~/.ssh/blog_deploy_key2 -o IdentitiesOnly=yes -T git@github.com
Hi ZebinGao/ZebinGao.github.io! # 账号级钥匙报用户名,Deploy Key 报仓库名

4)updated_option: 'mtime' 在 CI 上必然翻车。 一个隐蔽的坑:Hexo 的 updated_option: 'mtime' 表示用文件修改时间作为文章的 updated 时间。本地跑没毛病,但 CI 每次都是全新 clone——所有文件的 mtime 都等于 clone 时间。后果是每次部署,RSS 里所有文章的 <updated> 全变成同一个构建时间戳,订阅者看到「全部文章都更新了」。

验证方法很简单,拉下 gh-pages 分支看 atom.xml:

1
2
git show origin/gh-pages:atom.xml | grep -o '<updated>[^<]*</updated>' | head -5
# 五个一模一样的时间戳 = 中招

修复只要一行:updated_option: 'date',真改了文章再手动在 front-matter 写 updated:

5)Pages 纹丝不动,查 deployments API。 新库 Pages 配置好了,域名 DNS check 也通过了,网站却一直 404。排查的关键一步是查部署记录:

1
2
curl -s https://api.github.com/repos/<user>/<repo>/deployments
# 返回空数组 = Pages 一次构建都没跑过

原因:gh-pages 分支的推送发生在配置 Source 之前,GitHub 不会回头补构建。解法是给分支来一次新推送触发它——而且用 git commit-tree 可以不碰工作区地构造一个空提交:

1
2
NEW=$(git commit-tree origin/gh-pages^{tree} -p origin/gh-pages -m "Trigger Pages build")
git push <repo_url> $NEW:refs/heads/gh-pages

另外两个容易误判的点:.nojekyll 文件 Hexo 会自动放进产物,Jekyll 转换天然是关的,不用额外设置;自定义域名会做 www ↔ 裸域名的 301 规范化,curl 测试时要加 -L 跟随重定向,否则看到 301 以为挂了。

6)顺带理解了 Git 的「删除」。 这次选了最彻底的清理档位:删库重建,而不是只删 main 分支。因为 Git 删分支只是撕掉一张便利贴——分支是指向某个提交的指针,指针没了,提交对象还躺在服务器存储里,知道 40 位哈希的人理论上还能通过直链访问,直到平台某天垃圾回收。要真正让历史不可达,删库重建是最干净的手段(当然,被别人 clone 走的副本谁也管不着——好在个人博客没人 clone)。

迁移后的日常

迁移前 迁移后
源码 公共库 main,人人可读 私有库,加密文章明文、草稿全部隐藏
公共库 源码 + 历史 + 遗留雷配置 只剩构建产物,历史从零开始
发布 push 等 CI(旁边躺着手滑雷) 同样 push 等 CI,雷已拆除
备份 插件(还没配过) git 本身:私库 + 两台电脑

写作习惯零改动:本地(或手机网页端在私库上)写 markdown → git push → 一两分钟后网站自动更新。唯一要记住的是公共仓库从此当成只读的「显示屏」,别再在它上面改任何东西。

写在最后

这次迁移动手前看着吓人(要动生产站点),实际拆开每一步都有明确的技术依据和验证手段:git ls-remote 轮询确认部署落地、ssh -T 验证密钥身份、deployments API 查构建状态、atom.xml 验证时间戳修复——每个环节都可观测,就不需要靠胆量

最后附上当时的完整任务清单。标记 🫵 的步骤需要我本人在网页上动手(建仓库、配密钥、删库——全是涉及账号凭证的事,机器代劳不了),其余全部在命令行里自动化完成:

  • ✅ 提交待处理的笔记内容
  • ✅ 本地清理:卸载 hexo-deployer-git / hexo-git-backup、删 deploy 配置块、修 updated_option、删闲置主题
  • ✅ 编写跨仓库部署的新 workflow
  • ✅ 生成 Deploy Key 密钥对
  • 🫵 创建私有仓库
  • 🫵 配置 Deploy Key(公共库)和 DEPLOY_KEY secret(私库)
  • ✅ 推送完整历史到私库,验证构建部署全链路
  • 🫵 删除公共库并同名重建、重新挂 Deploy Key
  • ✅ 推送产物到新公共库、配置 Pages 和域名
  • ✅ 最终验证:网站 200、RSS 正常、文章页正常、HTTPS 生效

十步里只有三步需要人到场。如果你也在考虑给 Hexo 博客做同样的改造,希望这份清单和这些坑能帮你少走几步弯路。