Skip to main content

CI/CD Integration for Monorepo Pipelines

CI/CD in a monorepo requires intelligence: you do not rebuild all packages on every commit, only the packages that changed and their dependents. GitHub Actions combined with Turborepo automates this, detecting affected packages, running tests and builds in parallel, and publishing only when changes are ready. This article covers setting up a complete CI/CD workflow with build caching, artifact persistence, and multi-stage deployment.

Understanding Affected Package Detection

When you push a commit to your monorepo, only some packages change. A smart CI pipeline detects affected packages and runs tasks only for them. Turborepo provides the --filter flag to run tasks on affected packages:

turbo build --filter "[HEAD~1]"

This builds all packages affected by the last commit. In CI, compare against the base branch:

turbo build --filter "origin/main...HEAD"

This builds packages changed between the base branch and current branch.

Setting Up GitHub Actions Workflow

Create .github/workflows/ci.yml in your monorepo root:

name: CI

on:
push:
branches:
- main
- develop
pull_request:
branches:
- main

jobs:
test-and-build:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 8

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Lint
run: turbo lint

- name: Build
run: turbo build

- name: Test
run: turbo test

- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage/coverage-final.json

This workflow runs on every push to main and every pull request. It installs dependencies, runs linting, builds all packages, and runs tests.

Building Affected Packages Only

Optimize the CI workflow to build only affected packages:

name: CI (Affected Only)

on: [push, pull_request]

jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
affected: ${{ steps.detect.outputs.affected }}

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Detect affected packages
id: detect
run: |
AFFECTED=$(turbo build --filter "origin/main...HEAD" --dry-run=json | jq -r '.tasks[].package')
echo "affected=${AFFECTED}" >> $GITHUB_OUTPUT

build-and-test:
runs-on: ubuntu-latest
needs: detect-changes
strategy:
matrix:
package: ${{ fromJSON(needs.detect-changes.outputs.affected) }}

steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build ${{ matrix.package }}
run: turbo build --filter=${{ matrix.package }}

- name: Test ${{ matrix.package }}
run: turbo test --filter=${{ matrix.package }}

This workflow detects which packages changed and builds only those in parallel using matrix strategy.

Caching with Turbocloud in CI

Enable distributed caching to share build artifacts across CI runs. Link your monorepo with Turbocloud (covered in Article 6), then use it in CI:

name: CI with Turbocloud Cache

on: [push, pull_request]

env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: mycompany

jobs:
build-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm

- name: Install
run: pnpm install --frozen-lockfile

- name: Build with cache
run: turbo build

- name: Test with cache
run: turbo test

When TURBO_TOKEN is set, Turborepo automatically uploads and downloads from Turbocloud, dramatically speeding up CI.

Multi-Stage Deployment Workflow

Separate building, testing, and deployment into distinct stages for clarity:

name: Build, Test, Deploy

on:
push:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: 18
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: turbo build
- run: turbo test

- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: builds
path: packages/*/dist

deploy:
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v3
with:
name: builds
path: packages

- name: Deploy web app
run: |
npm run deploy:web
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

- name: Deploy component library
run: |
npm publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

This workflow builds once, uploads artifacts, then downloads them for deployment. This ensures the same artifacts deployed everywhere.

Running Tasks in Dependency Order

Turborepo automatically determines task order based on your dependency graph. Ensure your monorepo has a well-defined graph by checking it:

turbo build --graph

Open the generated graph.html to verify:

  • Packages without dependencies build first.
  • Packages with dependencies build only after dependencies succeed.
  • No circular dependencies exist.

Handling Secrets and Environment Variables

Secrets (API keys, tokens) should never be committed. Store them in GitHub Secrets, then pass them to scripts:

steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
turbo deploy --filter=web

Scripts read API_KEY and NPM_TOKEN from environment variables.

Notifying on Build Failures

Slack notifications on CI failure:

- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "Monorepo CI failed on ${{ github.ref }}",
"attachments": [
{
"text": "Failed job: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
}
]
}

Key Takeaways

  • Use GitHub Actions to automate building, testing, and deploying your monorepo on every push.
  • Detect affected packages with turbo build --filter "origin/main...HEAD" to skip unnecessary builds.
  • Enable Turbocloud caching in CI to share build artifacts across runs, reducing CI time by 50–80%.
  • Use matrix strategies to build and test multiple packages in parallel.
  • Upload build artifacts to reuse between jobs, avoiding duplicate builds.
  • Store secrets in GitHub Secrets and pass them as environment variables to scripts.

Frequently Asked Questions

How do I run CI only for pull requests?

Add if: github.event_name == 'pull_request' to the job. Or use the on trigger to specify which events trigger the workflow.

Can I skip CI for certain commits?

Yes, add [skip ci] to your commit message. GitHub recognizes this and skips the workflow.

How do I trigger a full rebuild when caching causes issues?

Add a "rebuild" button or trigger a workflow manually:

on:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ... rest of workflow

Then click "Run workflow" in GitHub Actions UI.

What is fetch-depth: 0?

This fetches full git history, allowing Turborepo to compare current branch against main accurately. Without it, --filter may not detect all affected packages.

How do I deploy to multiple environments (staging, production)?

Use environment-specific workflow files or conditional steps:

- name: Deploy to staging
if: github.ref == 'refs/heads/develop'
run: npm run deploy:staging

- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: npm run deploy:production

Further Reading