一、前言
Hexo 自动部署不只是“构建后把 public 复制到服务器”。如果直接覆盖正式目录,上传中断、两个工作流并发或构建产物不完整,都可能让线上网站进入半更新状态。本篇从源码仓库开始,逐步建立依赖锁定、构建验证、临时上传、部署锁、备份激活和缓存排查。
本文中的服务器用户名、地址和目录都是示例。SSH 私钥必须保存到 GitHub Secrets,不要写进仓库、文章或日志。
二、交互演示:一次提交怎样安全到达服务器
三、第一步:分清三个位置
GitHub 仓库 |
不要在服务器正式目录中直接运行 Hexo,也不要把服务器目录当作源码仓库。
四、第二步:准备仓库构建命令
package.json 至少提供:
{ |
提交 package-lock.json,并在本地验证:
npm ci |
npm ci 严格按照锁文件安装;依赖声明与锁文件不一致时直接失败,比自动修改依赖树更适合 CI。
五、第三步:建立构建验证脚本
只检查 hexo generate 退出码不够。可以在 tools/verify-build.mjs 检查:
import { access, readFile } from 'node:fs/promises' |
项目还可以继续验证本地 JS/CSS 引用、空白页面、关键自定义模块和构建日志中的 ERROR、FATAL。
六、第四步:创建 GitHub Actions 工作流
新建 .github/workflows/deploy.yml:
name: Build and deploy Hexo |
concurrency 防止两个提交同时激活不同版本,但服务器端仍应有部署锁,避免手动发布与 Actions 冲突。
七、第五步:把 SSH 信息放进 Secrets
仓库 Settings → Secrets and variables → Actions 中添加:
DEPLOY_HOST 服务器地址 |
服务器公钥验证建议使用 known_hosts,不要长期关闭主机密钥检查:
- name: Prepare SSH |
部署用户只授予目标目录和激活脚本所需权限,不要直接使用 root 私钥。
八、第六步:先上传 staging
生成唯一发布目录:
- name: Upload staging release |
上传过程中,Nginx 继续提供旧版本,访客不会看到不完整文件。
九、第七步:在服务器验证 staging
激活前检查:
test -s "$STAGING/index.html" |
也可以抽查关键自定义页面。任何检查失败都应退出,保留现有正式版本。
十、第八步:使用部署锁
服务器激活脚本开头:
exec 9>/var/lock/blog-deploy.lock |
即使 Actions 已配置并发控制,部署锁仍能覆盖手动执行、重跑任务和其他发布入口。
十一、第九步:备份当前版本
TIMESTAMP="$(date +%Y%m%d-%H%M%S)" |
保留最近若干版本,定期删除更旧的备份,避免磁盘耗尽。删除逻辑要严格限制在 /www/backups/ 内。
十二、第十步:保留宝塔和证书保护文件
正式目录中可能有不属于 Hexo 的文件,例如:
.well-known/ |
激活前先保存,切换后再恢复:
PRESERVE="$(mktemp -d)" |
不要使用没有边界检查的通配符删除正式目录。
十三、第十一步:验证 Nginx 并准备回滚
if ! nginx -t; then |
如果服务器上任意其他虚拟主机配置损坏,nginx -t 也可能失败。此时不要跳过校验,应找出具体 vhost 问题。
十四、第十二步:由 Actions 调用激活脚本
- name: Activate release |
复杂服务器操作集中在服务器脚本中,工作流只负责上传和调用,审查与回滚更清晰。
十五、第十三步:发布后检查线上页面
至少验证:
https://blog.example.com/ |
静态资源带反盗链时,直接访问可能返回假 404,应使用博客页面 Referer 测试。
十六、第十四步:代码更新但线上没变化怎样排查
按层检查:
- Actions 使用的 commit 是否正确;
- 构建日志中是否生成目标文件;
- staging 是否包含新文件;
- 激活脚本是否真正更新 LIVE;
- Nginx root 是否指向该目录;
- HTML 是否引用新的资源版本;
- CDN 和浏览器是否仍命中旧缓存。
不要只检查本地源码,也不要只检查服务器某个同名目录;先确认它是否真是 Nginx 当前 root。
十七、第十五步:处理静态资源缓存
HTML 可以使用较短缓存,带版本的 CSS/JS 使用长期缓存:
<script src="/js/home-banner-carousel.js?v=20260808-1" defer></script> |
修改内容后更新版本号。对于内容哈希文件,文件名变化本身就是缓存失效信号。
十八、第十六步:完整发布验证
本地先运行:
npm ci |
推送后检查:
- 工作流所有步骤通过;
- staging 上传文件数量合理;
- 服务器部署锁正常获取;
- 正式目录保护文件仍存在;
- 最新备份能够找到;
- 线上首页与关键页面是新版本;
- 新 CSS/JS URL 返回正确内容;
- 回滚脚本在测试目录中演练通过。
十九、常见问题
1. npm ci 报锁文件不一致
在本地使用正确 Node 版本执行 npm install 更新锁文件,检查差异后提交。不要在工作流中改用 npm install 掩盖问题。
2. Actions 成功但服务器仍是旧页面
检查激活脚本和 Nginx root,常见原因是 staging 已更新但正式目录未切换,或者查看了另一个同名目录。
3. nginx -t 因其他站点失败
全局配置中任一虚拟主机错误都会导致校验失败。修复对应 vhost,不要删除当前博客的安全校验。
4. 图片直接访问 404,页面中却正常
这是反盗链 Referer 规则的可能表现。以首页 Referer 请求资源再判断,不要据此删除图片。
二十、维护入口与结语
- 构建发布流程:
.github/workflows/deploy.yml - 构建验证:
tools/verify-build.mjs - 服务器激活与回滚:受限的服务器脚本
- Secrets:GitHub 仓库设置
- 缓存版本:站点配置与静态资源 URL
安全发布的核心是把“上传”和“激活”分开:只有完整、验证通过的新版本才有资格接管正式目录;任何阶段失败,都保留当前可用版本并提供清晰回滚点。







