Mar 20, 2025·7 min read

Monorepo CI for faster daily shipping in large repos

Monorepo CI keeps large repositories fast by testing only changed services, using path filters, affected checks, and clear release rules.

Monorepo CI for faster daily shipping in large repos

Why full repo testing slows daily shipping

A large repo gets slow fast when every commit wakes every job. One small change in a shared config file, a web page, or a single backend service can trigger tests, builds, and checks for parts of the codebase nobody touched.

That sounds safe, but it usually wastes time. If 40 services live in one repo and a developer changes one of them, running all 40 test suites does not give better feedback. It gives slower feedback.

The queue is where the pain shows up first. A few small pull requests land in the morning, and each one starts the full pipeline. Runners fill up. Later commits wait behind work that has nothing to do with them. By the afternoon, a five minute check can turn into a 35 minute wait.

Teams feel that delay in their daily habits. People merge less often because each change drags behind it. They batch work together to avoid repeated waits, which makes pull requests larger and harder to review. When a test finally fails, the author has already switched tasks and lost context.

A full repo pipeline also creates noise. If unrelated jobs fail, developers have to scan pages of output to figure out whether their change caused anything. Trust in monorepo CI starts to fade. Once people expect slow, noisy results, they stop treating CI as a quick safety check and start treating it like background paperwork.

The pattern is easy to spot. Someone updates copy in one customer facing app, yet the pipeline rebuilds mobile packages, runs integration tests for internal tools, and checks deploy steps for services untouched by the edit. Nothing is wrong with those jobs. They just do not belong to that commit.

Fast feedback changes behavior. When checks finish quickly, developers push smaller commits, review more often, and fix problems while the code is still fresh in their heads. That is why selective testing matters. It saves runner time, but more than that, it keeps the team moving.

What path filters do

Path filters look at the files in a commit and decide which CI jobs should run. They do not guess whether code is safe. They answer a simpler question: "Which parts of this repo did this change touch?"

That simple check cuts a lot of waste in monorepo CI. If someone edits services/api/, the pipeline can run API linting, tests, and build steps for that service. If the change only touches apps/web/, the mobile jobs should stay idle instead of burning another 15 minutes.

A clean setup maps folders and files to the jobs that depend on them. Teams usually start with obvious paths like API, web, mobile, and worker folders. Then they add rules for areas that often confuse pipelines, such as docs, CI config, infrastructure code, and shared packages.

Shared code needs more care. If packages/ui/ changes, web and mobile might both need checks. If packages/auth/ changes, the API, background jobs, and end to end tests may all matter. This is the point where path filters stop being "folder equals job" and become "dependency equals job."

Docs and config deserve their own rules too. A change in docs/ often needs no product tests at all. A change in CI config, Docker files, or deploy scripts may need a small set of verification jobs even when app code stays untouched.

In practice, the rules are usually straightforward. API code changes should run API tests and builds. Web only changes should run web checks and skip mobile work. Docs only changes can run spelling or markdown checks. Shared package changes should fan out to every app that imports that package.

Path filters are not something you set once and forget. Repos change, folders move, and teams split services or merge packages. If nobody reviews the rules after a restructure, the pipeline starts missing jobs or running too many of them. Both problems hurt daily shipping.

Good path filters make CI feel boring in the best way. Developers push a small change, the repo runs the jobs that match it, and main stays fast enough for daily releases.

How affected project detection works

Affected project detection starts with a plain question: which files changed in this commit or pull request? CI reads that file list first. If a change only touches services/billing, there is no good reason to run tests for the mobile app, docs site, and internal admin tool.

The next step is mapping each changed file to the project that owns it. In a monorepo, that usually means a service, package, app, or shared library. A file under apps/web belongs to the web app. A file under packages/auth belongs to the auth package.

That file to project match is only the start. You also need dependency rules, because one shared package can affect many projects. If the auth package changes and both the API and web app import it, CI should test all three. If a docs page changes, CI can skip product tests and move on.

