跳到正文
GitHub Actions 不直接 SSH:我把验证与部署拆开了
GitHub Actions 不直接 SSH:我把验证与部署拆开了

GitHub Actions 不直接 SSH:我把验证与部署拆开了

Actions 使用只读权限完成类型检查、构建和自测;VPS 只接受成功 workflow_run 的签名 webhook,并按精确提交发布、健康检查和自动回滚。

速读#

我最初使用 GitHub Actions,只是为了替代“SSH 登录、git pull、重启服务”这串容易漏步骤的手工操作,后来才把 CI 和 CD 真正分开。当前工作流不持有生产 SSH 私钥,也不直接登录 VPS;它用只读仓库权限完成类型检查、自测、普通构建、链接与 SEO 检查,以及带 /astro-narrow/ base 的兼容构建。只有 main 上一次成功的 Verify site 运行,才会通过 GitHub 签名 webhook 把精确提交交给服务器端部署服务。

VPS 端仍然不盲信“CI 绿了”。Webhook 先验证原始请求体签名、事件类型、仓库、分支、工作流名称、触发方式、结论和 head_sha;部署脚本再确认远端 main 仍然等于这个提交,构建独立 release,检查 health.json 中的 revision,原子切换 current,公开健康检查失败就恢复旧版本。自动化的价值不是机器永远正确,而是把同一组可审计的判断稳定执行,并在判断失败时停止。

手动部署真正缺的是状态和证据#

以前改完代码,我会登录服务器拉取、安装依赖、构建或重启。某次代码已经更新,进程却忘了重启,线上仍是旧版本;另一次命令成功执行,我却说不清当前目录究竟对应哪个提交。手动流程的问题不只是慢,而是关键状态存在人的记忆里:哪些检查跑过、哪个版本已上线、失败时改过什么,都没有统一记录。

流水线可以消除“这次忘了一步”,但会稳定重复 YAML 里写错的步骤。测试范围不足、部署脚本指错目录、权限给得过大,自动化只会更快地把错误扩散。因此我现在把它看成一份可执行的发布协议:输入是一个明确提交,过程必须产生日志和验证证据,输出是“该提交已验证”“已部署”或“被拒绝”,不能只剩一个绿色图标。

CI 只负责证明提交满足已知检查#

当前 GitHub Actions 工作流名为 Verify site,在 push、pull request 和手工触发时运行。它显式限制 GITHUB_TOKENcontents: read,同一 ref 的旧运行可以被新提交取消,避免无意义地继续消耗 runner。核心结构可以简化成:

name: Verify site
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: verify-${{ github.ref }}
cancel-in-progress: true

Runner 从干净环境开始,固定 Node 和 pnpm 主版本,使用 pnpm install --frozen-lockfile,然后依次运行类型检查、OAuth 与部署服务自测、构建、链接、CSP、资源、SEO 和浏览器检查。第二次构建设置 ASTRO_BASE=/astro-narrow/,专门防止根路径部署正常、GitHub Pages 项目路径却坏掉。PR 与 main 使用相同验证,但 PR 成功不会因此获得生产部署资格。

本地复现也遵循同一入口,而不是凭记忆挑几条命令:

Terminal window
corepack enable
pnpm install --frozen-lockfile
pnpm run typecheck
pnpm run oauth:self-test
pnpm run deploy:self-test
pnpm build
pnpm run links:self-test
pnpm run seo:self-test

CI 绿色只说明这些已知检查在对应 runner 和提交上通过。它不能证明 Cloudflare、Tunnel、VPS 磁盘、生产权限和外部依赖当前正常,也不能发现没有写进测试的业务错误;这些属于部署门禁和上线后的烟雾检查。

为什么不让 Actions 直接拿 SSH 私钥#

最简单的 CD 示例通常把 VPS 地址、用户名和私钥放进 Actions Secrets,再用第三方 SSH Action 远程执行 docker compose up -d。小项目可以这样做,但它让 GitHub runner 和所用 Action 同时接触长期生产凭据;一旦 workflow、依赖 Action 或日志处理出现问题,攻击者获得的不只是一次部署许可,而可能是服务器登录能力。

当前方案把权限换成更窄的单向通知:GitHub 只向公开 webhook 发送仓库事件,请求使用共享 secret 对原始 body 生成 X-Hub-Signature-256;服务器验证成功后,才由本机低权限部署用户执行固定脚本。Webhook secret 留在 VPS,不进入 Actions;部署服务也不会接受任意 shell 命令,外部只能提交一个经过筛选的 GitHub 事件。

push / pull request
-> GitHub Actions: Verify site
-> 失败:停止
-> 成功 PR:只记录验证结果
-> 成功 main push:GitHub 发送签名 workflow_run
-> VPS webhook 验签与事件过滤
-> 部署精确 head_sha

