首页 / Node.js 教程 / CI/CD 自动化

Node.js 教程

CI/CD 自动化

本教程共 76 篇 · 第 70 篇 · 更新于 2026-07-25 · 约 12 分钟阅读

Node.jsCI/CDGitHub Actions自动化部署

70. CI/CD 自动化

本节目标:- CI/CD 到底是什么,解决什么问题 - 写一个 Node.js 的 GitHub Actions 工作流(Workflow) - 在工作流里跑测试、lint、构建,并缓存依赖加速 - 把构建产物发布到容器镜像仓库或直接部署 先厘清两个词:CI(Continuous Integration,持续集成)是「每次提交都自动构建+测试」,目的是早发现问题;CD(Continuous Deployment/Delivery,持续交付/部署)是「测试通过后自动发布到环境」。本章两个都沾。

上一章你把部署三件套配好了,但发布还得手动 SSH 上去、拉代码、重启——这套动作做一次两次还行,天天做就是折磨,而且人一慌就容易漏步骤。CI/CD 的核心就是:把「构建 → 测试 → 发布」固化成一条机器自动跑的流水线,你只管往 main 分支推代码。

本章以 GitHub Actions 为主讲对象(最普及、和 GitHub 仓库无缝集成),但思路对任何 CI 平台都成立。

一个最小工作流

GitHub Actions 的配置放在仓库的 .github/workflows/ 目录下,文件是 YAML 格式。

# .github/workflows/ci.yml
name: CI

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Use Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm

      - run: npm ci
      - run: npm test

推上去之后,每次往 main 推代码或开 PR,GitHub 都会自动跑一遍 npm cinpm test。这不只是一道保险——它让「能合并」和「能跑通」画上等号,团队越大越值钱。

Note

actions/setup-node@v4cache: npm 会自动按 package-lock.json 缓存 node_modules,下次构建省掉重复下载,速度能快几倍。这是现在的标准写法,别忘了。

节点版本矩阵

你不一定只支持一个 Node 版本。想验证「在 v22 和 v24 上都能跑」,用 matrix

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [22, 24]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm
      - run: npm ci
      - run: npm test

矩阵会把每个版本各跑一遍。注意我们基准是 v24 LTS,所以至少覆盖 22(维护性 LTS)和 24(当前 LTS)。

加上 lint 和类型检查

测试之外,通常还想跑 lint。把步骤串起来:

      - run: npm ci
      - run: npm run lint
      - run: npm run build
      - run: npm test

前提是你的 package.json 里得有对应脚本:

{
  "scripts": {
    "start": "node app.js",
    "dev": "node --watch app.js",
    "build": "node --run build || true",
    "lint": "eslint .",
    "test": "node --test"
  }
}
Tip

自 v24 LTS 起,node --run <script> 是运行 package.json 脚本的推荐方式,比 npm run 更轻(不加载 npm 的全部逻辑)。写脚本的 start/build/lint/test 时可以直接用 node --run 调子脚本,但 CI 里用 npm ci && npm test 仍然最稳,因为它会处理依赖安装。

缓存与构建加速

除了 setup-node 的依赖缓存,如果你的构建产物(比如 dist/)在多次运行间可复用,可以用 actions/cache

      - name: Cache build output
        uses: actions/cache@v4
        with:
          path: dist
          key: ${{ runner.os }}-build-${{ github.sha }}

keygithub.sha(本次提交的哈希)保证每次提交对应独立缓存,避免拿到旧产物。

发布:构建并推送 Docker 镜像

CI 通过后,常见的 CD 动作是把镜像推到仓库(Docker Hub、GHCR、阿里云 ACR 等)。下面是把镜像推到 GitHub 容器仓库(GHCR)的例子:

# .github/workflows/deploy.yml
name: Build and Push Image

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  docker:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write   # 推 GHCR 需要这个权限
    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}

secrets.GITHUB_TOKEN 是 GitHub 自动注入的,不需要你手动建;但 permissions 里必须声明 packages: write,否则推送会被拒。

Warning

镜像 tag 别只打 latestlatest 没法回滚、不好追溯。至少带上 github.sha(提交哈希)或语义化版本 tag,真出问题能精准回退到某一版。

部署到服务器(SSH)

如果你是自己的一台 VPS,可以在镜像推送后 SSH 上去拉新镜像重启:

      - name: Deploy over SSH
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /opt/my-api
            docker compose pull
            docker compose up -d
            docker image prune -f

敏感信息(主机地址、私钥)一律走仓库的 Settings → Secrets,用 ${{ secrets.XXX }} 引用,绝对不要明文写进 YAML。

在 CI 里跑带数据库的集成测试

单元测试不碰外部依赖,但集成测试需要真数据库。GitHub Actions 可以用 services 起一个临时数据库容器,跑完就销毁,干净又隔离:

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U test"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    env:
      DATABASE_URL: postgres://test:test@localhost:5432/testdb
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run migrate   # 建表
      - run: npm test

services 里的 options 是容器健康检查,等数据库真正能连了再开始跑测试,避免「库还没起来,测试已经连不上报错了」。这种测试跑一次的成本比本地手动起库低得多。

覆盖率与测试产物

CI 的价值之一是「每次提交的质量可见」。把覆盖率跑出来,并作为产物(artifact)保留,方便事后翻:

      - name: Run tests with coverage
        run: npm test -- --experimental-test-coverage

      - name: Upload coverage report
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: coverage
          path: coverage/
Tip

if: always() 很关键:即使测试挂了,覆盖率报告照样上传,你能看到「是在哪一步开始崩的」。--experimental-test-coverage 是 Node.js 内置测试运行器(node --test)的覆盖率开关,自 v22 起逐步稳定,v24 可用。

