先把失败范围缩到最后一步

这次 GitHub Actions 运行中,Checkout、Node 环境、依赖安装和静态构建全部通过,只有 peaceiris/actions-gh-pages 的推送失败。日志明确显示它正在向 JimLiuxinghai.github.iomaster 分支执行 git push,随后远端返回用户名或令牌无效。这个证据很重要:如果构建产物已经生成,就不应该继续修改主题、Node 版本或 Hexo 配置。

原工作流把个人访问令牌保存为 PAGES_TOKEN。这类令牌可能过期、被撤销,也可能因为权限模型变化失去目标仓库写权限。重新生成一个覆盖多个仓库的个人令牌当然能恢复,但它同时扩大了泄露后的影响范围。

为什么选择单仓库 Deploy Key

部署任务只需要完成一件事:从博客构建仓库向站点仓库写入生成文件。Deploy Key 把一对 SSH 密钥直接绑定到一个目标仓库,公钥只获得该仓库的写权限;私钥作为源仓库的 Actions Secret 保存。即使这把密钥泄露,它也不能读取或修改账号下的其他私有仓库。

我为目标仓库创建独立的 Ed25519 密钥,把公钥命名为 blogbackup GitHub Actions deploy key 并开启写权限;私钥写入源仓库的 PAGES_DEPLOY_KEY。工作流只把配置从 personal_token 改成 deploy_key,目标仓库、发布分支和产物目录保持不变。改动越小,越容易判断恢复是否来自权限修复。

工作流本身仍采用最小权限

源仓库工作流保留 permissions: contents: read。它只需要读取源码;向另一个仓库写入时使用专用 SSH 密钥,不需要给自动生成的 GitHub Token 额外权限。部署步骤仍固定外部仓库、master 分支和 blog/public 目录,避免模糊变量把内容推向错误位置。

密钥生成后,我先通过 API读取目标仓库已有 Deploy Key 和源仓库已有 Secret 的名称,确认不会创建重复项。写入后只查看 Secret 的元数据,不读取私钥内容。临时目录中的私钥在部署验证完成后立即删除,长期副本只留在 GitHub 加密 Secret 中。

验证不能停在“推送成功”

新工作流触发后用时 36 秒,构建和部署步骤全部成功。随后我检查了三个独立信号:目标仓库最新提交是否引用本次源提交;线上 /ads.txt 是否包含正确发布商记录;首页是否恰好加载一次 AdSense 脚本。只有这三项同时成立,才能证明权限、产物和线上内容形成了完整链路。

如果只看绿色 CI,仍可能把空目录、旧产物或错误分支推送成功。部署的完成条件应该写成用户可观察的结果,而不是某个命令的退出码。

这套方案的边界

Deploy Key 适合单一来源向单一目标仓库发布。多个来源仓库不能复用同一把 Deploy Key;如果项目需要组织级权限、审计、短期凭据或更复杂的仓库矩阵,GitHub App 会更合适。密钥也不会自动轮换,因此仍应记录用途,并在工作流停用时从目标仓库和源 Secret 两端一起撤销。

这次故障最后只改了一行工作流配置,但可靠修复依赖完整的判断顺序:先读失败步骤,再收窄权限,再验证线上结果。它比“重新塞一个更大的 Token”多花几分钟,却把后续风险控制在了一个仓库里。