The flow is simple: collect the changed files from the branch or pull request, match those files to their owning projects, walk the dependency graph to find projects that depend on them, and run build, test, and lint only for that affected set.

This works best when your repo has a real dependency map in code. Do not keep it in someone's head or in a stale wiki page. Store project boundaries and dependencies in workspace config, build files, or a small manifest that CI can read on every run.

Skipping unchanged projects should count as a normal result, not a failure and not a warning. That sounds minor, but it changes how teams read pipeline results. Green should mean "everything that could break was checked," not "we ran every job we have."

Imagine a growing product team with a web app, API, worker, and five shared packages. One pull request changes a retry helper used only by the worker. Good affected project detection runs the worker jobs, the helper package tests, and maybe one small integration check. It leaves the rest alone. That is how daily shipping stays fast without turning CI into guesswork.

A simple pipeline you can set up

Start with a map of the repo, not the CI tool. Write down every deployable app, every service, and every shared package. If a folder can break production or block a release, CI should know that folder belongs to a project.

Start with clear folder rules

Most teams try to get clever too early. That usually makes the first version fragile. Begin with plain path rules for the folders everyone already understands, such as apps/web, services/billing, or packages/ui.

Then add one git diff step. Compare the branch against main, collect the changed files, and turn that into an affected list. That list becomes the input for the rest of the pipeline.

A practical setup often looks like this:

  1. Detect changed files with git diff.
  2. Match those files to projects through path rules.
  3. Expand the list when a shared package changed and downstream apps depend on it.
  4. Run lint, tests, and builds only for affected projects.
  5. If nothing important changed, run a small smoke check and stop there.

This works because it handles the obvious cases first. A docs edit should not build ten services. A change in packages/auth probably should test every app that imports it.

Do not skip the safety net. Keep one scheduled full repo run, usually once per night or once per day. That job catches bad path rules, missed dependencies, and edge cases that selective testing can miss.

If the scheduled full run fails, fix the mapping before you trust the selective jobs again. That discipline matters more than the exact CI product you use. A modest setup with clear rules is often enough to make a large monorepo feel fast again.

Release rules that keep main stable

Talk Through Your CI
Oleg can review repo structure, filters, and release logic with your team.

Fast monorepo CI only works if merge and release rules stay strict. When the pipeline skips work, the contract has to stay clear: every affected project must pass before code reaches main, and every shared change must fan out to the apps that depend on it.

Merge gates

A pull request should not merge until CI proves the changed code and its dependents still work. Path filters help decide where to start, but they are not enough on their own. If a developer edits a shared auth package, the API, worker, and admin app that use it all need rebuilds and tests, even if their folders did not change.

That rule keeps main boring, which is exactly what you want. Teams get into trouble when they test only the folder that changed and ignore the dependency graph. The pull request looks green, then the next deploy fails because a downstream app broke.

A simple policy is enough for most teams. Block merges unless every affected app, package, and test suite passes. Expand the affected set when a shared library, schema, or build config changes. Treat CI config, Docker files, and root package files as repo wide changes. Let docs only edits skip most checks, but only when the rule is explicit.

Release triggers

Releases should stay selective too. If one service changed, publish that service and leave the rest alone. That keeps deploy time short and avoids pointless version bumps across the repo.

Tags help when they match the repo structure. If apps/api ships as api-v1.8.2 and apps/web ships as web-v3.4.0, nobody has to guess what each tag refers to. The same idea works for shared packages. Clear tag names make rollbacks and audits much easier.

One case deserves a wider check: major dependency updates. If you bump a framework version, change a base image, or swap a build tool, run a full release check across the whole repo. That costs more on that one change, but it saves you from finding breakage after release.

This is where monorepo CI earns trust. It skips work on normal commits, then goes wide only when a change can spread across the repo. That balance is what lets a team ship every day without turning main into a gamble.

A realistic example from a growing product team

Picture a team with one repo for several apps and shared packages. On a Tuesday morning, a developer updates the checkout screens in the web app and changes one shared pricing library that calculates discounts and tax rules.