这不是绝对安全。共享 secret 仍要限制权限、妥善轮换,webhook 要限制请求体大小、去重 delivery 并防重放,本机部署用户也只能写发布目录和运行必要命令。边界的改进在于:CI 不再拥有通用远程登录能力,泄露后的可用动作更少。

服务器为什么还要重新做部署判断#

Webhook 只接受指定仓库、main 分支、名为 Verify site 的工作流、由 push 触发且结论为 success 的 workflow_run。失败、取消、PR、fork、其他工作流和其他分支都会被忽略。它提取成功运行的 head_sha,部署脚本 fetch 后要求远端 main 仍然等于该 SHA;如果后续提交已经推进分支,旧的绿色运行不能覆盖新版本,必须等新提交自己的验证结果。

部署时使用文件锁避免并发切换;如果新验证在部署中到达,服务只保留最新待部署提交。每个 commit 构建到独立 release 目录,产物必须包含首页、healthz 和 revision 精确匹配的 health.json。准备完成后通过临时符号链接原子替换 current,随后从预定 Host 发起健康检查;失败则把链接恢复到上一 release,而不是在已经损坏的目录上继续打补丁。

这种设计会重复一次构建:CI 构建用于验证,VPS 构建用于生成本机实际发布产物。它增加了时间和对包仓库的依赖,但省去了跨环境传输 artifact 的另一套信任与清理逻辑,也能确认服务器自己的 Node、权限和磁盘路径可用。规模扩大或构建变重后,可以改为签名 artifact 交付;当前体量下,重复构建仍是我能理解和维护的较小方案。

Nginx、systemd 服务代码和其他维护级文件不会因为普通内容提交就无条件自动替换。高风险变更先被 staging,再由管理员复制、校验配置和重启相应服务;这牺牲完全自动化,换来“修改发布系统本身时不能由旧发布系统盲目覆盖”的边界。

Pipeline 红了以后怎样查#

第一步不是点击 Re-run,而是确认失败对应哪个 commit、哪个 job、哪一个 step,以及日志中的第一个因果错误。后续堆栈常常只是前一步失败的连锁反应。依赖安装失败要看 lockfile、registry 和运行时版本,构建失败要在干净工作区复现,浏览器检查失败则保存页面、控制台和网络证据,不把所有红灯都归为“Actions 抽风”。

同一提交重跑后变绿,说明测试或依赖存在不稳定因素,并不等于问题消失。应记录失败频率,判断是网络下载、时间、并发、外部 API 还是测试隔离;能缓存的依赖缓存,必须访问外部服务的测试设置明确超时和替身,真正偶发的平台故障才有限次重试。无限重跑会把 CI 从质量门禁变成概率抽奖。

还要区分“CI 失败”和“部署失败”。CI 通过但线上仍旧,检查 webhook 是否收到事件、事件是否因分支或 SHA 过期被拒绝、部署服务日志和 current 指向;health.json 的 revision 可以直接回答公网正在服务哪个提交。没有版本证据时,“我刚刚应该部署过”仍然只是记忆。

Secrets 只是最低线,不是完整密钥策略#

GitHub Secrets 比把 token 写进 YAML 安全,但日志掩码不是数据防泄漏系统。字符串被拆分、编码、写入 artifact、传给不受信 Action 或嵌入错误消息,都可能绕过自动打码。工作流应避免打印环境,Token 按最小权限和最短有效期配置;能使用 OIDC 换取短期云凭据时,不必长期保存静态密钥。当前站点不需要在 Actions 中保存生产 SSH 私钥,因此直接不提供比依赖掩码更可靠。

第三方 Action 本身也是供应链依赖。uses: owner/action@v7 便于自动接收同一主版本更新,但标签可以移动;高保证场景应固定完整 commit SHA,再用 Dependabot 或人工审查更新。无论选择便利还是不可变引用,都要显式知道自己允许谁在 runner 上执行代码,而不是把官方市场页面当成永久信任证明。

什么项目值得自动化#

我不再使用“改过三次以上才值得 CI/CD”这种固定门槛。一次性但会删除数据的迁移,也值得写成可审查脚本;长期维护但每次只发布一个静态文件的项目,可能只需要最小构建检查。判断标准是步骤是否重复、遗漏代价、需要多少协作记录,以及自动化本身的维护成本。

对这个站点,类型检查、两种 base 构建、链接与安全头检查已经反复帮我挡住回归,自动发布也通过精确 SHA、健康 revision 和回滚降低了记忆负担,所以值得保留。它没有把人移出流程:人仍然决定测试覆盖、权限边界和高风险维护变更;机器负责稳定执行已经写清楚的规则,并在规则不满足时拒绝上线。

版权许可

CC BY-NC-SA 4.0 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

相关文章

s1oopX

登录 s1oopX