Mapping GitLab CI rules to branch and path changes Jump to heading
GitLab decides which jobs run through rules: — ordered conditions on variables, changed files and pipeline source. Getting them right turns a monorepo pipeline from “everything, every time” into “what this change touched”. Getting them wrong produces familiar problems: two pipelines for every push to a merge request branch, a changes: rule that runs every job on a new branch because there is nothing to compare against, or a required job that is skipped and leaves the merge request blocked. This page maps the common intents — per-branch jobs, per-path jobs, merge-request versus branch pipelines — to rules that behave predictably, as the GitLab side of CI/CD pipeline trigger mapping.
When to use this approach Jump to heading
- Your repositories are on GitLab and pipelines run more jobs than a change needs.
- You see duplicate pipelines — one branch pipeline and one merge request pipeline — for every push.
changes:rules behave unexpectedly on new branches or tags.- You are porting path filters from another CI system, such as those in optimizing CI triggers for path-specific changes.
Step 1 — Decide when pipelines exist at all with workflow rules Jump to heading
workflow: rules: decide whether a pipeline is created. The most common setup creates merge request pipelines for branches with an open merge request, branch pipelines otherwise, and avoids both for the same push.
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never # branch has an MR: skip the duplicate branch pipeline
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_TAG # Verification: one push to an MR branch shows one pipeline, of source merge_request_event
glab ci list --per-page 3 Step 2 — Run jobs per branch Jump to heading
Job-level rules are evaluated in order; the first match decides. Put the specific cases first and end with an explicit default.
deploy-staging:
script: ./deploy.sh staging
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- when: never
publish:
script: ./publish.sh
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
- when: never The explicit when: never at the end documents that the job runs nowhere else. Without it, a job with rules runs only when a rule matches anyway, but readers have to know that.
Step 3 — Run jobs per path with changes Jump to heading
changes: makes a rule match only when listed paths changed. What “changed” is compared against depends on the pipeline type, and that is where surprises come from.
test-billing:
script: make -C services/billing test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
paths: [services/billing/**/*, libs/money/**/*]
- if: $CI_COMMIT_BRANCH
changes:
paths: [services/billing/**/*, libs/money/**/*]
compare_to: refs/heads/main # stable comparison even on new branches Include shared dependencies in the path list — libs/money/ above — or a change to a library will not run the tests of the services that use it. For monorepos, generating these lists from a dependency graph is covered in detecting affected projects from git diff.
Step 4 — Keep required jobs from being skipped Jump to heading
If merge requests require a successful pipeline and the only jobs are path-filtered, a change touching none of the paths produces a pipeline with no jobs — or none at all — and the merge request cannot merge. Add a lightweight job that always runs, so every pipeline has a result.
pipeline-ok:
stage: .post
script: echo "pipeline completed"
rules:
- when: always The same problem exists on other forges as skipped required checks, discussed in required checks for path-filtered workflows.
Step 5 — Test rules before relying on them Jump to heading
Rules are easy to get subtly wrong. GitLab’s pipeline editor can simulate a pipeline for a chosen branch, and the CI lint API validates syntax. Use both on a branch before merging rule changes.
glab ci lint .gitlab-ci.yml
# Simulate the pipeline for a branch (dry run)
glab api -X POST "projects/$PROJECT_ID/ci/lint" -f content="$(cat .gitlab-ci.yml)" \
-F dry_run=true -f ref=feature/billing-rounding | jq '.jobs[].name' Step 6 — Reuse rule sets instead of repeating them Jump to heading
Large pipelines repeat the same conditions across dozens of jobs, and copies drift. Define each rule set once with a YAML anchor or !reference, and attach it to every job that needs it.
.rules-billing:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes: { paths: [services/billing/**/*, libs/money/**/*] }
- if: $CI_COMMIT_BRANCH
changes: { paths: [services/billing/**/*, libs/money/**/*], compare_to: refs/heads/main }
test-billing:
script: make -C services/billing test
rules: !reference [.rules-billing, rules]
lint-billing:
script: make -C services/billing lint
rules: !reference [.rules-billing, rules] When the path list changes — a new shared library, a moved directory — one edit updates every job. Pair this with a periodic check that every top-level service directory appears in at least one rule set, so a newly added service does not silently get no CI at all.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Should I still use only/except? Jump to heading
No. only and except are older syntax and cannot be mixed with rules in the same job. rules are more expressive and are what GitLab documents going forward.
Why did a merge request pipeline run every job? Jump to heading
Usually because the merge request’s commits really touch many paths, or because a broad pattern like **/*.yml matches a configuration file every change touches. Narrow the patterns, and list shared configuration explicitly where it matters.
How do merged results pipelines interact with changes? Jump to heading
Merged results pipelines still compare the merge request’s changes with the target branch, so changes: behaves the same. They test the merged state, which is what you want for catching interactions, as described in catching semantic conflicts in CI.
Related Jump to heading
- CI/CD Pipeline Trigger Mapping — the parent topic.
- Verifying Signed Commits in GitLab CI — a job that uses pipeline-source rules.
- Cancelling Superseded CI Runs with Concurrency Groups — the GitHub counterpart for reducing wasted runs.
- GitLab Merge Trains — landing merge requests safely once rules are in place.