Monorepo Deploy Pipeline
Learn to build a deploy pipeline for a monorepo in this hands-on CI/CD foundations tutorial. Step-by-step guidance from problem to solution, with practical exercises, troubleshooting, and next steps.
Focus: build a deploy pipeline for a monorepo
Shipping multiple applications from one repository sounds efficient — until your CI/CD pipeline deploys the whole monorepo because one service changed. You burn minutes (or hours) rebuilding unchanged apps, trigger deployments that don’t need to happen, and pray that a typo in one package doesn’t take down production. This lesson shows you how to build a deploy pipeline for a monorepo that changes that story: pipelines that detect what actually changed, build only those projects, and deploy them safely — without turning your CI config into a tangled script.
The problem this lesson solves
A monorepo holds many independent services — an API, a worker, a frontend — all in one Git repository. The naive CI/CD approach treats the whole repo as a single deployable unit: every push triggers a full build and deploy of everything. That causes three pain points you’ve likely felt:
- Slow feedback loops — a change to a README rebuilds your entire backend.
- Unnecessary deployments — you redeploy services that didn’t change, increasing risk of regressions for zero benefit.
- Tight coupling — one failed build in a service blocks the pipeline for unrelated services, making deployments brittle.
Without a monorepo-aware deploy pipeline, you’re not practicing continuous delivery — you’re practicing continuous everything, and that’s a recipe for burnout and broken production.
Core concept / mental model
Think of your monorepo not as one big deployable, but as a shelf of jars. Each jar (service) has its own label, recipe, and shelf-life. When you add a new spice to one jar, you don’t need to re-cook every jar on the shelf — you only re-process the one that changed. Your deploy pipeline should mimic that: detect which jars changed, process only those, and leave the rest untouched.
In technical terms, the core idea is change-based filtering combined with path-scoped triggers. Your CI/CD tool watches specific directories (e.g., services/api/) and triggers the pipeline only when files in those paths change. Then, within the pipeline, you compare the current commit against the previous one (or use git diff) to decide which services to build and deploy.
Key definitions to keep in mind:
- Monorepo: a single repository containing multiple projects, often with shared tooling.
- Path filter: a rule that triggers a pipeline when files under a specific subdirectory change.
- Change detection: the logic inside the pipeline that determines which projects were affected by a commit.
- Deploy promotion: moving a built artifact from one environment (QA) to another (production), ideally with approval gates.
Pro tip: You don’t need fancy tools to build a monorepo deploy pipeline — just careful path rules and a bit of scripting. Every major CI/CD platform (GitHub Actions, GitLab CI, Jenkins) supports path filtering natively.
How it works step by step
Building a monorepo deploy pipeline follows a repeatable five-step pattern. Here’s the mental sequence:
-
Define your service inventory — list every deployable project in the monorepo and its root directory. For example:
services/api,services/worker,frontend/web. -
Set up path-scoped triggers — configure your CI/CD tool to run the pipeline only when files under those directories change. This is your first filter, saving resources on irrelevant commits.
-
Detect changes — within the triggered pipeline, run a
git diffagainst the previous commit or use the tool’s built-in “changed files” API to list which paths were modified. -
Build only affected services — based on the change detection, build and package only those services. Store the artifacts (Docker images, zip files) in a registry or artifact store.
-
Deploy with promotion — deploy the new artifact to your staging environment automatically, then optionally await a manual approval before promoting to production.
The cause → effect chain is simple: a commit touching only services/worker triggers a pipeline that builds and deploys only the worker, leaving the API and frontend untouched. This reduces deploy time from minutes to seconds and eliminates risky full-repo deployments.
Hands-on walkthrough
Let’s build a real monorepo deploy pipeline using GitHub Actions. We’ll create a minimal monorepo with two services: api and worker. We’ll use path filters to trigger separate jobs, and use dorny/paths-filter to detect changes for a unified pipeline approach.
1. Repository layout
monorepo/
├── services/
│ ├── api/
│ │ └── app.py
│ └── worker/
│ └── worker.py
└── .github/workflows/deploy.yml
2. Define a reusable workflow with path triggers
Create .github/workflows/deploy.yml with two jobs — one per service — and path filters:
name: Deploy Monorepo Services
on:
push:
branches: [main]
paths:
- 'services/api/**'
- 'services/worker/**'
jobs:
deploy-api:
if: ${{ contains(github.event.commits.*.modified, 'services/api/') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy API
run: |
echo "Deploying API service..."
# In a real pipeline, run your deploy script here
# e.g., ./scripts/deploy.sh api
echo "API deployed successfully!"
deploy-worker:
if: ${{ contains(github.event.commits.*.modified, 'services/worker/') }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy Worker
run: |
echo "Deploying Worker service..."
# e.g., ./scripts/deploy.sh worker
echo "Worker deployed successfully!"
Expected output on a commit that touches only services/api/app.py: the deploy-api job runs, deploy-worker is skipped (no log output), and you see API deployed successfully!.
3. A unified pipeline with change detection
But what if you have many services and don’t want to write a separate job for each? Use the dorny/paths-filter action to build a dynamic matrix:
name: Deploy Changed Services
on:
push:
branches: [main]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.filter.outputs.changes }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
api: services/api/**
worker: services/worker/**
deploy:
needs: detect-changes
if: ${{ needs.detect-changes.outputs.services != '[]' }}
runs-on: ubuntu-latest
strategy:
matrix:
service: ${{ fromJSON(needs.detect-changes.outputs.services) }}
steps:
- uses: actions/checkout@v4
- name: Deploy ${{ matrix.service }}
run: |
echo "Deploying ${{ matrix.service }}..."
# Run your service-specific deploy script
./scripts/deploy.sh ${{ matrix.service }}
Expected output: if only the worker directory changes, the detect-changes job outputs [\"worker\"], and the deploy job runs once with service: worker. The API job is not even created.
Pro tip: Always fetch at least 2 commits in your checkout step so
git diffand path filters have a reference point. For very large monorepos, consider using a Git sparse checkout to speed up checkout.
Compare options / when to choose what
There are several ways to build a monorepo deploy pipeline. Here’s a quick comparison to help you choose:
| Approach | Pros | Cons | Best for |
|---|---|---|---|
| Separate jobs with path triggers | Simple, built-in, no external actions | Repetitive, hard to scale to many services | 2–3 services, simple monorepo |
| Unified job with change detection (paths-filter) | Single job, scales via matrix, less duplication | Requires third-party action, slight complexity | 4+ services, frequent changes |
Custom script with git diff |
Full control, no dependencies | You reinvent the wheel, error-prone | Specialised needs, offline environments |
| Monorepo build tools (Nx, Bazel, Turborepo) | Advanced caching, dependency awareness | Heavy learning curve, new tooling | Very large monorepos with complex dependencies |
When to choose what:
- Start with separate path-triggered jobs if you have only a few services and want zero dependencies.
- Move to change-detection with matrix once you add more services — it keeps your YAML DRY.
- For enterprise-scale monorepos with hundreds of packages, invest in a build orchestration tool like Nx or Bazel, but integrate them with your CI/CD’s change detection for the best of both worlds.
Troubleshooting & edge cases
Even with a solid pipeline, you’ll hit these common issues:
- Path filters not triggering — Double-check your glob patterns.
services/api/**matches files and subdirectories. If your services are deeper, adjust the pattern. Also ensurepushis the correct event —pull_requestevents often require different filter syntax. - Change detection misses renamed files — Git rename detection can treat a rename as delete+add. Use
git diff --name-statuswith-Mto detect renames, or rely on CI tools that handle renames natively. - Pipeline runs for a README change — If you use a single job without path filters, every push triggers a full build. Add a top-level
pathsfilter to skip docs changes, or exclude**/*.mdfrom triggers. - Deploy conflicts between services sharing a database — If two services deploy simultaneously and both run migrations, you can get deadlocks. Serialise deployments with a lock or a shared deployment queue.
- Matrix job skips all — no deploy happens — If your
needsoutput is empty, the matrix job won’t run. Add anifcondition and a fallback job to notify you when no services are affected.
Pro tip: When debugging, inspect the
github.event.commitspayload to see exactly which files GitHub reports as changed — it sometimes includes files outside your path filters (like workflow files).
What you learned & what's next
You’ve now learned how to build a deploy pipeline for a monorepo that is fast, focused, and safe. In this lesson, you:
- Understood the problem of full-repo deployments and why change-based pipelines are better.
- Built a mental model of monorepo as a shelf of independent jars, each deployable on its own.
- Went step-by-step through path triggers, change detection, and matrix deployments.
- Saw two working GitHub Actions examples and compared them with alternative approaches.
- Anticipated and solved common troubleshooting edge cases.
Now you’re ready to apply this pattern in your own projects. The next lesson in this track explores deployment strategies — blue-green and canary — so you can take your monorepo pipelines from “deploy successfully” to “deploy safely with zero downtime." Open your terminal, copy the repository layout, and build your first monorepo deploy pipeline today.
Practice recap
Set up a minimal monorepo with two services (api and worker) on GitHub, then implement a workflow with path filters for each. Push a change to only one service and verify the other job is skipped. Then refactor to use dorny/paths-filter with a matrix deployment — challenge yourself to add a third service and see how quickly the pipeline scales.
Common mistakes
- Triggering the pipeline on every push without path filters, causing full monorepo builds for documentation-only changes — always add path filters to your on.push event.
- Forgetting to fetch enough git history (fetch-depth: 2) so change detection has a previous commit to compare against, leading to incorrect 'changed files' results.
- Applying the same deployment steps to every service without considering service-specific dependencies (like database migrations), which can cause conflicts when deploying multiple services simultaneously.
- Using a single job with no matrix, so one failing service blocks deployment of all other unaffected services — separate jobs or a matrix with per-service error handling is critical.
- Not excluding CI/CD workflow files from your path filters, so editing .github/workflows triggers an unexpected deployment of all services.
Variations
- Use GitLab CI with rules:changes and a dynamic child pipeline to build only affected services instead of GitHub Actions.
- Integrate a monorepo build tool like Nx or Turborepo to automatically detect affected projects and cache build outputs, then feed that into your CI/CD deploys.
- Implement a custom deploy script that uses
git diff --name-onlyagainst the previous tag, giving you full control over change detection in any CI environment.
Real-world use cases
- A SaaS company with a monorepo containing a web app, API, and background workers uses change-based pipelines to deploy only the service that changed, cutting deploy time from 30 minutes to 5.
- A fintech startup runs separate path-triggered pipelines for a payments service and a reporting service in the same monorepo, ensuring a bug in one never blocks the other's release.
- An e-commerce platform with a large monorepo uses Nx in CI to detect affected packages and deploys only the microservices for the checkout flow, reducing infrastructure costs during peak sales.
Key takeaways
- The pain a monorepo deploy pipeline solves: unnecessary full-repo builds, slow feedback, and risky coupled deployments.
- The mental model: treat each service as an independent deployable jar on a shelf — only re-cook the jar that changed.
- Steps: inventory services, set path triggers, detect changes, build affected services, deploy with promotion gates.
- GitHub Actions supports path filters and matrix deployments for scalable monorepo pipelines; choose the approach based on service count.
- Troubleshooting is about precise glob patterns, correct git history depth, and serialising database migrations.
- Next: apply deployment strategies like blue-green or canary to make your monorepo deploys even safer.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.