静态网站的自动化编译与部署
这个网站是一个基于 Hexo 7.3 和 Butterfly 主题的静态博客。文章、配置和主题定制保存在 Git 仓库中,提交到 main 分支后,由 GitHub Actions 自动完成依赖安装、站点生成、图片压缩和服务器部署。
本文记录当前项目实际使用的自动化流程。文中的域名、服务器地址、登录用户、部署目录、验证文件名和私钥均不展示真实值,而是通过 GitHub Secrets 或 Variables 注入。
一、整体流程
一次部署依次经过以下阶段:
1 | 提交并推送到 main |
项目中与这套流程有关的文件如下:
1 | project_web/ |
二、为什么要自动化
静态网站的部署看起来只是“生成文件再上传”,但手工操作很容易出现几个问题:
- 本地 Node.js 或依赖版本不同,导致构建结果不一致;
- 忘记上传某个文件,线上内容处于新旧混合状态;
- 每次都重新压缩全部图片,部署时间越来越长;
- Hexo 读取文件系统时间后,文章更新时间可能被检出操作改写;
- 私钥、服务器地址或站点路径被直接写进仓库。
把这些步骤固定在 CI/CD(持续集成与持续部署)工作流中,可以让每次发布使用相同的环境和命令,也能把敏感配置留在仓库之外。
三、准备服务器和 GitHub Environment
1. 创建专用部署用户
服务器应使用权限受限的部署用户,不要让流水线直接以 root 身份登录。部署用户只需要拥有站点目录的读写权限,以及通过 SSH 登录的权限。
在可信设备上为流水线生成一对独立的密钥:
1 | ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./github_actions_deploy |
把公钥 github_actions_deploy.pub 添加到部署用户的 ~/.ssh/authorized_keys,把私钥 github_actions_deploy 的完整内容保存到 GitHub Environment Secret。私钥不能提交到仓库,也不要在日志中输出。
2. 配置 Environment
当前工作流使用名为 secrets 的 GitHub Environment。建议在该 Environment 中配置以下内容:
| 类型 | 名称 | 作用 | 示例值 |
|---|---|---|---|
| Secret | SERVER_HOST |
服务器主机名或 IP | <SERVER_HOST> |
| Secret | SERVER_USER |
SSH 部署用户 | <DEPLOY_USER> |
| Secret | SSH_PRIVATE_KEY |
部署专用私钥全文 | <PRIVATE_KEY_CONTENT> |
| Variable | SERVER_DEPLOY_PATH |
服务器站点绝对路径 | <DEPLOY_DIRECTORY> |
表格中的尖括号内容只是占位符,不能原样用于部署。GITHUB_TOKEN 不需要手工创建,它由 GitHub Actions 为每次运行临时生成。本项目只用它在构建阶段读取公开的 GitHub 贡献数据,因此工作流将仓库权限限制为 contents: read。
如果 Environment 配置了审批规则,生产部署会在执行前等待批准,这也能避免一次误推送立即覆盖线上站点。
四、固定构建环境
项目在 blog/package.json 中声明 Node.js 24.18.1、npm 11.16.0,工作流同时固定 Node.js 版本,并通过 package-lock.json 恢复依赖:
1 | cd blog |
这里使用 npm ci 而不是 npm install,原因是它严格按照锁文件安装,更适合可复现构建。--legacy-peer-deps 用于兼容当前 Hexo 插件的依赖关系,--no-audit --no-fund 可以减少与构建无关的网络请求和日志。
常用脚本如下:
1 | { |
本地只想检查内容时,可以执行:
1 | cd blog |
npm run clean 会删除 public/ 和 Hexo 数据库缓存,不应把它当作普通检查命令随意执行。CI 中的工作区是一次性的,因此可以在构建前清理这些可再生成内容。
五、恢复文章的文件时间
Git 记录提交历史,但不会保存文件系统的修改时间。Actions 检出仓库后,许多文件会得到接近当前时间的时间戳,可能影响主题显示的文章更新时间。
本项目的 run.sh 会查询每个 blog/source 文件最近一次提交的 Unix 时间戳,再使用 GNU touch 写回文件时间:
1 |
|
因为脚本需要查询完整提交历史,actions/checkout 必须设置 fetch-depth: '0'。如果使用默认的浅克隆,较早文章的最后修改提交可能不在本地,恢复结果就不可靠。
六、使用 Sharp 增量优化图片
Hexo 生成完成后,tools/optimize-images.js 会递归扫描 blog/public/ 中的 .jpg、.jpeg 和 .png 文件。当前处理策略为:
- 图片最长边限制在
1920 × 1080范围内,并禁止放大小图; - JPEG 使用质量
80和 MozJPEG 编码; - PNG 使用压缩级别
9; - 如果压缩后的文件反而更大,则保留原文件;
- 以“图片内容、扩展名和压缩配置”共同计算 SHA-256 缓存键;
- 相同内容只处理一次,之后直接复用缓存结果;
- 默认并发数不超过
2,降低 CI 中的瞬时内存压力。
缓存目录是 blog/.cache/sharp-images/v1。工作流先恢复旧缓存,只在出现新缓存内容时保存新版本,避免每次部署都上传完全相同的缓存。
本地修改图片后,可以先演练:
1 | cd blog |
演练模式只输出扫描数量、缓存命中数和预计压缩体积,不改写图片。Windows PowerShell 中可以这样设置一次性环境变量:
1 | $env:IMAGE_DRY_RUN='true' |
GIF 和 SVG 不在当前 Sharp 脚本的处理范围内。工作流仍保留旧 Gulp 图片流程作为紧急回退,但默认关闭,也不会把 Gulp 作为项目直接依赖安装。
七、完整的脱敏工作流
下面的示例保留了本项目的关键步骤,同时将所有环境相关信息改成 Secrets 或 Variables。真实站点的域名、服务器地址、用户、目录、验证文件名和密钥都不应出现在仓库中。
1 | name: Build and deploy Hexo site |
strip_components: 3 会去掉 repo/blog/public 这三级目录,只把 public/ 内的内容放进站点目录。缺少这个参数时,服务器上可能出现多余的 repo/blog/public/ 嵌套目录。
当前部署方式会先清理旧文件,再上传新文件,优点是不会残留已经从源码删除的页面;缺点是上传期间可能有短暂空窗。如果网站对连续可用性要求较高,可以进一步改成“上传到版本目录 → 校验 → 原子切换软链接 → 保留上一版本”的发布方式。
站点验证文件、robots.txt 等额外文件也应在构建阶段复制到 public/,但不要在公开文章或工作流中写入真实验证码文件名。更简单的做法是把不含敏感信息的固定静态文件放在 Hexo 的 source/ 下,让构建过程自动复制。
八、部署后的检查
工作流成功不等于网站一定正常。至少还需要检查:
- Actions 中安装、构建、压图、清理、上传和验证步骤均成功;
- 首页及一篇新文章可以访问,CSS、JavaScript 和图片没有出现
404; - 浏览器控制台没有资源路径或脚本错误;
- 服务器上的
index.html非空,文件属主和 Web 服务读取权限正确; - 图片优化日志中的输入、输出体积合理,没有意外放大文件;
- 修改旧文章后,页面显示的更新时间与 Git 历史一致。
如果部署失败,可以按阶段定位:
| 现象 | 优先检查 |
|---|---|
npm ci 失败 |
Node.js 版本、锁文件是否同步、对等依赖参数 |
| 文章时间异常 | fetch-depth 是否为 0、run.sh 是否有执行权限 |
| 图片步骤失败 | public/ 是否已生成、Sharp 是否正确安装、CI 内存是否充足 |
| SSH 连接失败 | Environment 是否正确、主机与用户是否匹配、公钥是否已授权 |
| 上传成功但页面 404 | SERVER_DEPLOY_PATH、strip_components 和 Web 根目录配置 |
| 新页面存在但旧页面也残留 | 清理步骤是否成功执行、部署用户是否有目录写权限 |
九、安全要点
- 工作流只保存 Secret 的名称,不保存任何真实值;
- 为部署单独创建 SSH 密钥,不复用个人日常登录密钥;
- 使用权限受限的部署用户,并把写权限限制在站点目录;
- 不在命令中输出私钥、令牌或带凭据的 URL;
- 对部署目录做非空和非根目录检查,避免变量配置错误扩大清理范围;
- GitHub Actions 和第三方 Action 使用明确版本,升级前先阅读变更说明;
- 定期轮换部署密钥,成员或服务器发生变化时及时撤销旧公钥;
- 生产 Environment 可以设置审批和分支保护,只允许受保护分支部署。
完成这些配置后,日常发布只需要提交文章并推送到 main。构建环境、图片优化和文件上传都由流水线统一执行,服务器上也不需要保留完整源码或安装 Node.js。