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 的优势主要有几类:
- 触发条件和 GitHub 事件天然打通,比如
push、pull_request、release、workflow_dispatch。 - Secrets、权限、环境和分支保护都在 GitHub 里管理。
- Marketplace 里有大量可复用 action,常见任务不用从零写脚本。
- 标准 runner 覆盖 Ubuntu、Windows、macOS,适合做跨平台验证。
- 日志、状态检查、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 负责:
- PR 阶段跑测试、类型检查和构建。
- 主分支 push 后发送部署通知。
- npm 包、Docker 镜像、文档这类标准产物的发布。
- 在日志里记录 commit sha、触发人、workflow run URL。
服务器负责:
- 校验 webhook 签名、仓库、分支和 commit sha。
- 串行化部署,避免并发覆盖。
- 拉取代码,安装依赖,执行构建。
- 把产物切换到 Nginx 站点目录。
- 记录部署日志,必要时支持回滚。
- 管理 Node、pnpm、PM2、Nginx、证书和系统权限。
这个分工并不复杂,但边界更合理:GitHub Actions 做“触发器”和“质量门禁”,服务器做“生产环境里的部署执行者”。
总结
从 Travis 迁移到 GitHub Actions 时,我更关注的是 CI/CD 能不能更稳定、更方便。这个判断今天仍然成立:GitHub Actions 是个人项目和开源项目非常好用的自动化入口。
但经历过 npm 发布、Docker 镜像构建和博客部署改造后,我对它的边界更清楚了。
适合放在 Actions 里的,是和仓库强相关、无状态、可重复、失败后容易重跑的任务。比如测试、构建、发包、镜像构建、文档生成和部署通知。
不适合完全压在 Actions 上的,是依赖生产服务器状态、需要大量跨境传输、涉及长期权限和运维细节的任务。对这些场景,Actions 更适合作为触发器,而不是执行环境本身。
一句话总结:把 GitHub Actions 当自动化入口,不要把它当唯一的部署机器。