Run GitLab CI jobs only when files change
Updated
rules:changes adds a job to a pipeline only when the listed files changed. When it seems not to work, the pipeline's type is usually why: each type compares with something different, and some compare with nothing.
| Pipeline | rules:changes compares with | Result |
|---|---|---|
| Merge request | The target branch | What most people expect |
| Branch | The previous commit on the branch | Only what the latest push changed |
| New branch | Nothing | Always true, so the job runs |
| Tag, scheduled or manual | Nothing: there's no Git push | Always true, so the job runs |
These rules come from GitLab's rules:changes reference.
Filter a job
Pair changes with a condition on the pipeline type, so the comparison is the one you want:
test:
script:
- npm ci
- npm test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- src/**/*
- package-lock.jsonWhen no rule matches, GitLab leaves the job out of the pipeline. A path that starts with * needs double quotes, such as "**/*.json".
Why the job runs every time
- The branch is new. Its first pipeline has nothing to compare with.
- The pipeline is scheduled, a tag or manual.
changesis always true there. - A later rule matches. Rules are checked in order and the first match wins, so a final
- when: on_successadds the job wheneverchangesdoesn't. - The job is in two pipelines. A branch pipeline and a merge request pipeline can both run, each with its own comparison.
Compare with a branch
compare_to sets what to compare with, which covers new branches:
test:
script:
- npm ci
- npm test
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH
changes:
paths:
- src/**/*
compare_to: refs/heads/mainThe first rule matters: main compared with main has no changes, so without it the job would never run there. GitLab also warns that compare_to can give unexpected results in merged results pipelines and in forks, because the comparison base isn't what it seems.
Stop duplicate pipelines
A push to a branch with an open merge request can start both a branch pipeline and a merge request pipeline. GitLab's workflow:rules example keeps one of them:
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCHWhen another job needs a filtered job
needs expects the job to be in the pipeline. If rules can leave it out, mark it optional:
deploy:
script:
- ./deploy.sh
needs:
- job: test
optional: trueWhen test is in the pipeline, deploy waits for it. When it isn't, deploy starts once its other needs are met.
When every job is filtered out
If rules leave a merge request with no jobs, it has no pipeline. With Pipelines must succeed on, GitLab won't merge it: a merge request with no pipeline doesn't count as having a successful one. This is GitLab's version of a required check stuck on GitHub.
Keep one job that always runs in merge request pipelines:
ci-ok:
stage: test
script:
- echo "CI ran"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"Give it a normal stage: a pipeline with only .pre or .post jobs doesn't run. For pipelines skipped with [skip ci], the project setting Skipped pipelines are considered successful allows the merge.
What rules:changes can't see
rules:changes asks what changed, not whether this code already passed:
- It can skip too much. A shared file missing from the list passes with nothing tested.
- It can run too much. A revert, or a rebase that leaves the files as they were, shows up as changed files, though these exact files already passed.
hushrun run compares the files with what already passed. The job stays in every pipeline, so there's no list to keep up to date, and a skip exits 0, so the pipeline succeeds:
test:
image: node:22
script:
- npm ci
- npm testHere npm test is wrapped once in package.json, as hushrun run -- vitest run. GitLab CI records results with an agent token and the key that comes with it: see Other CI. The job still starts and installs, so hushrun saves the check's minutes, not the job's.