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 | ~40s | pnpm 项目 |
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 }} 在命令行 |
相关阅读
- GitHub Actions 详解 — 核心概念与语法速查
- GitHub Actions Docker CI/CD — 容器化项目的 CI/CD
- GitHub Actions 部署到 Vercel — 前端项目自动部署
- Docker 专题 — Docker 部署与镜像优化
- Node.js 专题 — Node.js 性能优化
- React 测试指南 — Vitest + Playwright 的 CI 集成
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。