GitHub Actions Node.js CI 完全指南:Lint → Type Check → 测试 → 构建 → 发布的完整流水线

用 GitHub Actions 搭建 Node.js 项目的完整 CI 流水线:缓存优化、矩阵构建、并行测试、覆盖率上报、Monorepo 支持、Playwright E2E 集成、安全扫描(npm audit/Snyk)、自动发布(npm/changesets)。含完整的生产级配置模板和性能调优策略。

Node.js 项目的 CI 流水线是工程化的基石。一个完善的 CI 应该在代码合并前自动验证代码质量、运行测试、检查类型安全,并在发布时自动构建和部署。本文提供从简单项目到 Monorepo 的完整 GitHub Actions 配置模板和最佳实践。


一、最小可用 CI(90% 项目的起点)

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

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

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

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm test -- --coverage
      - run: npm run build

为什么这个配置能覆盖 90% 需求?

  • npm ci 严格按 lockfile 安装,避免"在我机器上跑得好"问题
  • lint + type-check + test + build 四重检查过滤 95%+ 的编码问题
  • cache: 'npm' 自动缓存 ~/.npm,Node 依赖安装提速 3-5 倍

二、完整生产级 CI:分层流水线

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

on:
  push:
    branches: [main, develop]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'package*.json'
      - '.github/workflows/ci.yml'
  pull_request:
    branches: [main]
    paths:
      - 'src/**'
      - 'tests/**'
      - 'package*.json'
      - '.github/workflows/ci.yml'

jobs:
  # ═══════════════════════════════════════════════
  # 阶段 1:代码质量检查(最快失败策略)
  # ═══════════════════════════════════════════════
  lint:
    name: 🔍 Lint & Format
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run format:check

  # ═══════════════════════════════════════════════
  # 阶段 2:类型安全与单元测试(矩阵构建)
  # ═══════════════════════════════════════════════
  test:
    name: 🧪 Test (Node ${{ matrix.node }})
    needs: lint
    runs-on: ${{ matrix.os }}
    timeout-minutes: 10
    strategy:
      fail-fast: false  # 一个版本失败不影响其他版本
      matrix:
        node: [18, 20, 22]
        os: [ubuntu-latest]
        include:
          - node: 20
            os: windows-latest
          - node: 20
            os: macos-latest

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'
      - run: npm ci
      - run: npm run type-check
      - run: npm test -- --coverage
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        if: matrix.node == 20 && matrix.os == 'ubuntu-latest'
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info

  # ═══════════════════════════════════════════════
  # 阶段 3:E2E 测试(Playwright)
  # ═══════════════════════════════════════════════
  e2e:
    name: 🎭 E2E Tests
    needs: [lint, test]
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
      - uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7

  # ═══════════════════════════════════════════════
  # 阶段 4:安全扫描
  # ═══════════════════════════════════════════════
  security:
    name: 🔒 Security Scan
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm audit --audit-level moderate
      - uses: snyk/actions/node@master
        if: github.event_name == 'pull_request'
        continue-on-error: true
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

  # ═══════════════════════════════════════════════
  # 阶段 5:构建验证
  # ═══════════════════════════════════════════════
  build:
    name: 📦 Build Verification
    needs: [lint, test]
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 5

高级触发器与条件控制

生产级 CI 需要根据代码变更范围、分支类型甚至 Pull Request 标签来精细化控制流水线行为,避免不必要的构建。

路径过滤与文件变更检测

on:
  push:
    branches: [main]
    paths:
      - 'src/**'
      - 'packages/**'
      - '!**/*.md'        # 排除 Markdown 文档变更
      - '!docs/**'        # 排除文档目录
  pull_request:
    types: [opened, synchronize, reopened, ready_for_review]
    paths-ignore:
      - '**/*.md'
      - '.vscode/**'

jobs:
  # 仅在 Pull Request 不是 Draft 时运行
  build:
    if: github.event.pull_request.draft == false
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

workflow_dispatch 手动触发与参数化

