以前改完个人网站,我总要再想一遍:现在服务器上跑的是哪个提交?我有没有忘记重启?页面能打开,是新版本真的到了,还是旧容器仍在回答?这类问题一次两次靠记性可以,改得频繁以后就不行了。

我想要的不是“推一下代码就自动执行一串命令”,而是一个更窄的承诺:合并过的主线提交,经过检查,才能成为线上正在运行的版本;失败要停住,已经换上去又坏了要能退回。

先分清 GitHub Actions 和 Runner#

GitHub Actions 是安排工作流的地方。仓库里的 .github/workflows/*.yml 告诉 GitHub:发生什么事件、启动哪个 job、每一步做什么。Runner 才是真正执行命令的机器。用 GitHub 托管 Runner,是借一台临时机器;用自托管 Runner,是让我自己的 Mac 接活。runs-on 决定这份工作排给谁。

我的网站仓库是私有的,CI 和发布使用一台只分配给这个仓库的自托管 Mac。机器睡着了,任务就在队列里等;它不会神奇地跑到另一台机器上。相应地,这台 Mac 能碰到的本机文件和网络资源,也是每个工作流需要认真划的边界。注册入口在仓库 Settings → Actions → Runners,GitHub 会给出与当前系统和架构对应的安装命令;注册令牌是短时有效的,不应该复制进文章或仓库。GitHub 的自托管 Runner 安装说明把这一步写得很清楚。

最小的验证工作流可以先只问机器是谁,不要一上来就给它部署密钥:

name: Runner smoke test
on:
  workflow_dispatch:
jobs:
  identify:
    runs-on: [self-hosted, macOS, ARM64, owen-site-ci]
    steps:
      - run: uname -m && sw_vers -productVersion

在 Actions 页面手动运行它,看到 job 被指定的 Runner 接走、命令成功,才算“接入”完成。机器名、登录账号、注册令牌都不必出现在公开文章里。

我的官网实际走了五步#

一个主线提交,走到官网的五道门 PR 合入 main 主线 CI 通过 构建镜像 按 digest 部署 公网健康检查 任何一步失败,发布停止;线上检查失败,部署入口恢复旧容器。
按个人网站实际发布工作流重绘;图里省略了仓库凭据、服务器地址和受限 SSH 入口的细节。

第一步,PR 的检查通过后才合进 main。第二步,main 上再跑一次 CI,测试和网站构建都针对合并后的提交。第三步,发布工作流只接受这次成功的主线 CI,确认它仍是 main 的最新提交,而且能关联到已合并的 PR。第四步,构建 linux/amd64 镜像,推到 GHCR,用不可变的镜像 digest 标识真正要部署的东西。第五步,服务器替换容器,检查首页、笔记页和产品页;公网检查失败就请求回滚。GitHub 的部署环境文档说明了环境如何限制分支和密钥访问,GHCR 文档解释了为何可以按 digest 拉取镜像。

发布工作流的入口可以先缩成下面这几行看,重点是只接主线成功的 CI。这只是触发条件,不包含服务器的受限部署脚本:

on:
  workflow_run:
    workflows: [CI]
    types: [completed]
    branches: [main]
jobs:
  publish:
    if: >-
      github.event.workflow_run.conclusion == 'success' &&
      github.event.workflow_run.event == 'push' &&
      github.event.workflow_run.head_branch == 'main'
    runs-on: [self-hosted, owen-site-ci]

网站内容另有一个仓库,文章合入后独立同步到服务器,站点容器只读地挂载它。这样改一篇笔记不用重新构建整站镜像;改页面代码则必须经过镜像发布。两条链在官网汇合,但各有自己的部署记录。

真正搭时,我踩的不是 YAML 缩进#

第一次把这条链跑起来,发布 job 先报了 Resource not accessible by integration (HTTP 403)。不是服务器坏了,而是用于检查“这个提交属于已合并 PR”的 GITHUB_TOKEN 权限不够。补上读取 PR 的最小权限后,下一次又卡在 macOS 钥匙串:无人值守的 Runner 想保存 GHCR 登录信息,系统回答 User interaction is not allowed (-25308)。发布 job 不能等一个人半夜去点解锁。我把 Docker 登录配置放进每次 job 的临时目录,用完删除,才避开交互式钥匙串。

这两次失败很像小事,却提醒我:Runner 是一台真实的 Mac,不是 GitHub 页面上那个绿色圆点。它会有自己的登录会话、钥匙串、Docker daemon、磁盘和休眠状态。排障时我先看失败落在哪一步,再看它在谁的机器上运行,而不是直接 SSH 到服务器乱改。私有仓库的发布记录里,35848100857 卡在权限,35848682424 卡在钥匙串,35849300479 才成功走到线上;编号留在这里,方便我自己回查,不把失败讲成一次顺利的配置。

如果你也想从手动上线迁到 Actions,我建议按这个顺序做:先用不带密钥的 smoke job 验证 Runner;再让 CI 在主线上构建成功;然后接入只针对主线成功结果的发布 job;最后加入服务器健康检查和回滚。不要把部署凭据放进会执行陌生 PR 代码的 job。GitHub 特别提醒,workflow_run 可以拿到前一个工作流没有的密钥,若把不可信代码或产物直接交给它执行,会越过原本的信任边界。事件触发文档有这条警告。

对我来说,自动部署最重要的结果不是少敲一次命令,而是能回答一个很具体的问题:官网现在跑的提交,究竟是哪一个,谁检查过它,坏了还能不能退。