CI/CD for a monorepo: where the build breaks
A monorepo wins at the start and begins to slow releases as it scales.
The first bottleneck is building the entire package tree on every change: an edit in one service pulls tests, linters, and the build of the rest, and build time grows with the number of packages, not the size of the edit.
Waiting for the build before a release becomes part of the release cycle rather than its preparation.
The cause is usually not agent capacity but the absence of an impact graph. Until tasks know which packages they depend on, the scheduler cannot skip untouched branches and reuse results from previous runs.
A cache keyed by input hashes — files and dependency versions — returns a result in seconds if the inputs have not changed.
Next are dependency version conflicts. A shared library bumps a major version, some packages remain on the previous one, and two incompatible copies live in the same tree.
Locally the build passes, but in shared steps the types and behavior of utilities diverge: you have to search in a package the author of the edit did not touch.
A separate pain is failing checks on other packages. A test unrelated to the edit fails, the branch is not merged, the trunk is red — and workarounds appear in the form of disabled steps.
Below we discuss how this is addressed: an impact graph, remote cache, and rules for versions of shared modules.
The effect of a single pipeline: speed and predictability
The first tangible effect is building affected packages instead of the whole repository. The pipeline builds a dependency graph and, for each change, runs only tasks whose inputs changed: an edit in one package no longer triggers a rebuild of the entire tree.
The build result cache works by the hash of task inputs — files, environment variables, tool versions — so a repeated run on the same revision takes a ready artifact instead of recomputing.
Hence the second consequence — release reproducibility. Bazel and build systems of the same class require explicitly described inputs for each target, so the result stops depending on what already exists on a particular builder machine.
The same commit produces an identical artifact for the developer, in the pipeline, and in the staging environment: discrepancies are caught during checks, not in the live environment.
The third metric, which is usually not counted, is the share of false failures. While the build depends on task order and a warmed-up cache on a single builder, the pipeline goes red for no reason, and the team gets used to restarting blindly. A clean graph and isolated steps remove this: a red result again means a real breakage.
Recovery time after a failure is reduced by the same cache and rollback to the previous revision — the pipeline repeats only the failed branch, not the entire path from scratch.
The result on a typical change: build time drops by an order of magnitude, result repeatability stops being a matter of faith, and a failed release is rolled back and replayed predictably.
Implementation order: from audit to handover
The order of work is not arbitrary: each next step relies on data from the previous one. First we capture facts — build time by task, composition of repeated runs, cache behavior — and only then do we change the configuration. This way, pipeline edits rely on measurements, not assumptions about where it "is slow".
- Audit of build steps. We run the current pipeline, measure the time of each task, and calculate how much work is performed repeatedly when files are unchanged.
- Package dependency scheme. We build a graph: what depends on what, which packages build in parallel, where cyclic links prevent splitting into independent tasks.
- Design of run rules. We fix which tasks are triggered for a changed package and its dependents, how the cache key is calculated, and what happens on branches and release tags.
- Configuration of build environments. We bring tool versions on agents to a single set so that local runs and the pipeline produce the same result and failures are reproducible.
- Validation on real changes. We run the pipeline on actual commits: an edit in a leaf package, an update of a shared library, a rollback to a previous release.
- Handover of the pipeline to the team. We hand over configurations with comments, review typical failures and the procedure for fixing them, open access, and supplement the documentation.
What remains in the repository after implementation
The customer receives not a "configured server" but a set of artifacts that the team can use to evolve the build without us — for a monorepo on Turborepo, Nx, or Bazel. Each item lives in the repository next to the code and goes through review like a regular change.
- Pipeline configuration — which tasks run on a change, what is cached between runs, which steps run in parallel and which only on the main branch, how to add a new task.
- Dependency scheme — a graph of packages and applications: what depends on what, which tasks are affected by an edit to one file, where the boundary between shared libraries and products lies.
- Required check rules — what blocks a merge, what works as a warning, on which paths it triggers, who and under what conditions can bypass the block.
- Secret access policies — where keys are stored, which tasks and branches can see them, what is closed to requests from external copies, how rotation is performed.
- Release procedure — package versioning, order of publishing artifacts, rollout sequence, and rollback to the previous release.
- Team documentation — repository structure, typical operations (add a package, update a shared library, speed up the build), and troubleshooting for common errors.
We hand over the set with a walkthrough: we show what is where and how to change it, so that the pipeline does not turn into a black box available only to the authors of the setup.
Case: reducing build time on a large repository
A monorepo with 42 packages and four product teams: every edit in a shared module triggered a full CI/CD run that took 55 minutes. Developers stopped waiting for the result and moved on to other tasks, while the run queue grew faster than changes were closed.
What it was
The pipeline built and checked all packages on every commit, artifact cache was not reused between runs, and static analysis, type checking, and integration checks ran as a single stage.
About a third of failures turned out to be false: flaky checks and environment differences — restarting "on luck" became a habit, and real errors were lost in the noise.
What was changed
We built a dependency graph and switched to Turborepo: only affected packages and what depends on them are built and checked.
We added a remote cache for builds and check results, separated stages into fast and heavy, pinned environments, and moved flaky checks into quarantine with automatic task labeling.
What became
The full run decreased from 55 to 14 minutes, and a typical run for one edit — to 3–4 minutes. The share of false failures fell from 30% to 4%: a check again became a signal rather than a reason to restart.
Rolling back a failed release takes about three minutes instead of forty minutes of manual actions — we return the previous build with one command.
How does CI/CD for a monorepo differ from building in parts?
In a set of separate repositories, the unit of change is the repository, so a task affecting several packages splits into several independent change sets. They have to be coordinated manually: merge order, version compatibility, release timing. In a monorepo, the unit of change is the entire graph.
The key difference is how the build scope is selected. Separate pipelines either build everything indiscriminately or do not see links between repositories and skip what is needed.
A monorepo builds a package dependency graph and computes the affected subset: the cache relies on the contents of a package and its dependencies, so only what actually changed is rebuilt.
A single version of shared libraries removes a separate class of failures. When packages live separately, a shared library diverges across versions and several variants of it live in production at the same time — incompatibility surfaces already in the running environment. In a monorepo, the library is one, and a change passes checks of all consumers before release.
Atomic release is a consequence of a single graph. A public interface change and all its consumers ship as one set, without a desynchronization window between them. Rollback returns a consistent state as a whole, not half of the system.
The price is pipeline discipline: splitting tasks by package, cache isolation, access rights at the package level, release versioning. Setup is more complex than a set of separate pipelines, but it is what gives predictable check time and result reproducibility.
Practical doubts: rollbacks, cache, environment isolation
Doubts around a pipeline in a monorepo are usually the same: one commit affects several packages at once, the cache speeds up the build but sometimes returns a stale result, and environments share one repository. Below are short answers to what is most often asked before starting.
How do you roll back a failed release?
Each release is an immutable artifact with a version tag. Rollback is switching the pointer to the previous artifact: no rebuild is needed, and the state returns in minutes.
We make schema migrations backward compatible so that the code of the previous version works with the current database.
What if the build cache returns an incorrect result?
We calculate the cache key from the contents of input files, configuration, and dependency versions — if any of them changes, the entry invalidates itself.
If a poisoned cache is suspected, we reset it for a specific branch, not globally: other builds do not lose the accumulated speedup.
How are environments separated?
The same artifact is promoted across environments without rebuilding — only the values of variables and secrets change. Builds in the test environment and in production are identical, so discrepancies do not surface at release time.
Who has access rights to the pipeline?
We separate access by roles: a developer runs checks in their branch, and only a protected branch with mandatory approval leads the release. Secrets of the production environment are unavailable to tasks from user branches.
Why do pre-merge checks take so long?
The required set builds and tests only affected packages along the dependency graph — that is minutes, not hours. The full run goes to the nightly schedule and does not block work on the next task.
Let's discuss the task and come back with an implementation plan
To come back with an implementation plan rather than general considerations, we need a minimum of input data from your side. Nothing needs to be prepared in advance — what is already in the repository and task tracker is enough.
- repository structure: where packages and applications are located, how dependencies between them are connected;
- current build and test time — from build logs, even if they are run manually;
- release bottlenecks: which steps wait the longest and how many manual actions per release;
- branching rules and the list of environments where a change goes after merging;
- read access to the repository, build system, and image registry — for measurements.
Using this data, we reproduce the build picture and look at which tasks are rebuilt unnecessarily, where the check chain runs sequentially although it could run in parallel, and at which step the release loses the most time.
- a dependency graph diagram with a note about which steps are parallelized;
- implementation plan: build cache, incremental checks, remote task execution, release;
- timeline estimate for each stage and the order of safely enabling them;
- a list of places that need changes to package configurations and places that need agreements within the team;
- result verification metrics: build time, share of reused cache, time to release.
Describe the task — which applications are in the monorepo and what currently prevents releasing more often. We will come back with a timeline estimate and a plan that can be enabled in stages.