on:
  workflow_dispatch:
    inputs:
      environment:
        description: '部署目标环境'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production
      skip_tests:
        description: '跳过测试(仅紧急修复)'
        required: false
        type: boolean
        default: false
      custom_tag:
        description: '自定义镜像标签'
        required: false
        type: string

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.event.inputs.environment }}
    steps:
      - name: Conditional test
        if: ${{ github.event.inputs.skip_tests == 'false' }}
        run: npm test

矩阵条件过滤

strategy:
  matrix:
    node: [18, 20, 22]
    os: [ubuntu-latest, windows-latest, macos-latest]
    # 排除特定组合
    exclude:
      - node: 18
        os: macos-latest  # macOS 不跑旧版本
    include:
      - node: 20
        os: ubuntu-latest
        experimental: true
        cache: 'pnpm'

自定义 GitHub Actions 开发

当项目中有多个仓库需要复用相同的 CI 逻辑时,将公共步骤封装为自定义 Action 是最佳实践。

Composite Action(组合步骤)

# .github/actions/setup-node-pnpm/action.yml
name: 'Setup Node.js + pnpm'
description: '统一配置 Node.js 和 pnpm,带缓存'

inputs:
  node-version:
    description: 'Node.js 版本'
    required: false
    default: '20'
  pnpm-version:
    description: 'pnpm 版本'
    required: false
    default: '9'

runs:
  using: composite
  steps:
    - uses: pnpm/action-setup@v3
      with:
        version: ${{ inputs.pnpm-version }}
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'pnpm'
    - run: pnpm install --frozen-lockfile
      shell: bash
    - name: Turbo 远程缓存
      run: |
        echo "TURBO_TOKEN=${{ env.TURBO_TOKEN }}" >> $GITHUB_ENV
        echo "TURBO_TEAM=${{ env.TURBO_TEAM }}" >> $GITHUB_ENV
      shell: bash

使用自定义 Action:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/setup-node-pnpm
        with:
          node-version: '22'

JavaScript Action(复杂逻辑)

// .github/actions/bundle-analyzer/index.js
const core = require('@actions/core');
const github = require('@actions/github');

async function run() {
  try {
    const baseSize = core.getInput('base-size');
    const currentSize = core.getInput('current-size');
    const threshold = parseFloat(core.getInput('threshold')) || 5;
    
    const diff = ((currentSize - baseSize) / baseSize) * 100;
    
    core.setOutput('diff-percent', diff.toFixed(2));
    
    if (diff > threshold) {
      core.setFailed(`Bundle size increased by ${diff.toFixed(2)}%, exceeds ${threshold}% threshold`);
    }
  } catch (error) {
    core.setFailed(error.message);
  }
}

run();

三、缓存策略深度优化

3.1 npm 缓存

steps:
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'  # 自动缓存 ~/.npm(推荐)

3.2 Node Modules 缓存(更快但更大)

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20

  - name: Cache node_modules
    uses: actions/cache@v4
    id: cache
    with:
      path: node_modules
      key: ${{ runner.os }}-node20-${{ hashFiles('package-lock.json') }}
      restore-keys: |
        ${{ runner.os }}-node20-

  - if: steps.cache.outputs.cache-hit != 'true'
    run: npm ci

缓存命中 vs miss 对比:

策略缓存命中缓存未命中适用
cache: 'npm'~30s~60s通用推荐
node_modules~5s~60s大型项目
pnpm store~10s~40spnpm 项目

3.3 Turborepo 远程缓存

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: 'npm'

  - env:
      TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
      TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
    run: npx turbo run build test lint --cache-dir=.turbo

四、Monorepo CI(pnpm workspace + Turborepo)

name: Monorepo CI

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

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run lint type-check test build --filter=[origin/main...HEAD]

五、自动发布流程

5.1 npm 自动发布(Changesets + GitHub Release)

name: Release

on:
  push:
    branches: [main]