In a slow pipeline, that small change would wake up every job in the repo. Search would build. Admin would test. Mobile would run. Half the work would have nothing to do with the change, and the team would wait anyway.

With monorepo CI, the repo reacts to what actually changed. Path filters notice edits in the web checkout area and in the pricing package. Affected project detection then checks which projects depend on that pricing code.

The result is a much smaller test plan: web app tests for checkout flows, pricing library unit tests, and a quick contract check between the web app and the billing API. Search, admin, and mobile stay idle because neither the changed paths nor the dependency graph points to them.

That last part matters. Skipping jobs is not guesswork. The repo already knows which code each project uses, so it can avoid unrelated work for a clear reason.

The contract check matters more than people expect. The checkout UI may still pass its own tests while sending the wrong payload to billing after a pricing change. A quick check at that boundary catches the kind of break that often slips through local tests.

Release rules stay narrow too. When the branch merges, the pipeline publishes a new web app build and releases the pricing package. It does not version the admin panel. It does not rebuild mobile artifacts. It leaves the search service alone.

That keeps the release clean. The deploy note is short, the rollback path is obvious, and the team knows exactly what moved.

A setup like this can turn a 45 minute wait into 10 or 12 minutes. That difference changes behavior. People stop batching unrelated work together just to avoid another long CI run. They ship the checkout fix the same day, verify the pricing package release, and move on to the next task instead of babysitting a full repo pipeline.

For a growing team, that is usually the real win. Selective testing saves compute, but the bigger gain is pace. Small changes stay small all the way to production.

Mistakes that break selective CI

Improve Repo Infrastructure
Get help with GitLab, runners, observability, and lean delivery workflows.

Selective CI saves time until one small rule goes stale. Then the pipeline starts giving false comfort: a commit looks safe, jobs stay green, and a broken service shows up later when someone tries to ship.

Shared code causes most of the damage. Teams often write neat path filters for each app, but they forget folders like packages/auth, libs/ui, shared schemas, or build scripts. One change in a common package can affect many services at once. If those downstream tests never run, the bug slips through with a green checkmark.

Renames create a quieter problem. A team moves packages/core to libs/core, updates imports, and merges the refactor. The CI rules still watch the old path. Nothing looks broken in the pipeline, but the new folder no longer triggers the jobs it should.

Generated files can waste just as much time. Codegen output, lockfiles, snapshots, and generated SDKs often touch many directories in one commit. If your rules treat those files like normal source code, they wake up jobs nobody needs. Soon the fast pipeline starts to feel slow again.

A few habits prevent most of this. Map shared folders to every service that depends on them. Review filters after every rename or folder move. Exclude generated output unless it changes runtime behavior. Test skip logic during refactors, not a week later.

Release rules drift away from the repo faster than teams expect. A service may publish only when its own folder changes, even though it depends on shared API contracts, migrations, or generated clients. The repo stays green, but the release pulls in a mismatch. That is how teams end up debugging "random" breakage that CI should have caught.

Big repo cleanups make all of this worse. When engineers split packages, merge services, or reorganize directories, they usually review application code and forget the pipeline logic. Monorepo CI needs the same level of review as the code it protects.

Treat selective CI rules like product code. Put them in code review, add tests for them, and give one person or team clear ownership. If nobody owns the rules, the rules stop matching reality.

A quick checklist before you trust it

Tighten Merge Gates
Set merge rules that protect main without waking the whole repo.

Selective CI saves time only when people can tell why the pipeline behaved the way it did. If jobs seem random, the team stops trusting the results and starts asking for full runs again.

Before you rely on monorepo CI for daily shipping, check these five points:

  • A developer can explain every run or skip in under a minute.
  • Shared libraries follow clear dependency rules.
  • A scheduled full run still passes.
  • The release script can name the changed service without guessing.
  • CI logs print the affected project list early.

