A practical pattern for monorepo CI with path filters, matrix builds, caching, and deployment guards that keep feedback fast as teams scale.
Monorepos improve shared ownership and cross-service refactoring, but CI pipelines can become slow and expensive when every pull request runs every job. The fix is not one giant workflow file. The fix is a clear build contract with targeted execution, predictable caching, and strict release gates.
Use three layers:
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
detect:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.set.outputs.services }}
steps:
- uses: actions/checkout@v4
- id: set
run: |
CHANGED=$(git diff --name-only origin/main...HEAD | cut -d/ -f1,2 | sort -u)
SERVICES=$(echo "$CHANGED" | grep '^services/' | cut -d/ -f2 | jq -R -s -c 'split("\n")[:-1]')
echo "services=$SERVICES" >> $GITHUB_OUTPUT
This keeps you from building unrelated applications when docs or other folders change.
test:
needs: detect
if: ${{ needs.detect.outputs.services != '[]' }}
strategy:
matrix:
service: ${{ fromJson(needs.detect.outputs.services) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint --workspace=services/${{ matrix.service }}
- run: npm run test --workspace=services/${{ matrix.service }}
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
Pair this with protected environments and manual approvals for production.
With this model, monorepo CI stays fast as service count grows.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A production-focused guide to Azure DevOps: standardized YAML templates, secure service connections, rollout safety, and measurable delivery reliability.
How to implement Backstage with real templates, scorecards, and golden paths so internal platform work reduces delivery friction.
Explore more articles in this category
Every hook on this list caught a bug or a security issue in the last twelve months. The configs are short. The savings have been considerable.
We've been running the OTel Collector at the edge of every cluster for 18 months. The config patterns that lasted, the ones we ripped out, and a few processors that quietly saved us money.
Blue/green is easy for stateless services. We did it for our primary Postgres cluster with 3.2TB of data and ~8k connections. Here's exactly how — and what almost went wrong.