concurrency: ${{ github.workflow }}-${{ github.ref }}

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v3
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
          registry-url: 'https://registry.npmjs.org'
      - run: pnpm install

      - name: Create Release PR or Publish
        uses: changesets/action@v1
        with:
          publish: pnpm release
          version: pnpm version-packages
          commit: 'chore(release): version packages'
          title: 'chore(release): version packages'
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

5.2 语义化版本自动标签

jobs:
  release:
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 获取完整历史用于生成 changelog

      - name: Bump version and push tag
        uses: anothrNick/github-tag-action@1.67.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          WITH_V: true
          DEFAULT_BUMP: patch

测试报告聚合与代码质量可视化

CI 不仅是"跑通测试",更要让团队直观看到代码质量趋势。将测试结果、覆盖率、类型错误数量等指标可视化,是驱动质量文化的关键。

JUnit XML 报告 + GitHub Checks

- name: Run tests
  run: npm test -- --reporter=junit --outputFile=./test-results/junit.xml
  
- name: Publish Test Results
  uses: dorny/test-reporter@v1
  if: success() || failure()    # 即使测试失败也上传报告
  with:
    name: Jest Tests
    path: ./test-results/junit.xml
    reporter: jest-junit
    fail-on-error: false

Codecov 覆盖率趋势

- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v4
  with:
    token: ${{ secrets.CODECOV_TOKEN }}
    files: ./coverage/lcov.info
    flags: unittests
    name: codecov-umbrella
    fail_ci_if_error: false
    # PR 评论中显示覆盖率变化
    comment: true

Bundle Size 监控

- name: Analyze bundle size
  run: |
    npm run build
    ls -lh dist/assets/*.js | awk '{print $5, $NF}' > bundle-size.txt
    
- name: Compare with main branch
  run: |
    git fetch origin main
    # 对比基础分支的 bundle size...

最佳实践:在 PR 评论中自动回复 “Coverage: 87% (-2%) | Bundle Size: 234KB (+12KB)",让代码审查者一眼判断影响。

Secrets 管理与安全加固

CI/CD 流水线处理的是代码构建和部署权限,本身就是高价值攻击目标。Node.js 生态常因 npm token 或 SSH_KEY 泄露导致供应链攻击。

最小权限原则

做法危险做法安全做法
Token 存储硬编码在 YAML 中使用 GitHub Secrets(Encrypted)
权限范围contents: write(全写)contents: read + pull-requests: write
npm 发布长期 token使用 granular access token + IP 白名单
SSH Key持久化密钥GitHub Deploy Keys(仅读仓库)

OpenID Connect(OIDC)无密钥部署

现代云平台(AWS、Azure、GCP、Cloudflare)支持 OIDC,GitHub Actions 可以用临时 JWT Token 换取云平台的临时凭证,无需长期 Secrets:

permissions:
  id-token: write    # 必须授权以获取 JWT
  contents: read

jobs:
  deploy:
    steps:
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
          aws-region: us-east-1
          # 无需 aws-access-key-id / aws-secret-access-key!
      - run: aws s3 sync dist/ s3://my-bucket/

AWS IAM Trust Policy 中配置条件限制:

{
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
      "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
    }
  }
}

安全收益:即使 GitHub Secrets 泄露,攻击者也无法在其他环境使用凭证;凭证寿命限制在单个 Job 的运行时长内(通常 < 15 分钟)。

六、性能调优与故障排查

问题原因解决
npm ci 超时依赖解析慢使用 lockfile + 缓存,或换 pnpm
测试随机失败测试间有状态泄漏并行度设为 1,或隔离数据库
Docker 构建慢没有层缓存cache-from/to: type=gha
Artifact 过大包含 node_modules.gitignore 同样忽略上传
Secrets 泄露命令回显用 env: 注入,不用 ${{ secrets }} 在命令行

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「saas」更多文章

  1. 短链接对 SEO 的影响与优化最佳实践
  2. UTM 参数 + 短链接:追踪每一条营销链路
  3. 私域流量运营中的短链接策略:从引流到转化