GitHub Actions 适合做什么,不适合做什么

从 Travis 迁移、npm 自动发布、Docker 镜像构建和博客 webhook 部署改造出发,重新梳理 GitHub Actions 的使用边界。

我最早用 CI/CD,是在 GitHub 开源项目里接入 Travis。后来 Travis 稳定性和免费策略都不再适合个人项目,GitHub Actions 又和 GitHub 仓库天然集成,于是博客、文档和开源项目陆续迁了过去。

那时我的判断很简单:代码在 GitHub,自动化也放在 GitHub,提交后自动测试、构建、发布,体验非常顺。

几年后再看,这个判断只对了一半。

GitHub Actions 很适合做“围绕仓库的一次性自动化任务”:测试、构建、发布 npm 包、构建 Docker 镜像、生成文档、通知外部系统。它不一定适合直接完成所有部署,尤其是目标服务器在国内、需要传输大量静态文件、依赖服务器本地状态或需要更清晰运维边界时。

这篇文章把几段实践放在一起复盘:

  • 从 Travis 迁移到 GitHub Actions。
  • 用 Actions 自动发布 npm 包。
  • 在 Actions 里构建大型 Docker 镜像。
  • 把博客部署从“Actions 直接部署”改成“Actions 通知服务器,服务器本地拉取、构建、发布”。

结论先放前面:GitHub Actions 是很好的自动化入口,但不应该默认成为生产服务器的执行环境。

从 Travis 到 GitHub Actions:CI 最适合放在仓库旁边

早期用 Travis 的原因很简单:开源项目托管在 GitHub,Travis 接入方便,写一个 .travis.yml 就能在每次提交后跑测试和构建。

但 Travis 最大的问题是稳定性和生态集成。CI 结果在另一个系统里,权限、日志、触发条件、缓存和部署都要跨系统理解。后来 GitHub Actions 成熟后,把 CI 放回 GitHub 仓库附近就很自然。

Actions 的优势主要有几类:

  1. 触发条件和 GitHub 事件天然打通,比如 pushpull_requestreleaseworkflow_dispatch
  2. Secrets、权限、环境和分支保护都在 GitHub 里管理。
  3. Marketplace 里有大量可复用 action,常见任务不用从零写脚本。
  4. 标准 runner 覆盖 Ubuntu、Windows、macOS,适合做跨平台验证。
  5. 日志、状态检查、PR 门禁都和代码审查流程在一起。

以前在 Mpx 模板项目里,我就用过 matrix 同时覆盖多个操作系统和 Node 版本:

strategy:
  matrix:
    os: [macos-latest, windows-latest, ubuntu-latest]
    node: [10, 12, 14]

这种事情放在本机很难做,放在云端 runner 非常合适。模板项目最怕的是“生成出来的项目不能跑”,而 Actions 可以在每次提交后把不同平台、不同 Node 版本的基础构建都跑一遍。

所以,CI 是 GitHub Actions 最稳的基本盘:只要任务是无状态的、可重复的、和仓库代码强相关的,就很适合放在 Actions 里。

适合场景一:测试、Lint 和构建

这是最没有争议的场景。

name: test

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: pnpm
      - run: corepack enable
      - run: pnpm install --frozen-lockfile
      - run: pnpm run lint
      - run: pnpm test
      - run: pnpm run build

这类任务有几个共同点:

  • 输入是仓库代码和 lockfile。
  • 输出是测试结果或构建产物。
  • 失败可以阻止合并。
  • 不依赖生产服务器状态。
  • 可以安全地重复执行。

在这个边界里,Actions 的价值很明确:让质量门禁自动化,不靠人记得执行命令。

适合场景二:发布 npm 包

npm 包发布也适合放在 Actions 里,因为它本质上是“仓库状态 -> registry 版本”的自动化。

比较稳的触发方式是 tag 发布:

name: publish

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org/
      - run: corepack enable
      - run: pnpm install --frozen-lockfile
      - run: pnpm test
      - run: pnpm run build
      - run: npm publish --provenance --access public

这里有两个关键点。

第一,发布前必须跑测试和构建。发包不是单纯执行 npm publish,而是把“可发布”的判断写进流程。

第二,今天更应该优先考虑 npm trusted publishing,也就是用 OIDC 建立 GitHub Actions 和 npm 之间的信任关系,减少长期 npm token 的暴露面。如果项目暂时还没配置 trusted publishing,再用 npm automation token 作为过渡方案。

发布 npm 包适合放在 Actions 里,是因为 Actions 能把版本、tag、构建、测试、发布日志连在一起。它的执行环境虽然是临时的,但发布任务本来也应该是临时的。

适合场景三:构建并推送 Docker 镜像

Docker 镜像构建也经常适合放在 Actions 里,尤其是镜像需要推到 Docker Hub、GitHub Container Registry 或其他镜像仓库时。