防止并发部署打架

如果你和同事同时往 main 推,可能两个部署流水线同时跑、互相覆盖。加 concurrency 让同一条流水线排队:

concurrency:
  group: deploy-production
  cancel-in-progress: false   # 不取消正在跑的,等它结束再跑下一个

cancel-in-progress: true 则更激进:新提交来了直接掐掉旧的那次。选哪个看你能不能接受「正在发布的版本被中途打断」。

可复用工作流

如果你的组织有多个 Node 项目,每个都抄一遍 setup-node + npm ci + npm test 很重复。GitHub Actions 支持「可复用工作流(Reusable Workflow)」:把通用步骤抽成一个被调用的文件,各项目 uses 它。

# .github/workflows/node-ci.yml (被复用)
name: Node CI Reusable
on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '24'
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test

别的项目直接调:

# 某业务的 .github/workflows/ci.yml
jobs:
  ci:
    uses: org/shared/.github/workflows/node-ci.yml@main
    with:
      node-version: '24'

workflow_call 是触发条件,表示「被别人调用时才跑」。这样团队的测试规范改一处,所有项目同时生效。

Tip

复用工作流有个坑:secrets 不会自动传给被调用方,要在 uses 里显式传 secrets: inherit,或者在工作流里声明 secrets 输入。忘了传,构建到一半会因为拿不到 token 而失败。

部署前先跑数据库迁移

很多团队卡在「代码发上去了,表结构却没更新」。正确顺序是把迁移(migration)作为发布流水线的一步,且要在「替换新代码之前」完成(向下兼容的迁移先跑,再发新版本):

      - name: Run database migration
        run: npm run migrate
        env:
          DATABASE_URL: ${{ secrets.PROD_DATABASE_URL }}

      - name: Pull and restart
        uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /opt/my-api
            docker compose pull
            docker compose up -d

迁移脚本本身要写成「可重复执行、失败可重跑」的(幂等),否则流水线一重试就出错。

回滚:发错了怎么办

再严谨的流水线也有发崩的时候。回滚(Rollback)能力必须提前想好,而不是事故了才手忙脚乱。两个常见做法:

  • 镜像回滚:因为每次发布都打了 github.sha 的镜像 tag,回滚就是「把 compose 里的镜像换回上一个 sha 重新拉起」:
# 在服务器上切回上一个镜像版本
sed -i 's|:newsha|:oldsha|' docker-compose.yml
docker compose up -d
  • 数据库迁移要可降:这是最容易翻车的地方。如果你上线时跑了「加列」迁移,回滚代码后旧代码不认新列通常没事;但如果你跑了「删列 / 改类型」迁移,旧代码一回来就炸。所以破坏性迁移要单独排期、单独审批,别混在普通发布里。
Warning

千万不要「代码回滚了,但数据库迁移没回滚」或者反过来。代码和数据库 schema 必须当成一对一起考虑。破坏性的 schema 变更,宁可多花一次发布周期,也要保证新旧代码都能跑。

多环境:staging 与 production

正规一点会有至少两个环境:staging(预发,尽量和生产一致,用来做最后验证)和 production(生产)。GitHub Actions 用 environment 区分,可以给 production 加人工审批门槛:

  deploy-prod:
    needs: deploy-staging
    environment: production   # 在仓库 Settings 里给这个环境配审批人
    runs-on: ubuntu-latest
    steps:
      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.PROD_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /opt/my-api && docker compose pull && docker compose up -d

environment: production 配上仓库里设置的「required reviewers」,流水线跑到这步会暂停,等人点批准才继续。staging 环境通常不设审批,自动过。

用 tag 触发正式发布

日常提交只跑 CI,只有打 v1.2.0 这种 tag 才走发布流程,更符合实际节奏:

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  test:
    if: github.ref_type != 'tag' || startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test

github.ref_typegithub.ref 是 Actions 提供的上下文变量,能区分「这次触发是分支还是 tag」。

Tip

别忘了 pull_request 也接 CI。PR 合入前的那次测试,比合入后再发现错误便宜得多。很多团队规定:CI 不绿,不准合并。

打 tag 发布时,建议用语义化版本号(v1.2.0)而不是 latest,这样版本历史清晰、出问题能精准定位到哪一版。配合 actions/create-release 还能自动生成带 changelog 的发布说明,省去手写的麻烦。

失败怎么办

流水线最怕「红了也没人在意」。几个实用习惯:

  • 测试不全就别写 npm test 占位——空测试永远绿,等于没 CI。
  • 给仓库开「保护分支(branch protection)」,强制 main 必须过 CI 才能合并。
  • 把构建通知接到你的 IM(钉钉/飞书/Slack),红了立刻可见。
  • 关键发布步骤加人工审批(environment + required reviewers),正式环境别全自动。

一个项目的完整 CI/CD 串起来

把前面零散的配置拼成一个真实项目,你大概会看到这样的两个文件:

.github/workflows/ci.yml——每次提交和 PR 都跑:

name: CI
on:
  pull_request:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm test

.github/workflows/deploy.yml——只往 main 推且测试通过后才发:

name: Deploy
on:
  push:
    branches: [main]
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    steps:
      - uses: appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.DEPLOY_KEY }}
          script: |
            cd /opt/my-api
            docker compose pull
            docker compose up -d

needs: build-and-push 保证「镜像没推成功就不去部署」,这是流水线里最基本的依赖顺序。两边合起来,就是你从「提交代码」到「线上更新」的完整自动化。

把发布流程写进代码之后,你半夜被叫醒的次数会肉眼可见地减少。