Monorepo 项目的 GitHub Actions CI/CD
monorepo 的 CI/CD 管道面临着与单一应用程序管道完全不同的挑战:当 5 个应用程序中只有 1 个发生更改时,如何运行快速、可靠的管道?天真地在每次提交上构建所有内容会将 2 分钟的管道变成 15 分钟的瓶颈,开发人员可以通过批量提交来解决这个问题 - 这正是您想要防止的。
本指南介绍了 Turborepo monorepo 的生产 GitHub Actions 设置:仅受影响的构建、并行作业执行、Turbo 远程缓存集成、基于环境的部署门以及防止机密泄漏到日志或分叉 PR 运行中的安全模式。
要点
- 在结账时使用
fetch-depth: 2来启用 Turbo 受影响的包裹检测- 并行运行 lint、类型检查、测试和构建 — 不要按顺序链接它们
- 矩阵作业让您可以跨多个 Node.js 版本或操作系统进行测试,而无需重复 YAML
- 将 Turbo 远程缓存凭证存储为 GitHub Secrets,而不是 YAML 中的环境变量
- 对生产部署作业使用
environment:保护规则以要求手动批准paths-filter操作可让您在仅更改文档或非代码文件时完全跳过 CI- 永远不要将机密打印到日志中 - 使用
::add-mask::作为运行时生成的机密- 可重用工作流程 (
workflow_call) 消除多个存储库工作流程中的重复
CI 的存储库结构
良好的 CI 设置反映了您的开发工作流程。以下是适用于 Turborepo monorepo 的结构:
.github/
workflows/
ci.yml — Runs on every PR and push to main
deploy.yml — Runs on push to main (after CI passes)
security.yml — Weekly security scans
cleanup.yml — Scheduled: delete stale preview deployments
actions/
setup-pnpm/
action.yml — Reusable: install Node + pnpm + cache
核心 CI 工作流程
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
# Cancel in-progress runs when a new run starts
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
NODE_VERSION: '22'
PNPM_VERSION: '10.28.0'
jobs:
# Fast path: skip if only docs/assets changed
changes:
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
docs: ${{ steps.filter.outputs.docs }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- 'apps/**/*.ts'
- 'apps/**/*.tsx'
- 'packages/**/*.ts'
- 'turbo.json'
- 'package.json'
- 'pnpm-lock.yaml'
docs:
- 'apps/docs/**'
- '**/*.md'
lint:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Required for turbo affected detection
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm turbo run lint
- name: Type check
run: pnpm turbo run type-check
test:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: ecosire_test
POSTGRES_USER: ecosire
POSTGRES_PASSWORD: password
ports:
- 5433:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm turbo run test
env:
DATABASE_URL: postgresql://ecosire:password@localhost:5433/ecosire_test
REDIS_URL: redis://localhost:6379
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/api/coverage/lcov.info
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build all apps
run: pnpm turbo run build
env:
NEXT_PUBLIC_API_URL: ${{ vars.STAGING_API_URL }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: |
apps/web/.next/
apps/api/dist/
retention-days: 1 # Keep briefly for deploy job
与 Playwright 进行 E2E 测试工作
e2e:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
project: [chromium, firefox, mobile-chrome]
fail-fast: false # Don't cancel other browsers if one fails
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: cd apps/web && npx playwright install ${{ matrix.project }} --with-deps
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-${{ github.sha }}
- name: Start test servers
run: |
cd apps/api && node dist/main.js &
cd apps/web && npx next start &
npx wait-on http://localhost:3000 http://localhost:3001/health
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- name: Run E2E tests
run: cd apps/web && npx playwright test --project=${{ matrix.project }}
env:
BASE_URL: http://localhost:3000
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report-${{ matrix.project }}
path: apps/web/playwright-report/
retention-days: 7
安全扫描工作流程
# .github/workflows/security.yml
name: Security Scan
on:
schedule:
- cron: '0 2 * * 1' # Weekly Monday 2am UTC
push:
paths:
- '**/package.json'
- 'pnpm-lock.yaml'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: '10.28.0'
- name: Run pnpm audit
run: pnpm audit --audit-level moderate
continue-on-error: true # Don't fail CI, just report
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: test
args: --severity-threshold=high
secret-scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for secret scanning
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
部署工作流程
# .github/workflows/deploy.yml
name: Deploy to Production
on:
workflow_dispatch:
inputs:
environment:
description: 'Deploy environment'
required: true
default: 'production'
type: choice
options:
- production
- staging
# Only one production deploy at a time
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false # Don't cancel in-flight deploys
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: '10.28.0'
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Run quality checks
run: |
pnpm turbo run lint type-check test
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
deploy:
needs: quality-gate
runs-on: ubuntu-latest
environment: ${{ inputs.environment }} # Requires approval for production
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e
cd /opt/ecosire/app
echo "Pulling latest changes..."
git pull origin main
echo "Installing dependencies..."
pnpm install --frozen-lockfile
echo "Building..."
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} \
TURBO_TEAM=${{ vars.TURBO_TEAM }} \
npx turbo run build
echo "Running migrations..."
pnpm --filter @ecosire/db db:migrate
echo "Restarting services..."
pm2 restart ecosystem.config.cjs --update-env
echo "Health check..."
sleep 10
curl -f https://ecosire.com/api/health || exit 1
curl -f https://api.ecosire.com/api/health || exit 1
echo "Deploy complete!"
- name: Notify on success
if: success()
uses: actions/github-script@v7
with:
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: `Deployed to ${process.env.ENVIRONMENT} at ${new Date().toISOString()}`
})
env:
ENVIRONMENT: ${{ inputs.environment }}
- name: Notify on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Deploy failed: ${process.env.ENVIRONMENT}`,
body: `Deploy to ${process.env.ENVIRONMENT} failed at ${new Date().toISOString()}\n\nRun: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
})
env:
ENVIRONMENT: ${{ inputs.environment }}
缓存策略
有效缓存是 2 分钟和 8 分钟 CI 运行之间的差异:
# Reusable setup step (extract to composite action)
# .github/actions/setup-pnpm/action.yml
name: Setup pnpm
runs:
using: composite
steps:
- uses: pnpm/action-setup@v4
with:
version: '10.28.0'
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
# Turbo cache (separate from pnpm cache)
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
shell: bash
安全最佳实践
永远不要回显秘密:
# Wrong — secret appears in logs
- run: echo "API key is ${{ secrets.API_KEY }}"
# If you must generate and use runtime secrets
- run: |
echo "::add-mask::$GENERATED_SECRET"
GENERATED_SECRET=$(generate-secret)
echo "GENERATED_SECRET=$GENERATED_SECRET" >> $GITHUB_ENV
限制权限:
# Minimum permissions at workflow level
permissions:
contents: read
actions: read
checks: write # Add only what you need
jobs:
test:
permissions:
contents: read # Override at job level if needed
防止拉取请求注入:
# For PRs from forks — never expose secrets
on:
pull_request:
jobs:
test:
# Secrets not available in fork PRs by default
# Use pull_request_target only for trusted actions
runs-on: ubuntu-latest
常见问题
如何仅在特定包发生更改时触发工作流程?
使用 dorny/paths-filter 检测哪些包发生了更改,然后将过滤器输出传递给作业条件。对于 Turbo 内置的受影响检测,请确保结账步骤中包含 fetch-depth: 2 ,以便 Turbo 可以与之前的提交进行比较。使用 pnpm turbo run test --filter="...[HEAD^1]" 仅对更改的包及其依赖项运行测试。
如何在 CI 中处理数据库迁移?
在测试之后但创建最终构建工件之前运行的单独作业中运行迁移。在 CI 中使用专用的测试数据库 URL(与开发和生产分开)。切勿自动从 CI 运行针对生产的迁移 — 使用 workflow_dispatch 并通过 GitHub 环境保护规则手动批准生产数据库更改。
跨多个环境管理机密的最佳方法是什么?
将 GitHub 环境(设置 > 环境)与保护规则结合使用。为 staging 和 production 创建单独的环境。在每个环境下存储特定于环境的机密,而不是存储在存储库级别。对于共享机密(例如 Turbo 远程缓存令牌),请使用存储库级机密。作业配置中的 environment: 键启用特定于环境的秘密和保护规则。
如何加快 CI 中的 pnpm 安装速度?
actions/setup-node@v4 和 cache: 'pnpm' 缓存 pnpm 存储。对于大型 monorepos,恢复缓存后也使用 pnpm install --prefer-offline 。 pnpm 存储缓存和 Turbo 远程缓存的组合通常可以将缓存命中的安装 + 构建时间缩短 60-80%。
如何为 PR 与主分支推送运行不同的工作流程?
使用 on 触发条件:on.push.branches 用于 main,on.pull_request.branches 用于 PR。您可以在 main 上运行更快的 PR 测试子集(lint、单元测试)和完整套件。使用 github.event_name == 'push' 条件仅在推送到主程序时运行部署作业,而不是在 PR 事件上运行。
后续步骤
精心设计的 CI/CD 管道是高速工程团队的基础 - 它让您有信心频繁交付并自动捕获回归。 ECOSIRE 运行 GitHub Actions CI,包括 lint、类型检查、1,301 单元测试、Playwright E2E 以及每次推送到主程序时的零停机 SSH 部署。
无论您需要 CI/CD 架构咨询、Turborepo monorepo 设置还是完整的 DevOps 支持,探索我们的工程服务。
作者
ECOSIRE Research and Development Team
在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。
相关文章
AI-Powered Accounting Automation: What Works in 2026
Discover which AI accounting automation tools deliver real ROI in 2026, from bank reconciliation to predictive cash flow, with implementation strategies.
Payroll Processing: Setup, Compliance, and Automation
Complete payroll processing guide covering employee classification, federal and state withholding, payroll taxes, garnishments, automation platforms, and year-end W-2 compliance.
AI Agents for Business Automation: The 2026 Landscape
Explore how AI agents are transforming business automation in 2026, from multi-agent orchestration to practical deployment strategies for enterprise teams.