Monorepo Pipelines in GitLab: Only Build What Actually Changed
A monorepo that rebuilds everything on every commit is a tax on every developer. Here's how I use rules:changes and child pipelines to build only the affected services.
- #gitlab
- #cicd
- #monorepo
- #child-pipelines
- #rules
- #scaling
Monorepos are wonderful right up until CI. One repo, twelve services, and a single typo in a README triggers a 50-minute pipeline that rebuilds and redeploys everything. I’ve watched teams adopt a monorepo for the developer experience and then accidentally destroy that experience with a naive pipeline.
The fix is conceptually simple: only do work for the things that changed. Getting GitLab to actually behave that way takes a couple of specific techniques. Here’s how I build monorepo pipelines that stay fast no matter how many services land in the repo.
The core idea: change detection
Everything starts with rules:changes. A job should only run when files it cares about were modified:
build-payments:
rules:
- changes:
- "services/payments/**/*"
- "libs/shared/**/*"
script: ./build.sh payments
Note that payments also rebuilds when libs/shared changes — because it depends on that library. Mapping these dependencies correctly is the heart of monorepo CI. Get it wrong and you either skip work you needed (broken deploys) or do work you didn’t (slow pipelines).
The two failure modes
There are exactly two ways to get monorepo change detection wrong:
- Too broad — you trigger more than necessary and you’re back to rebuilding everything. Annoying but safe.
- Too narrow — you skip a service that actually depended on the change. Fast but dangerous: you ship a build that was never tested against the change that affects it.
I lean slightly toward “too broad” when in doubt, because a wasted build is cheaper than a broken deploy. But the real answer is to model dependencies explicitly.
Scale up with child pipelines
rules:changes on a flat pipeline works for a handful of services. Past that, a single .gitlab-ci.yml with rules on every job becomes unreadable. This is where child pipelines earn their place: one per component, each owning its own config.
The parent pipeline decides which children to trigger:
payments:
trigger:
include: services/payments/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- "services/payments/**/*"
billing:
trigger:
include: services/billing/.gitlab-ci.yml
strategy: depend
rules:
- changes:
- "services/billing/**/*"
Now each team owns the .gitlab-ci.yml inside their service directory. The parent is just a router. This decentralization is what keeps a 30-service monorepo’s CI maintainable — no single giant file, no one team bottlenecking everyone else.
Generate the parent dynamically
The most powerful pattern is dynamic child pipelines: a job that generates the parent pipeline YAML based on what changed, then triggers it:
generate-pipeline:
stage: build
script:
- ./ci/detect-changes.sh > generated-pipeline.yml
artifacts:
paths:
- generated-pipeline.yml
run-pipeline:
stage: build
needs: ["generate-pipeline"]
trigger:
include:
- artifact: generated-pipeline.yml
job: generate-pipeline
strategy: depend
Your detect-changes.sh diffs against the target branch, figures out which services are affected (including via the dependency graph), and emits only the jobs that matter. This scales to hundreds of components because the pipeline is exactly as big as the change.
Diff against the right base
A subtle but critical detail: rules:changes compares against different bases depending on context. On a merge request it diffs against the target branch (good). On a plain branch push it can diff against the previous commit (often not what you want). For monorepos, run change detection in merge-request pipelines and diff against the MR target branch so you catch everything in the MR, not just the last commit.
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Don’t forget shared code
The thing that bites everyone: a change to a shared library should rebuild every service that depends on it. Maintain an explicit dependency map — even a simple JSON file — and have your detection script expand “shared lib changed” into “all dependents changed.” Without this, you’ll skip a service that needed rebuilding and find out in production.
Watch the metrics
Two numbers tell you if your monorepo CI is healthy:
- Average jobs per pipeline — if it’s not dropping as the repo grows, your change detection isn’t working.
- Pipeline duration distribution — most pipelines should be fast (small changes), with only occasional long ones (shared-lib changes).
If every pipeline takes the same long time regardless of what changed, your detection is too broad and you’ve effectively got a non-monorepo pipeline wearing a costume.
Where AI helps
The dependency mapping is where mistakes hide. I paste my service layout and the rules:changes blocks into a model and ask: “If libs/shared changes, which services should rebuild, and does my config actually trigger all of them?” It catches missing dependency edges — the exact bug that ships an untested service. I keep GitLab CI prompts for monorepo CI reviews, and run pipeline-generation scripts through our Code Review tool before they go live.
The payoff
Done right, a monorepo pipeline does work proportional to the change, not the repo. A one-line fix runs one fast pipeline; a shared-library change runs everything, as it should. The repo can grow to fifty services and a typo fix still finishes in two minutes.
That’s the deal a monorepo is supposed to give you. Change detection and child pipelines are how you actually collect on it.
AI suggestions for change detection and dependency mapping are assistive, not authoritative. Always verify that shared-code changes trigger every dependent service in a real pipeline run.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.