典型流程是:

name: docker

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: your-name/your-image:latest

这个场景里,Actions 的好处也很明显:

  • 构建环境干净。
  • 可以结合 tag 或 commit sha 标记镜像。
  • 可以把镜像推到 registry,让服务器只负责拉镜像和运行。
  • 不需要每个开发者本机都有完整 Docker 构建环境。

但 Docker 镜像构建会碰到 runner 资源边界。普通 Web 服务镜像通常没问题;如果是 Stable Diffusion 这类 AI 镜像,Python、CUDA、PyTorch 和模型相关依赖会迅速吃掉磁盘空间。以前我构建过 A1111/Stable-Diffusion-WebUI 相关镜像,需要先清理 runner 上不需要的工具和缓存,才能勉强构建成功。

这种技巧有用,但不是无限扩展方案。镜像继续变大后,更合理的选择是:

  • 优化 Dockerfile,减少层和无用依赖。
  • 使用 BuildKit cache 或 registry cache。
  • 使用 GitHub larger runners。
  • 使用 self-hosted runner。
  • 把构建放到更靠近目标环境的机器或专用构建服务里。

所以 Docker 构建适合 Actions,但前提是资源规模仍在 runner 能承受的范围内。超过这个范围,就不应该继续靠清理磁盘硬撑。

不适合场景一:把国内服务器部署完全压在 Actions 上

博客部署改造,就是 GitHub Actions 使用边界的一个典型案例。

之前的部署思路是:GitHub Actions 负责构建博客,然后把生成的静态文件同步到服务器。这个方案简单直观,早期也能工作。

问题是,Actions 的 runner 在海外,目标服务器在国内。构建本身不慢,慢的是把文件从 runner 传到服务器。一次部署接近 4 分钟,其中很多时间并没有花在业务逻辑上,而是花在网络传输和远程同步上。

这类场景有几个隐患:

  • runner 到服务器的网络不可控。
  • 静态文件越多,传输越容易成为瓶颈。
  • SSH key 或部署 token 要放在 GitHub Secrets 里。
  • 部署失败时,需要同时查 Actions 日志和服务器状态。
  • 服务器本地环境、Nginx、证书、进程管理都不是 Actions 真正能掌控的东西。

所以现在博客部署改成了另一种结构:

git push
  -> GitHub Actions
  -> 发送带签名的 webhook
  -> 目标服务器上的 NestJS 接收通知
  -> 服务器本地 git pull / pnpm install / build
  -> 同步到 Nginx 站点目录

Actions 现在只负责一件很轻的事:通知。

当前 workflow 的核心大概是这样:

name: deploy

on:
  push:
    branches:
      - master
  workflow_dispatch:

concurrency:
  group: production-deploy
  cancel-in-progress: true

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - name: Notify deployment server
        env:
          DEPLOY_WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }}
          DEPLOY_WEBHOOK_SECRET: ${{ secrets.DEPLOY_WEBHOOK_SECRET }}
          REPOSITORY: ${{ github.repository }}
          REF: ${{ github.ref }}
          SHA: ${{ github.sha }}
        run: |
          payload="$(jq -cn \
            --arg repository "$REPOSITORY" \
            --arg ref "$REF" \
            --arg sha "$SHA" \
            '{ repository: $repository, ref: $ref, sha: $sha }')"

          signature="sha256=$(printf '%s' "$payload" \
            | openssl dgst -sha256 -hmac "$DEPLOY_WEBHOOK_SECRET" -binary \
            | xxd -p -c 256)"

          curl --fail-with-body --request POST "$DEPLOY_WEBHOOK_URL" \
            --header "Content-Type: application/json" \
            --header "X-Lihuanyu-Signature-256: $signature" \
            --data "$payload"

这个方案的变化点在于:Actions 不再搬运产物,只发送一个可验证的部署请求。真正的部署发生在目标服务器上。

这样做的收益很直接:

  • GitHub Actions 执行时间变短。
  • 不再从海外 runner 向国内服务器传大量文件。
  • 服务器可以复用本地 Git、pnpm 缓存和构建环境。
  • 部署日志集中在目标服务器,和 Nginx、PM2、证书状态更接近。
  • GitHub Secrets 里不需要保存服务器 SSH 私钥,只保存 webhook URL 和签名密钥。
  • webhook 可以校验仓库、分支、commit sha 和 HMAC 签名,避免被随便触发。

代价也要承认:

  • 服务器上要维护 Node、pnpm、Git、构建脚本和部署目录权限。
  • webhook 服务要有鉴权、锁、日志和错误处理。
  • 首次 clone 或 GitHub 网络波动时,服务器拉代码也可能慢。
  • 部署过程需要处理并发推送,避免两个部署互相覆盖。
  • 回滚要在服务器部署脚本里设计,而不是只看 Actions。

