在大型组织和 Monorepo 项目中,CI/CD 的 DRY(Don’t Repeat Yourself)原则至关重要。GitHub Actions 提供可复用工作流(Reusable Workflows)和 Composite Actions,让组织级别的工作流标准化成为可能。本文覆盖从可复用设计到 Monorepo CI 的完整实践,并深入探讨矩阵构建、Artifact 传递、秘密管理、跨仓库共享、覆盖率合并与本地调试等高级主题。
一、可复用工作流(Reusable Workflows)
1.1 为什么需要可复用工作流?
多个项目的重复 CI 配置:
- Project A: install → lint → test → build
- Project B: install → lint → test → build
- Project C: install → lint → test → build
→ 提取到 shared-workflows → 所有项目调用同一模板
优势:集中维护、统一标准、减少重复、强制合规。当安全扫描规则或测试命令需要全局调整时,只需修改一处即可影响所有调用方。
1.2 定义可复用工作流
可复用工作流通过 workflow_call 事件触发,支持 inputs、secrets、outputs 三种接口。输入参数支持 string、number、boolean 三种类型,并可通过 required 和 default 控制必要性。
# .github/workflows/reusable-test.yml
name: Reusable Node.js Test
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version'
type: string
required: true
default: '20'
run-e2e:
description: 'Run E2E tests'
type: boolean
required: false
default: false
working-directory:
description: 'Working directory'
type: string
required: false
default: '.'
secrets:
codecov-token:
description: 'Codecov upload token'
required: false
sonar-token:
description: 'SonarQube token'
required: false
outputs:
test-passed:
description: 'Whether tests passed'
value: ${{ jobs.test.outputs.result }}
coverage-percentage:
description: 'Coverage percentage'
value: ${{ jobs.test.outputs.coverage }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.test.outputs.result }}
coverage: ${{ steps.test.outputs.coverage }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
working-directory: ${{ inputs.working-directory }}
- name: Run Lint
run: npm run lint
working-directory: ${{ inputs.working-directory }}
- name: Run Type Check
run: npm run type-check
working-directory: ${{ inputs.working-directory }}
- name: Run Tests
id: test
run: |
npm test -- --coverage
echo "result=success" >> $GITHUB_OUTPUT
COVERAGE=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
echo "coverage=${COVERAGE}" >> $GITHUB_OUTPUT
working-directory: ${{ inputs.working-directory }}
- name: Upload Coverage
if: secrets.codecov-token != ''
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.codecov-token }}
files: ${{ inputs.working-directory }}/coverage/lcov.info
- name: Run E2E
if: inputs.run-e2e
run: npm run test:e2e
working-directory: ${{ inputs.working-directory }}
1.3 调用可复用工作流
调用方可使用 uses 关键字引用可复用工作流,通过 with 传参、secrets 传秘钥。secrets: inherit 可一次性继承所有调用仓库的 secrets。
# .github/workflows/main.yml
name: CI
on: [push, pull_request]
jobs:
call-test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: '20'
run-e2e: true
working-directory: './packages/core'
secrets:
codecov-token: ${{ secrets.CODECOV_TOKEN }}
sonar-token: ${{ secrets.SONAR_TOKEN }}
1.4 跨仓库调用
跨仓库调用时,必须在被调用仓库中配置 actions 权限。引用格式为 owner/repo/.github/workflows/file.yml@ref,其中 ref 可以是分支、标签或 commit SHA。
jobs:
call-shared:
uses: my-org/shared-workflows/.github/workflows/test.yml@v1.2.0
with:
node-version: '20'
run-e2e: false
secrets:
inherit
call-canary:
uses: my-org/shared-workflows/.github/workflows/test.yml@main
with:
node-version: '22'
secrets:
inherit
最佳实践:生产环境使用固定标签(如 @v1.2.0),开发环境可使用分支引用以便快速验证变更。
二、Monorepo CI 策略
2.1 pnpm Workspace + Turborepo
Monorepo 中最大的挑战是避免每次提交都构建整个仓库。Turborepo 的远程缓存和任务管道可以显著减少重复工作。--filter=[origin/main...HEAD] 语法确保只构建相对于 main 分支有变更的包及其依赖。
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
setup:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build & Test affected
if: github.event_name == 'pull_request'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
run: |
pnpm turbo run lint type-check test build --filter=[origin/main...HEAD] -- --coverage
- name: Build all (main branch)
if: github.ref == 'refs/heads/main'
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
run: pnpm turbo run build
2.2 基于路径的筛选
路径过滤(paths/paths-ignore)是另一层优化:当修改只涉及文档或设计稿时,完全跳过 CI 执行,节省 runners 时间与成本。
on:
push:
paths:
- 'apps/web/**'
- 'packages/ui/**'
- 'packages/utils/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/web.yml'
paths-ignore:
- '**.md'
- '**.spec.ts'
三、Composite Action 组合
Composite Action 与 Reusable Workflow 的核心区别在于执行上下文。Composite Action 在调用者的工作流上下文中运行,共享环境变量和 secrets,适合封装重复的步骤序列;Reusable Workflow 在独立的运行环境中执行,适合定义完整的 CI 阶段。
# .github/actions/setup/action.yml
name: 'Setup Node + pnpm + Cache'
description: 'Sets up Node.js with pnpm and caching, plus environment validation'
inputs:
node-version:
description: 'Node.js version'
required: true
default: '20'
install-args:
description: 'Extra args for pnpm install'
required: false
default: '--frozen-lockfile'
outputs:
node-version:
description: 'Resolved Node.js version'
value: ${{ steps.node.outputs.version }}
runs:
using: "composite"
steps:
- name: Validate Environment
shell: bash
run: |
if [ -z "${{ github.token }}" ]; then
echo "Error: GitHub token is required"
exit 1
fi
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
id: node
with:
node-version: ${{ inputs.node-version }}
cache: 'pnpm'
- name: Install dependencies
shell: bash
run: pnpm install ${{ inputs.install-args }}
- name: Verify lockfile
shell: bash
run: |
if git diff --name-only | grep -q "pnpm-lock.yaml"; then
echo "Warning: lockfile was modified during install"
fi
四、发布自动化
4.1 Changesets 自动版本管理
Changesets 是 Monorepo 中最成熟的语义化版本管理方案。开发者通过 changeset add 记录变更,Changesets 在 CI 中自动计算版本号、生成 CHANGELOG、创建 Release PR,合并后自动发布到 npm 和 GitHub Releases。
name: Release
on:
push:
branches: [main]
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- 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'
- name: Install dependencies
run: pnpm install
- name: Build packages
run: pnpm turbo run build
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
version: pnpm changeset version
publish: pnpm changeset publish
commit: 'chore(release): version packages'
title: 'chore(release): version packages'
setupGitUser: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
五、性能优化矩阵
| 优化手段 | 效果 | 适用场景 |
|---|---|---|
cache: 'npm' | 减少 30~60 秒安装时间 | 通用 Node.js 项目 |
pnpm store 缓存 | 减少 40% 磁盘 I/O | pnpm 项目 |
turbo run --filter | 仅构建变更包及其依赖 | Monorepo |
paths 过滤 | 避免无关提交触发 CI | 大仓库 |
cancel-in-progress | 节省并发资源 | 快速迭代的 PR |
fetch-depth: 0 按需 | 浅克隆减少 checkout 时间 | 非 history 敏感任务 |
timeout-minutes | 防止僵尸作业耗尽配额 | 所有工作流 |
jobs:
optimized-build:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 1
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Restore Turborepo cache
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run build --cache-dir=.turbo
六、矩阵构建策略:OS + Node.js 版本 + 浏览器测试矩阵
矩阵构建(Strategy Matrix)允许在多个维度上并行执行相同的作业,是确保跨平台兼容性的核心手段。合理的矩阵设计应平衡覆盖率与资源消耗,避免维度爆炸。
name: Matrix CI
on: [push, pull_request]
jobs:
test-matrix:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20, 22]
browser: [chromium, firefox, webkit]
exclude:
- os: windows-latest
browser: webkit
include:
- os: ubuntu-latest
node-version: 22
browser: chromium
label: 'latest-ubuntu'
name: Test ${{ matrix.label || matrix.os }} / Node ${{ matrix.node-version }} / ${{ matrix.browser }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps ${{ matrix.browser }}
- name: Run unit tests
run: pnpm test
- name: Run E2E with ${{ matrix.browser }}
run: pnpm test:e2e --project=${{ matrix.browser }}
env:
CI: true
exclude 用于排除不兼容的组合(如 Windows 不支持 WebKit),include 用于添加特殊的组合或注入额外变量。fail-fast: false 确保单个矩阵项失败不会立即取消其余项,便于一次性查看所有兼容性结果。
七、Artifact 传递与缓存策略(cache hit/miss 可视化)
在分阶段工作流中,Artifact 用于在作业之间传递构建产物。与缓存(cache)不同,Artifact 是工作流级别的临时存储,适合传递编译结果、测试报告和静态产物;缓存则是跨工作流的持久化存储,适合依赖和工具链。
name: Artifact & Cache Pipeline
on: [push]
jobs:
build:
runs-on: ubuntu-latest
outputs:
cache-hit: ${{ steps.turbo-cache.outputs.cache-hit }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Restore Turborepo cache
id: turbo-cache
uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ hashFiles('**/package.json', '**/turbo.json') }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-${{ hashFiles('**/package.json', '**/turbo.json') }}-
turbo-${{ runner.os }}-
- run: pnpm install --frozen-lockfile
- run: pnpm turbo run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.sha }}
path: |
apps/*/dist
packages/*/dist
retention-days: 5
- name: Print cache status
run: |
echo "Cache hit: ${{ steps.turbo-cache.outputs.cache-hit }}"
if [ "${{ steps.turbo-cache.outputs.cache-hit }}" == "true" ]; then
echo "Skipped rebuild due to cache hit"
else
echo "Full rebuild executed"
fi
test:
runs-on: ubuntu-latest
needs: build
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
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist-${{ github.sha }}
- name: Run tests against prebuilt artifacts
run: pnpm test
缓存策略的关键在于键(key)的设计。采用多层恢复键(restore-keys)可以在精确匹配失败时回退到较旧的缓存,而不是从零开始。hashFiles 确保了当 package.json 或 turbo.json 变更时缓存自动失效。
八、秘密管理:GitHub Secrets vs OIDC vs Vault 集成
GitHub Actions 提供三级秘密管理机制:仓库级 Secrets(最简单)、OIDC 令牌(最安全)和 HashiCorp Vault 动态凭据(最灵活)。
GitHub Secrets 适合静态凭据,但对云资源访问需要手动轮换。OIDC(OpenID Connect) 允许工作流直接换取云提供商的短期访问令牌,无需存储长期密钥,是零信任架构的核心实践。Vault 则适合多环境、多团队的动态凭据分发。
name: Secure Deployment with OIDC & Vault
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
deploy-aws:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS Credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
aws-region: us-east-1
- name: Deploy to S3
run: aws s3 sync ./dist s3://my-bucket --delete
deploy-with-vault:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Import Secrets from Vault
id: vault
uses: hashicorp/vault-action@v3
with:
method: jwt
url: https://vault.company.com
role: github-actions
secrets: |
secret/data/ci/aws access_key | AWS_ACCESS_KEY_ID ;
secret/data/ci/aws secret_key | AWS_SECRET_ACCESS_KEY
- name: Deploy using dynamic credentials
run: |
echo "Deploying with dynamically issued credentials..."
aws s3 sync ./dist s3://my-bucket --delete
env:
AWS_ACCESS_KEY_ID: ${{ steps.vault.outputs.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ steps.vault.outputs.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: us-east-1
OIDC 配置要点:在 AWS IAM 中创建信任策略,限制 sts:AssumeRoleWithWebIdentity 只能由特定仓库、分支和工作流触发。Vault 的 JWT 认证则需将 GitHub 的 OIDC 公钥配置为可信的 JWT 签发方。
九、跨仓库共享工作流:组织级 .github 仓库模式与版本化
组织级共享工作流的推荐架构是在 .github 仓库中集中存放所有可复用工作流,通过 Git 标签进行语义化版本管理。调用方引用 @v1 获得最新的向后兼容更新,引用 @v1.2.3 获得完全稳定的构建。
my-org/.github
├── .github/
│ └── workflows/
│ ├── node-ci.yml # v1.x 系列
│ ├── docker-build.yml # v2.x 系列
│ └── security-scan.yml # v1.x 系列
├── README.md
└── RELEASING.md
# 调用方工作流
name: Org Standard CI
on: [push, pull_request]
jobs:
standard-test:
uses: my-org/.github/.github/workflows/node-ci.yml@v1
with:
node-version: '20'
secrets:
inherit
security-gate:
uses: my-org/.github/.github/workflows/security-scan.yml@v1
secrets:
inherit
版本化策略建议:主版本号(v1, v2)用于破坏性变更,次版本号(v1.1, v1.2)用于功能新增,修订号(v1.1.0, v1.1.1)用于缺陷修复。每次发布时同时移动主版本标签到最新的次版本 commit。
# 发布共享工作流 v1.2.0
git tag -a v1.2.0 -m "Release v1.2.0: add pnpm 9 support"
git tag -f v1 v1.2.0 # 移动 v1 标签
git push --tags --force
十、测试覆盖率报告合并(Monorepo 多包合并到 Codecov)
Monorepo 中每个包通常独立运行测试并生成覆盖率报告。为了在 Codecov 中获得统一的覆盖率视图,需要先收集各包的 lcov.info,合并后一次性上传。
name: Coverage Aggregation
on: [push, pull_request]
jobs:
test-packages:
runs-on: ubuntu-latest
strategy:
matrix:
package:
- packages/core
- packages/ui
- packages/utils
- apps/web
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 --filter ${{ matrix.package }} test -- --coverage
- name: Upload package coverage
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.package }}
path: ${{ matrix.package }}/coverage/lcov.info
merge-upload:
runs-on: ubuntu-latest
needs: test-packages
steps:
- uses: actions/checkout@v4
- name: Download all coverage artifacts
uses: actions/download-artifact@v4
with:
pattern: coverage-*
merge-multiple: true
path: ./coverage-reports
- name: Merge coverage reports
run: |
npm install -g lcov-result-merger
mkdir -p ./merged-coverage
lcov-result-merger './coverage-reports/**/lcov.info' ./merged-coverage/lcov.info
- name: Upload merged coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./merged-coverage/lcov.info
flags: monorepo
name: merged-coverage
fail_ci_if_error: true
lcov-result-merger 将多个 lcov.info 文件中的行覆盖信息合并为一份统一报告。Codecov 的 flags 参数允许对不同类型测试(单元测试、集成测试)进行分组分析,fail_ci_if_error: true 防止静默失败导致覆盖率数据缺失。
十一、GitHub Actions 调试与故障排除(verbose log / SSH debug / act 本地测试)
CI 流水线失败时的调试效率直接影响开发体验。GitHub Actions 提供三级调试手段:启用详细日志、使用 tmate 进行 SSH 远程调试、通过 act 在本地复现工作流。
name: Debug Pipeline
on:
workflow_dispatch:
inputs:
enable-ssh-debug:
description: 'Enable SSH debug on failure'
type: boolean
default: false
env:
ACTIONS_STEP_DEBUG: true
ACTIONS_RUNNER_DEBUG: true
jobs:
debug-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Verbose environment dump
run: |
echo "=== Environment ==="
env | sort
echo "=== Disk Usage ==="
df -h
echo "=== Memory ==="
free -h || true
echo "=== Runner Info ==="
echo "Runner OS: ${{ runner.os }}"
echo "Runner Arch: ${{ runner.arch }}"
- name: Setup with debug output
uses: actions/setup-node@v4
with:
node-version: 20
- name: Simulated failing step
run: |
echo "Step output for debugging"
exit 0
- name: Setup tmate session on failure
if: ${{ failure() && inputs.enable-ssh-debug }}
uses: mxschmitt/action-tmate@v3
timeout-minutes: 15
with:
limit-access-to-actor: true
act 是本地运行 GitHub Actions 的 CLI 工具,基于 Docker 复刻 GitHub 的运行器环境。在推送前本地验证工作流可以节省大量远程调试时间。
# 安装 act
brew install act
# 列出可用环境
act -l
# 本地运行 push 事件
act push
# 本地运行特定工作流
act -W .github/workflows/ci.yml
# 使用 verbose 模式查看详细日志
act push -v
# 模拟 secrets(避免提交到远程)
act push -s MY_SECRET=local-test-value
# 限制 job 执行(加快本地验证)
act push -j build
ACTIONS_STEP_DEBUG 和 ACTIONS_RUNNER_DEBUG 环境变量会在每个步骤的前后打印完整的上下文变量、路径和运行器信息。tmate 的 limit-access-to-actor: true 确保只有触发工作流的 GitHub 用户可以通过 SSH 连接,避免安全性风险。本地使用 act 时,建议配合 -s 传入 mock secrets,避免在本地环境中泄露生产凭据。
相关阅读
- GitHub Actions 详解 — 核心概念与语法
- GitHub Actions Node.js CI — 测试流水线
- GitHub Actions Docker CI/CD — 容器化构建
- GitHub Actions 安全 — 权限与审计
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。