GitHub Actions CI/CD 流水线从零搭建:让每次推送都自动跑起来
你肯定遇到过这种情况:代码在本地跑得好好的,推到仓库之后同事拉下来却报错;或者上线前一天才发现某个环境变量没配。CI/CD 要解决的,正是这类”只在某台机器上能用”的玄学问题。本文用一个真实可跑的流水线,讲清楚 GitHub Actions 的核心概念和常见坑。
为什么需要 CI/CD
先说结论:CI/CD 不是大厂专属的”高级货”,哪怕你维护一个几百 star 的个人开源项目,甚至一个只有自己在用的玩具仓库,它都能帮你省下大量重复劳动。
所谓 CI(持续集成)和 CD(持续交付/部署),拆开理解其实很朴素:
- CI:每次提交代码,自动跑一遍测试、lint、类型检查、构建,保证主干永远是健康的。
- CD:CI 通过之后,自动把产物部署到服务器或云平台。
没有 CI 的时候,这些步骤全靠人肉:手动 npm install、手动 npm test、手动打包上传服务器。一旦项目变大、协作者变多,漏掉一步就是一场事故。
GitHub Actions 是 GitHub 内建的 CI/CD 方案,对公共仓库免费,私有仓库每月也有免费额度。它最吸引人的地方在于:配置就是仓库里的一个 YAML 文件,不用自己搭 Jenkins 服务器,也不用维护 Runner。
一个小而完整的流水线
先看一个 Node.js 项目最基础的配置,感受一下结构。在仓库根目录创建 .github/workflows/ci.yml:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
这个文件做了四件事:拉代码、装 Node、装依赖、跑测试。第一次看到的人可能一脸懵,我们逐行拆开。
on 决定什么时候触发。 上面的意思是:向 main 分支 push,或者有人向 main 发起 pull request 时触发。你也可以改成 on: push(任何分支都触发),或用 schedule 做定时任务。
jobs 是执行单元。 一个 workflow 可以有多个 job,每个 job 跑在一台全新的虚拟机上,互不干扰。这里的 test 是 job 的名字,可以随便起。
runs-on 指定运行环境。 ubuntu-latest 是最常用的 Linux 环境,另外还有 windows-latest 和 macos-latest。注意这里的环境是全新的,没有任何你本地装过的东西。
steps 是一系列有序动作。 每个 step 要么用 run 直接执行 shell 命令,要么用 uses 复用别人写好的 Action。
uses 到底用了什么
actions/checkout@v4 和 actions/setup-node@v4 这种写法,是引用 GitHub 官方维护的 Action。checkout 负责把代码拉到虚拟机上,setup-node 负责安装 Node.js 并设置版本。
这里的 @v4 是版本号,强烈建议固定主版本,避免上游偷偷发布破坏性更新导致你的流水线突然挂掉。
with 用来给 Action 传参。node-version: 20 指定 Node 版本,cache: 'npm' 让 setup-node 自动缓存依赖,下次跑能省下不少时间。
如果你需要把自己的脚本也复用成一个 Action,本质上就是把一段 run 命令封装成独立的仓库,这里不展开,先会用官方的就行。
一个会出错的真实例子
光会跑测试还不够,我们加一个 lint 步骤,看看错误信息长什么样:
permissions:
contents: read
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm test
permissions: contents: read 这一行容易被忽略,它给 workflow 显式声明了只读权限,是 GitHub 官方推荐的最小权限实践,能降低 token 泄露后的风险。
当 lint 失败时,pipeline 会在对应 step 处停下来,并把错误日志完整展示在 Actions 页面里。这里有个常踩的坑:本地 npm install 生成的 lockfile 和线上不一致。npm ci 会严格按照 package-lock.json 安装,如果锁文件没提交,或者提交了但有人手动改过 node_modules,流水线就会报错。解决方式很简单——永远提交 lockfile,且不要手动改动依赖。
缓存机制:别让流水线每次都从头来
流水线每次都是全新环境,如果每次都重新下载依赖、重新构建,跑一次可能要几分钟甚至更久。GitHub Actions 提供了 actions/cache 来复用中间产物。
Node 项目用 setup-node 的 cache: 'npm' 已经自动处理了依赖缓存,但如果你用的是 Rust、Go 之类的语言,或者有自定义构建产物,可以手动配置:
- name: Cache build artifacts
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
key 是缓存的唯一标识,hashFiles 会根据 Cargo.lock 内容生成 hash,锁文件一改,缓存 key 就变,自动失效旧的缓存。restore-keys 是回退匹配,key 完全匹配不上时,用前缀匹配最近的缓存。
这里有个容易踩的坑:缓存不是全能的,restore-keys 匹配到的旧缓存可能是脏的。所以缓存 key 最好设计成”内容相关”而非”时间相关”,避免拿到过期的构建产物。
部署:CI 通过后自动上线
测试和 lint 都绿了,就可以考虑自动部署。以部署到 VPS 为例,用一个单独的 job 依赖测试 job:
jobs:
test:
runs-on: ubuntu-latest
outputs:
should-deploy: ${{ steps.check.outputs.result }}
steps:
# ... 测试步骤省略
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm ci --omit=dev
pm2 restart myapp
几个关键点:
needs: test让deploy在test成功后才运行。if: github.ref == 'refs/heads/main'保证只有主干触发部署,feature 分支不会误上线。secrets.SERVER_HOST这些是存放在仓库 Settings → Secrets 里的敏感信息,绝不能硬编码在 YAML 里,否则一旦仓库公开,服务器 IP、私钥全泄漏。
SSH 私钥、服务器账号这些,直接写在配置里是新手最常犯、后果也最严重的错误。哪怕仓库是私有的,也养成用 Secrets 的习惯。
几个高频踩坑小结
-
YAML 缩进:Actions 配置文件对缩进极其敏感,
-列表项和 key-value 缩进错了,GitHub 会直接报语法错误。本地可以用 yamllint 或直接在线校验。 -
npm civsnpm install:CI 环境务必用npm ci,它是”干净安装”,不会像npm install那样尝试更新锁文件,行为可预期。 -
环境变量传递:不同 step 之间的环境变量不会自动共享,想要跨 step 传值,用
GITHUB_ENV这种文件或outputs。 -
免费额度:私有仓库有分钟数限制,公共仓库基本不限制。写大流水线前先评估一下,别因为一个 while 死循环把额度烧光。
-
敏感日志:
console.log千万别输出 token、密码,Actions 的日志默认对贡献者(甚至公开仓库的所有人)可见。
写在最后
GitHub Actions 的上手成本其实就是学会写一个 YAML 文件,剩下的都是实践细节。建议的节奏是:先在个人项目里配一条最基础的”push 即测试”流水线,把它跑绿;然后逐步加 lint、构建缓存;最后再接上自动部署。不要一上来就追求”一键从提交到生产”的完整流水线,那样一旦失败,排查起来反而更费劲。
工具存在的意义,是让你把精力留在写代码上,而不是反复做那些可以交给机器的事。打开你手头的仓库,给 .github/workflows/ 里加上第一条配置吧。