Mastering rules:changes in GitLab CI: Path-Scoped Pipelines That Don't Lie
rules:changes can cut wasted CI dramatically — or silently skip the tests that matter. Here's how to path-scope pipelines correctly without dangerous false negatives.
- #gitlab
- #cicd
- #rules
- #monorepo
- #performance
- #devops
rules:changes is one of the highest-leverage features in GitLab CI and one of the most dangerous. Used well, it stops you from running the entire test suite because someone fixed a typo in a README — real time and money saved on every push. Used carelessly, it skips the tests that would have caught a breaking change, and you find out in production. The whole point of CI is to not lie to you about whether your code is safe to ship, and a sloppy changes rule makes it lie.
I’ve both saved hours with this feature and been burned by it. Here’s how to get the savings without the false confidence.
What rules:changes actually evaluates
rules:changes includes or excludes a job based on whether files matching a glob were modified in the pipeline’s diff. The classic use is “only run the frontend tests if frontend files changed”:
frontend-test:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- "frontend/**/*"
script:
- npm --prefix frontend test
Simple enough. The danger is hiding in what diff it compares against, and that varies by pipeline type. For merge request pipelines, GitLab compares against the merge base — generally what you want. For branch pipelines, it compares against the previous commit on the branch, which is a completely different and often surprising baseline. This mismatch is the source of most changes bugs.
Always pair changes with the right pipeline source
The fix for the comparison-baseline problem is to be explicit about which pipelines a changes rule applies to. Scope it to merge request events, where the comparison is sane:
frontend-test:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
paths:
- "frontend/**/*"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
script: npm --prefix frontend test
The newer changes:paths (and changes:compare_to) syntax lets you pin the comparison branch explicitly, removing the ambiguity entirely:
frontend-test:
rules:
- changes:
paths:
- "frontend/**/*"
compare_to: "refs/heads/main"
script: npm --prefix frontend test
compare_to: main says “consider a file changed if it differs from main,” which is deterministic regardless of pipeline type. For monorepo path-scoping, this is the safe default.
The false-negative trap
Here’s the failure that turns a cost optimization into an outage. You scope your service-A tests to services/a/**. Someone changes a shared library in libs/common/ that service A depends on. Your rule sees no change under services/a/, skips service A’s tests, the MR goes green, and the shared-library change breaks service A in production. The pipeline didn’t fail — it lied.
The defense is to make your changes globs follow your dependency graph, not just your directory layout:
service-a-test:
rules:
- changes:
paths:
- "services/a/**/*"
- "libs/common/**/*" # shared dep — changes here must test A
- "go.mod" # dependency changes affect everything
compare_to: "refs/heads/main"
script: ./test.sh services/a
Whenever a job is scoped by path, ask: “What else could change that would break this job?” Shared libraries, lockfiles, base Dockerfiles, CI config itself. Each of those belongs in the glob. Omitting them is how you build a pipeline that’s fast and wrong.
Belt and suspenders: full runs on the integration branch
Path-scoping is for the fast feedback loop on MRs. It is not where you should bet your release. Run the full, unscoped suite on main and before release so that even if an MR’s changes rule had a gap, you catch it before it ships:
full-suite:
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_COMMIT_BRANCH == "main"
script: ./run-everything.sh
This is the pattern that lets you be aggressive with path-scoping on MRs without losing sleep: fast, scoped feedback per merge request; thorough, unscoped verification on the integration branch and on a nightly schedule. The scoped run is an optimization; the full run is the safety net.
Don’t path-scope safety-critical jobs at all
Some jobs should run on every pipeline regardless of what changed: security scanning, license checks, the lint that enforces your commit conventions, anything compliance depends on. Don’t changes-scope those. The handful of seconds you save isn’t worth a security scan silently not running on the MR that introduced the vulnerability. Reserve path-scoping for test execution and build jobs where skipping is genuinely safe.
Debugging “why did this job (not) run”
When a changes rule misbehaves, work through this checklist:
- Check the pipeline type. MR pipeline vs. branch pipeline changes the comparison baseline. If you didn’t set
compare_to, this is your first suspect. - Verify the glob.
**/*and**behave differently; a missing/matters. Test your pattern against the actual changed file list. - Watch the first-push case. On a brand-new branch with no prior commit to compare,
changesrules can behave unexpectedly.compare_toa stable branch avoids this. - Remember rules are first-match-wins. The first rule in the list whose
if/changesmatches decides the job. Order matters; a broad early rule can mask a specific later one.
Where to go from here
rules:changes pays for itself the first day you stop running a full suite on a docs-only change. But it earns its danger pay too: make your globs follow dependencies not just directories, pin the comparison with compare_to, never path-scope security or compliance jobs, and always keep an unscoped full run on main as the net. Get those right and you have a pipeline that’s fast and honest.
For more on monorepo CI and pipeline efficiency, see our GitLab CI/CD guides. And when reviewing changes to your rules blocks, our AI code review assistant is good at spotting the dependency gaps that turn path-scoping into a false negative.
changes comparison behavior depends on pipeline type and GitLab version. Validate your rules against real diffs before trusting them to gate releases.
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.