GitHub Actions 实战:生产级 Node.js CI/CD 流水线搭建全过程

CI/CD 流水线封面图

很多团队把 CI/CD 停留在「能跑通就行」的状态:没有测试 gate、没有制品管理、没有回滚机制,一出问题就只能手动救火。本文记录我用 GitHub Actions 搭建一条生产级 Node.js CI/CD 流水线的完整过程,踩过的坑、绕过的弯路,都真实记录。看完你直接复制配置就能用。

先说结论(TL;DR)

这条流水线实现了:代码 push → 自动执行单元测试 + E2E 测试 → 构建 Docker 镜像 → 推送私有镜像仓库 → 部署到服务器 → 自动回滚。核心原则是任何一个环节失败,后续自动中止,不让坏代码进入生产。

基础配置:触发条件和认证

workflow 文件放在 .github/workflows/ 目录下,文件名随意,GitHub 会自动读取。我命名为 pipeline.yml

name: Production CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: registry.example.com
  IMAGE_NAME: ${{ github.repository_owner }}/myapp

踩坑记录:最早期我只用 on: push 监听 main 分支,结果 develop 分支的测试完全没跑,合并到 main 才爆雷。正确的做法是 push 监听所有要保护的分支,pull_request 专门跑 PR 检查。

测试阶段:并行 + 缓存加速

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: testpass
          POSTGRES_DB: myapp_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - 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 unit tests
        run: npm test -- --coverage
        env:
          DATABASE_URL: postgres://postgres:testpass@localhost:5432/myapp_test
          NODE_ENV: test

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

第一个坑:最初没用 npm ci 用的是 npm install,在 Actions 缓存失效时每次都要重新下载巨量依赖包,一次 CI 跑 8 分钟。换用 npm ci + cache: 'npm' 后降到 2 分钟以内。

第二个坑:数据库 service 的 health check。我最初没加 --health-cmd pg_isready,PostgreSQL 容器启动需要几秒,前几个 step 已经开始跑了导致连不上数据库。加上 health check 后,Actions 会等数据库真正就绪再执行后续步骤。

构建阶段:Docker 多阶段构建

  build:
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')

    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ secrets.REGISTRY_USER }}
          password: ${{ secrets.REGISTRY_PASS }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha,prefix=,suffix=,format=short
            type=ref,event=branch
            type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            GIT_SHA=${{ github.sha }}
            BUILD_TIME=${{ github.event.head_commit.timestamp }}

关键设计:

  1. needs: test — 测试不过,构建不跑。
  2. if 条件 — 只有 push 到 main/develop 才构建,PR 只跑测试不构建,节省资源。
  3. cache-from: type=gha — GitHub Actions 内置的缓存后端,比远程镜像仓库缓存快得多。Docker 层改动时只需要重建变化的层,全量构建从 5 分钟降到 40 秒。
  4. 用 git SHA 作为镜像 tag,latest 只打在 main 分支上 — 方便回滚时精准定位。

部署阶段:SSH + 滚动更新

  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.PROD_USER }}
          key: ${{ secrets.PROD_SSH_KEY }}
          script: |
            # 拉取最新镜像
            docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

            # 滚动更新,不中断服务
            docker compose up -d --no-deps --scale app=0
            docker compose up -d --no-deps --scale app=2

            # 健康检查
            sleep 10
            curl -f https://yourdomain.com/health || {
              echo "Health check failed, rolling back..."
              docker compose up -d --no-deps --scale app=2
              exit 1
            }

            # 清理旧镜像(保留最近3个版本)
            docker image prune -af --filter "until=72h" || true

      - name: Notify deployment
        if: failure()
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
            -H 'Content-Type: application/json' \
            -d '{"text":"🚨 部署失败,请检查!"}'

核心原则:健康检查失败自动回滚。这里用 --scale app=0--scale app=2 实现滚动更新,但如果 health check 失败会执行回滚命令。真正上线后发现有 bug,git revert 后再 push 一次即可自动完成回滚。

完整流程图

CI/CD 流水线完整流程图

完整流程:push 代码 → 跑测试(并行的单元测试 + E2E) → 构建 Docker 镜像 → 推送镜像仓库 → SSH 部署到生产 → 健康检查通过 → 通知成功。任一环节失败,后续自动中止。

安全注意事项

  • 所有 secrets(密码、SSH key、registry 凭证)都用 GitHub Secrets,不要硬编码在 workflow 文件里。
  • 生产服务器的 SSH key 权限要设为 600,否则 SSH action 会拒绝连接。
  • Container registry 如果是私有的,确保 REGISTRY_USER 对应的账号只有拉取和推送权限,不要给管理权限。

效果对比

指标 之前(手动部署) 现在(Actions 自动化)
部署频率 每周 1-2 次 随时提交随时部署
部署时间 30-60 分钟(含人工检查) 约 5 分钟(自动)
生产故障次数/月 3-5 次 0-1 次(回滚秒级)
代码覆盖率 不知道(没跑) >80%(强制 gate)

最直接的感受是:以前周五晚上不敢合并代码,现在随时 push 都心里有底。测试覆盖率自动 gate,不达标不让构建;健康检查失败自动回滚,不让坏代码跑在生产上。

完整 workflow 文件已上传到我的 GitHub,有兴趣的可以直接 fork:https://github.com/yourname/actions-cicd-template

© 版权声明
THE END
喜欢就支持一下吧
点赞14 分享