但这些代价属于部署系统本来就应该处理的问题。把它们放在目标服务器上,反而更接近真实运行环境。

不适合场景二:需要长期状态的运维动作

GitHub-hosted runner 是临时环境。GitHub 官方文档也把 hosted runner 描述为执行 workflow job 的机器,通常每次任务都是新环境。

这意味着它不适合承担长期状态:

  • 不适合保存构建缓存以外的重要数据。
  • 不适合做依赖本机状态的运维任务。
  • 不适合在 runner 上做需要人工持续观察的操作。
  • 不适合把数据库迁移、服务重启、证书更新等全部交给一段远程脚本硬跑。

数据库迁移、Nginx reload、PM2 reload、证书续期、静态目录切换,这些都可以被 CI/CD 触发,但最好由目标环境里的部署脚本负责执行,并且有清晰日志和失败处理。

Actions 可以发起部署,不一定要亲自执行部署。

不适合场景三:把 Secrets 暴露给不可信代码

只要涉及发布、部署、镜像推送,就会涉及 secrets。

常见风险是:workflow 既能跑不可信代码,又能拿到高权限 secrets。比如来自 fork 的 PR、动态下载的 action、没有固定版本的第三方 action、过宽的 GITHUB_TOKEN 权限,都可能扩大风险。

我的默认策略是:

  • permissions 显式写最小权限。
  • 发布任务只在 tag、release 或受保护分支上触发。
  • 部署任务只接受主分支或手动触发。
  • 第三方 action 固定到明确版本,关键场景可进一步固定到 commit sha。
  • npm 发布优先使用 OIDC trusted publishing,减少长期 token。
  • 服务器部署优先用 webhook 签名,不把 SSH 私钥直接交给所有 workflow。

Actions 很适合自动化,但自动化的本质是“稳定地重复执行”。如果权限边界没想清楚,它也会稳定地重复放大错误。

一张简单判断表

场景 是否适合 GitHub Actions 判断
Lint、单元测试、类型检查 适合 无状态、可重复、直接服务代码审查。
多系统、多 Node 版本矩阵测试 适合 runner 平台丰富,本机很难覆盖。
静态站点构建 适合 构建产物明确,失败成本低。
npm 包发布 适合 tag、测试、构建、发布可以形成闭环。
Docker 镜像构建并推送 registry 通常适合 镜像太大时要考虑 larger runner、自托管 runner 或专用构建环境。
部署到 GitHub Pages / Cloudflare Pages 适合 平台离 Actions 近,流程标准化。
向国内服务器传大量文件 不太适合 网络链路和传输耗时容易成为瓶颈。
生产服务器上的本地构建与目录切换 更适合放服务器 更接近真实环境,日志和权限更清楚。
数据库迁移和服务重启 谨慎 可以由 Actions 触发,但执行逻辑应有锁、回滚和日志。
需要固定 IP 或内网访问的任务 看情况 larger runner、自托管 runner 或服务器本地执行更合适。

我现在会怎么设计个人项目部署

如果是个人博客、管理后台、小型服务,我现在会按下面的方式分工。

GitHub Actions 负责:

  1. PR 阶段跑测试、类型检查和构建。
  2. 主分支 push 后发送部署通知。
  3. npm 包、Docker 镜像、文档这类标准产物的发布。
  4. 在日志里记录 commit sha、触发人、workflow run URL。

服务器负责:

  1. 校验 webhook 签名、仓库、分支和 commit sha。
  2. 串行化部署,避免并发覆盖。
  3. 拉取代码,安装依赖,执行构建。
  4. 把产物切换到 Nginx 站点目录。
  5. 记录部署日志,必要时支持回滚。
  6. 管理 Node、pnpm、PM2、Nginx、证书和系统权限。

这个分工并不复杂,但边界更合理:GitHub Actions 做“触发器”和“质量门禁”,服务器做“生产环境里的部署执行者”。

总结

从 Travis 迁移到 GitHub Actions 时,我更关注的是 CI/CD 能不能更稳定、更方便。这个判断今天仍然成立:GitHub Actions 是个人项目和开源项目非常好用的自动化入口。

但经历过 npm 发布、Docker 镜像构建和博客部署改造后,我对它的边界更清楚了。

适合放在 Actions 里的,是和仓库强相关、无状态、可重复、失败后容易重跑的任务。比如测试、构建、发包、镜像构建、文档生成和部署通知。

不适合完全压在 Actions 上的,是依赖生产服务器状态、需要大量跨境传输、涉及长期权限和运维细节的任务。对这些场景,Actions 更适合作为触发器,而不是执行环境本身。

一句话总结:把 GitHub Actions 当自动化入口,不要把它当唯一的部署机器。

扩展阅读