Teams often miss the second and fifth checks. They add path filters, see faster builds, and assume the problem is solved. Then a shared package changes, one service skips tests, and the bug reaches main because nobody saw the affected set clearly in the logs.

A simple rule helps: if a new engineer cannot read one commit and explain the pipeline result, the setup is not ready. Fast pipelines are nice. Clear pipelines are the ones teams keep using.

What to do next

Pick the slowest pipeline in your repo and fix that one first. Teams usually fail when they try to redesign CI for every service at once. Start where the pain is easy to see, such as a service that waits 20 minutes for test results after a tiny change.

Keep the first rollout easy to debug. A path filter that only watches one folder is easier to trust than a dense set of shared rules, tags, and exceptions. If someone asks why a job ran or did not run, you should be able to answer in one plain sentence.

Track a few numbers from the start: queue time before jobs begin, total test time for a normal commit, skipped jobs that should have run, and release errors after merge. Those numbers matter more than team gut feelings. If queue time drops and release errors stay flat, your monorepo CI is moving in the right direction. If skipped jobs start leaking bugs, stop adding new rules until you find the exact condition that caused the miss.

Give the setup two weeks of real commits before you tighten it. Real history reveals awkward cases fast: shared packages, schema updates, generated files, and release branch quirks. After that, trim noisy jobs, add missing dependency edges, and write down the exceptions so nobody has to guess.

A growing team might start with just one rule: frontend changes run frontend tests, API changes run API tests, docs changes run almost nothing. That alone can cut waste without making the pipeline hard to explain. Later, the team can add affected project detection for shared code and stricter release rules for main.

If your team wants a second opinion, Oleg Sotnikov at oleg.is works with startups and smaller companies as a Fractional CTO and often reviews repos, CI setups, and release flows. That kind of outside review can help you cut waiting time without making the pipeline fragile.

Do not chase a clever setup. Use one your team can explain on a whiteboard and fix under pressure. That is usually the point where selective testing starts saving time instead of creating fresh confusion.

Frequently Asked Questions

What does selective CI mean in a monorepo?

Selective CI runs checks only for the apps, services, and packages your commit touches. It keeps feedback fast, cuts queue time, and helps developers ship smaller changes without waiting for unrelated jobs.

How are path filters different from affected project detection?

Path filters look at changed files and match them to folders or projects. Affected project detection goes one step farther and follows dependencies, so a change in a shared package also triggers checks for every app that imports it.

Can docs-only changes skip most tests?

Skip product tests when the change really stays inside docs and does not touch shared code, build files, or deploy scripts. Run a small docs or markdown check so the pipeline still verifies something useful.

What changes should trigger a wider pipeline?

Treat root config, CI files, Docker files, base images, shared schemas, and build tool changes as repo-wide unless you know the exact impact. Those files often affect many projects even when app folders stay untouched.

How should I handle shared packages in CI?

Map each shared package to every app and service that depends on it. Keep that dependency map in code or workspace config, then let CI expand the affected set from the shared package to its downstream projects.

Do I still need a full repo run?

Run one full repo pipeline on a schedule, usually nightly or once a day. That job catches stale rules, missing dependency edges, and folder moves before they turn into release bugs.

How do I debug why a job ran or got skipped?

Start with logs. Print the changed files, the matched projects, and the final affected list near the top of every run. When people see that data early, they can explain most run and skip decisions in seconds.

What usually breaks selective CI?

Teams usually forget shared code, miss folder renames, or treat generated files like source code. Those mistakes either skip checks that should run or wake up too many jobs and bring the slow pipeline back.

Should release rules use the same affected logic?

Yes. If one service changed, release that service and leave the rest alone. When a shared library, schema, or framework upgrade spreads across the repo, widen the release checks for that change before you ship.

What is the best first step to adopt this setup?

Pick the slowest part of the repo and add simple folder rules first. Compare the branch against main, build an affected list from the diff, and keep the first version easy enough to explain on a whiteboard.

Monorepo CI for faster daily shipping in large repos | Oleg Sotnikov