从零配置 GitHub Actions 自动部署 Hexo 博客
Hexo 博客最容易卡住的地方,通常不是 hexo generate 本身,而是生成后的 public/ 怎么稳定发布。手动 hexo deploy 能跑,但它把构建环境、网络、Git 凭据都绑在本地机器上。换一台电脑、换一个网络、换一个 Node 版本,部署结果就可能变得不可预测。
更稳的做法是把部署流程写进仓库:源码仓库只保存 Markdown、主题配置、依赖锁和 workflow;每次 push 到 main,GitHub Actions 在云端安装依赖、生成静态站点,再把 public/ 推送到 GitHub Pages 仓库。
这篇文章记录从零配置这套流程的完整路径。例子以当前博客为准:
- 源码仓库:
magicliang/hexo-blog - 站点仓库:
magicliang/magicliang.github.io - Hexo 版本:8.1.2(Butterfly 5.x 主题)
- 包管理器:Yarn 1
- 构建命令:
npx hexo clean && npx hexo generate - 部署方式:
peaceiris/actions-gh-pages@v4推送public/
目标结构
最终结构不是在一个仓库里同时维护源码和构建产物,而是把两件事拆开:
1 | |
部署链路是:
1 | |
这样做的好处是清楚的:源码仓库只关心写作和配置,站点仓库只承载最终 HTML、CSS、JS 和图片。失败也好排查,Actions 日志会停在安装依赖、生成站点或推送站点中的某一步。
准备两个仓库
第一步是准备源码仓库。它至少要能在本地跑通:
1 | |
如果本地都不能生成,Actions 只是把失败搬到云端,不会自动修好依赖或配置问题。
第二步是准备 GitHub Pages 仓库。个人 Pages 通常使用这个命名:
1 | |
这个博客对应的是:
1 | |
Pages 仓库需要允许被写入。对于跨仓库部署,源码仓库自带的 GITHUB_TOKEN 不够用,因为它默认只属于当前 workflow 所在仓库。要写另一个仓库,需要额外凭据。
创建最小权限 Token
部署动作只需要把生成好的静态文件写进 magicliang.github.io。因此 token 不应该拿到整个账号的 repo 权限,更适合用 fine-grained personal access token。
创建路径:
1 | |
关键配置:
| 配置项 | 推荐值 |
|---|---|
| Token name | hexo-blog-deploy |
| Expiration | 个人博客可选 No expiration,更保守可选 180 或 366 天 |
| Resource owner | 自己的账号 |
| Repository access | Only select repositories |
| Selected repository | magicliang/magicliang.github.io |
| Repository permissions | Contents: Read and write |
| Metadata | GitHub 自动附带 Read-only |
这个 token 的边界很窄:它只能写目标 Pages 仓库的内容,不能写源码仓库,也不能写账号下其他仓库。相比 classic PAT 的 repo scope,风险面小很多。
生成 token 后,页面只会显示一次。复制之后立刻写入源码仓库的 Actions secret。
配置 DEPLOY_TOKEN
进入源码仓库:
1 | |
填写:
1 | |
workflow 里不会直接写 token 明文,而是通过:
1 | |
读取 GitHub 加密保存的值。这个名字要和 workflow 中的名字完全一致。
固定 npm registry
如果 yarn.lock 里混有国内镜像、公司内网 registry 或临时代理地址,海外 GitHub runner 可能会超时。这个博客用 .yarnrc 把 registry 固定到 npm 官方源:
1 | |
同时,yarn.lock 里的 resolved URL 也应该尽量保持可从 GitHub runner 访问。不要依赖只在本地网络能通的镜像地址。
写 deploy.yml
GitHub Actions 的 workflow 必须放在:
1 | |
当前博客使用的核心配置如下:
1 | |
这份文件决定了整个部署系统的行为。GitHub 页面上的 Actions 面板只是运行和展示入口,真正可 review、可回滚的配置都在仓库里。
当前博客的实际 deploy.yml 在这段核心配置上方还有约 37 行注释,记录了触发方式、相关链接、前置配置步骤和本地 fallback 入口。把操作文档直接写在 YAML 文件头部,跟配置走同一份版本控制,比散落在 wiki 或 README 里更不容易失去同步。
TZ: Asia/Shanghai 用来让 GitHub runner 的构建时区和博客配置保持一致。Hexo 的文章日期会参与 permalink 生成,如果文章只写 date: 2026-06-20 这种没有具体时分秒的日期,云端机器的默认时区可能让路径偏到前一天。把时区写进 workflow 后,本地预览和云端产物更容易一致。
触发条件
1 | |
push.branches: [main] 表示只要 main 分支收到新 commit,就自动部署。workflow_dispatch 允许在 GitHub 网页手动点 Run workflow,适合不想制造新 commit、只想重新跑一次部署的场景。
这两个触发方式覆盖了日常写作和手动修复:
| 场景 | 触发方式 |
|---|---|
| 写完文章并 push | 自动触发 |
| Secret 刚替换,想验证是否可用 | 手动触发 |
| GitHub runner 网络抖动,想重试 | 手动触发或 rerun |
并发控制
1 | |
博客部署不需要同一分支同时跑多次。连续 push 两个 commit 时,旧 run 继续跑只会浪费 runner 时间,甚至可能把旧版本站点推上去。concurrency 把同一分支的部署归到一个组里,新的 run 会取消还没完成的旧 run。
最小化 GITHUB_TOKEN 权限
1 | |
这里限制的是 workflow 自带的 GITHUB_TOKEN。源码仓库只需要被 checkout 读取,不需要用它写回源码仓库。真正写 Pages 仓库的是 DEPLOY_TOKEN。
这条配置不是装饰。Actions 是供应链攻击里很常见的入口,默认权限越小,某一步脚本出错或被污染时能造成的影响越小。
安装 Node 和依赖
1 | |
node-version: '22' 固定构建环境,避免 runner 镜像升级后突然换 Node 版本。cache: 'yarn' 让 Actions 缓存 Yarn 下载内容,减少每次从 registry 拉包的时间。cache-dependency-path: yarn.lock 让缓存跟 lockfile 绑定,依赖变化时自然换缓存。
--frozen-lockfile 的意义是拒绝在 CI 里偷偷改依赖树。如果 package.json 和 yarn.lock 不一致,CI 应该失败,而不是在云端生成一份没有进入 Git 的临时 lockfile。
CI 把 Node 版本固定在 22。本地开发时,deploy.sh 通过 nvm 或 fnm 自动加载当前可用的 Node.js,不限制具体版本。两边 Node 版本不一致通常不影响 Hexo 生成结果,除非某个依赖有 Node 版本硬性要求。
生成静态站点
1 | |
hexo clean 清掉旧的 public/ 和缓存,避免历史构建残留。hexo generate 根据 Markdown、主题和配置重新生成完整静态站点。
这里没有调用 hexo deploy。原因是 workflow 后面已经由 peaceiris/actions-gh-pages 负责发布 public/。hexo deploy 可以保留给本地 fallback,但云端部署链路不需要它。
推送到 Pages 仓库
1 | |
这一步把 public/ 的内容推送到目标仓库的 main 分支。
几个字段的含义:
| 字段 | 含义 |
|---|---|
personal_token |
使用 DEPLOY_TOKEN 写外部仓库 |
external_repository |
目标 Pages 仓库 |
publish_branch |
目标仓库接收静态文件的分支 |
publish_dir |
本次要发布的本地目录 |
如果源码仓库和 Pages 仓库是同一个仓库,GITHUB_TOKEN 可能够用。但当前这种“源码仓库 -> 另一个 Pages 仓库”的形态,需要额外 token 或 deploy key。这个博客选择 fine-grained PAT,是因为它比 classic PAT 权限小,配置又比 SSH deploy key 少一步。
Pages 仓库怎么配
目标仓库 magicliang.github.io 需要启用 GitHub Pages。典型配置是:
1 | |
对于 <username>.github.io 这种个人 Pages 仓库,GitHub 会把它发布到:
1 | |
当前博客就是:
1 | |
如果使用自定义域名,还要在 Pages 页面配置域名和 HTTPS。没有自定义域名时,默认的 github.io 地址已经足够。
第一次验证
配置完成后,最小验证路径是推一个很小的 commit。比如修改一篇文章、提交、push:
1 | |
然后打开:
1 | |
观察最新的 Deploy Hexo Blog run。一次成功的 run 至少要看到这些阶段通过:
1 | |
再打开目标仓库:
1 | |
如果最新 commit 来自 Actions,说明推送已经成功。最后访问站点地址,确认文章已经出现在页面上。
常见失败
403 或 Permission denied
这通常是 token 权限不对。
优先检查:
- Secret 名字是不是
DEPLOY_TOKEN - token 是否选中了
magicliang.github.io - token 是否有
Contents: Read and write - workflow 里是否写成
personal_token: ${{ secrets.DEPLOY_TOKEN }} - token 是否已经被 revoke 或过期
如果刚从 classic PAT 迁到 fine-grained PAT,先不要立刻删除旧 token。等新 token 跑通一次后,再 revoke 旧 token。
yarn install 失败
常见原因是 package.json 和 yarn.lock 不一致,或者 lockfile 里的 registry 地址在 GitHub runner 访问不到。
处理顺序:
1 | |
如果 lockfile 里出现只适合本地网络的 registry,应统一回 npm 官方源,避免云端 runner 被网络环境卡住。
Hexo generate 失败
这类失败通常和文章 front matter、主题配置、Markdown 渲染器或依赖版本有关。先在本地跑:
1 | |
本地复现后再修。不要只在 Actions 页面反复 rerun。
部署成功但页面没变
可能原因有三个:
- GitHub Pages 还在构建或缓存,等一两分钟再刷新。
- 目标仓库 Pages 分支不是 workflow 推送的分支。
- 浏览器缓存了旧页面。
先看 magicliang.github.io 仓库有没有收到新 commit,再看 Pages 设置。目标仓库没有新 commit 时,问题在 Actions 推送;目标仓库有新 commit 但页面不变时,问题在 Pages 配置或缓存。
日常写作-发布循环
配置跑通之后,日常写作不需要在本地跑构建和部署。当前博客准备了两个脚本,对应两条发布路径。
lite-deploy.sh
lite-deploy.sh 只负责源码提交和 push,构建交给 Actions:
1 | |
脚本内部做三件事:检测工作区变更并提交、git pull --rebase origin main、git push origin main。push 到 main 后 GitHub Actions 自动接管,云端完成 hexo generate 和站点部署。
不要求本地安装 Node.js 或 Hexo CLI。写完文章跑一次脚本,等 Actions 变绿,站点就更新了。
deploy.sh
CI 故障、想在本地预览后再部署、或 Hexo 配置变更需要验证时,用完整本地部署:
1 | |
deploy.sh 先委托 lite-deploy.sh --no-push 完成源码提交和 rebase,再加载本地 Node.js 环境(自动检测 nvm 和 fnm),然后依次跑 hexo clean、hexo generate 和 hexo deploy,最后 push 源码仓库。
两条路径的分工:
| lite-deploy.sh | deploy.sh | |
|---|---|---|
| 构建位置 | GitHub Actions(云端) | 本地机器 |
| 本地需要 Node/Hexo | 不需要 | 需要 |
| 适用场景 | 日常写作 | CI 故障、本地预览、配置调试 |
日常用 lite-deploy.sh,遇到问题再切 deploy.sh。
安全收尾
部署跑通后,还有几件小事值得做:
- 删除旧 classic PAT,只保留 fine-grained PAT。
- 保持
permissions: contents: read,不要给 workflow 默认写权限。 - 不要把 token 写进 YAML、脚本或 README。
- 给仓库添加
SECURITY.md,说明漏洞报告入口和处理边界。有这个文件后,GitHub 会在仓库的 Security 标签页显示安全策略入口。
用 Dependabot 管依赖更新
给源码仓库启用 Dependabot alerts,并用 .github/dependabot.yml 固定更新节奏。当前博客的配置覆盖两个生态系统——npm 依赖和 GitHub Actions:
1 | |
groups 把多个依赖更新合并到同一个 PR 里。没有分组时,Dependabot 会为每个包单独开 PR,对博客项目来说噪声太大。Hexo 相关的包归到 hexo-stack 组,其余 minor 和 patch 更新归到另一组。Actions 侧把所有更新归到一个组。两边都固定在周一早上跑,更新 PR 集中出现,不会分散在一周各处。
这些配置不复杂,但会让博客部署从”某台电脑上的一串命令”变成”仓库里可审计、可复制、可回滚的一套交付系统”。
当前博客的最小迁移清单
如果要把这套配置搬到另一个 Hexo 博客,按这个清单改:
| 位置 | 需要替换 |
|---|---|
.github/workflows/deploy.yml |
external_repository |
| GitHub Actions secret | DEPLOY_TOKEN 的值 |
| Pages 仓库设置 | 发布分支和目录 |
_config.yml |
站点 URL、deploy fallback 目标 |
.yarnrc / yarn.lock |
registry 是否可被 GitHub runner 访问 |
真正必须改的通常只有两项:目标仓库和 token。其他配置是稳定性和可维护性加固。
