Did GitLab skip your test job again?
You probably trusted a path filter.
I keep seeing this on merge requests.
A skipped badge becomes a safety story.
That story is usually wrong.
A skip is a diff, not a verdict.
This FAQ is a corrected mental model.
You leave with a local rehearsal, not a slogan.
Why skipped jobs feel so convincing
GitLab draws a neat skipped badge.
Humans read badges faster than git ranges.
The YAML looks precise and complete.
Glob lists feel like a reviewed contract.
They are not a contract.
They are a comparison against one baseline.
Miss that baseline, and the skip misleads.
The pipeline can still read as green.
Myth 1: A skip means those paths never changed
This is the loudest claim I hear.
"Frontend never touched src/, so tests skipped."
GitLab did not audit your intent.
It compared file names to a list.
Which commit is the left side?
That question is the whole bug.
On many branch pipelines the default is harsh.
rules:changes often compares to the previous commit.
Push a follow-up that only edits docs.
Your src/**/* job can skip on that push.
The merge request can still contain src edits.
Those edits just landed in an earlier push.
Corrected model: skipped means not in this comparison.
It does not mean absent from the merge request.
Check both file lists yourself:
git fetch origin
git log --oneline origin/main..HEAD
git diff --name-only origin/main...HEAD
git diff --name-only HEAD~1 HEAD
Those two diffs are not the same list.
One is the MR. One is the last push.
Primary docs live here, not in chat lore:
GitLab rules:changes.
Myth 2: MR pipelines share the branch-push baseline
Why would they?
The pipeline source just changed.
An MR pipeline can compare against the target branch.
A push pipeline can compare against HEAD~1.
Same YAML. Different left side.
Same glob. Different skip.
Look at this unexecuted example:
# example YAML — not a live pipeline
test_unit:
stage: test
script:
- npm test
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- src/**/*
- package.json
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never
- if: '$CI_COMMIT_BRANCH'
changes:
- src/**/*
Did you pin compare_to?
If not, GitLab picks a source-specific default.
You can make the baseline explicit:
# example YAML — verify against your GitLab docs
test_unit:
stage: test
script:
- npm test
rules:
- changes:
paths:
- src/**/*
- package.json
compare_to: 'main'
Still not magic.
compare_to needs a ref the clone can see.
Forks, shallow clones, and deleted branches bite here.
Default-branch renames bite too.
Corrected model: pipeline source selects the baseline.
YAML reuse does not freeze that baseline.
Confirm it from pipeline type and job log.
Do not confirm it from memory.
Myth 3: Skipped tests mean the change is safe
Safe for whom?
Safe against which failure mode?
A path filter is not a test plan.
It is a cost cut.
You skip jobs to save minutes.
You also skip evidence.
allow_failure is a cousin of this myth.
A warning job is not a green suite.
when: on_success plus a skip is quieter.
The DAG just omits a node.
Ask three questions before you merge:
- Which jobs actually ran?
- Which jobs skipped, and against which diff?
- Which jobs never entered the pipeline at all?
Those are three different states.
The UI folds them together in your head.
I keep this decision table next to the MR:
| Observation | What it proves | What it does not prove |
|---|---|---|
Job skipped on rules:changes
|
Paths missed this comparison | Paths never changed on the branch |
Job not created (workflow:rules) |
Pipeline rules excluded it | The suite is green |
| Job green after retry | A later attempt passed | The first attempt was valid |
Job green with allow_failure
|
The pipeline may still pass | Failures were absent |
| JUnit artifact missing | No report was uploaded | Zero tests failed |
Paste the table in the MR if you must.
Then paste git diff --name-only under it.
No table, no merge argument.
A skipped badge is not a receipt.
Myth 4: A parsed glob list is a reviewed filter
This one arrived with generated YAML.
The file parses. The globs look neat. Ship it?
Parsing is not review.
A glob that never matches is a silent skip.
src/**/* does not match Src/.
It does not match backend/src unless you said so.
*.md does not always match docs/guide.md.
GitLab path matching is its own matcher.
I draft YAML in a scratch session sometimes.
I still treat the model as a typist.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode offers free model access and a free server option.
I use that pair to draft checklists, not to run GitLab.
The free server is not your runner.
It does not see protected variables or environments.
Ask the model for a glob critique.
Then run the helper below on the real diff.
If the helper and GitLab disagree, trust GitLab.
Then fix the YAML, not the hallway story.
Artifact: a local rules:changes rehearsal
This is a rehearsal, not CI.
Label it that way in your notes.
Save this as scripts/rehearse-changes.sh.
#!/usr/bin/env bash
# rehearse-changes.sh
# Local rehearsal for GitLab rules:changes discussions.
# This does not evaluate GitLab glob syntax 1:1.
set -euo pipefail
BASE_REF="${1:-origin/main}"
SHIFT_REF="${2:-HEAD}"
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Run this inside a git checkout."
exit 1
fi
echo "## Baseline"
echo "compare_to-style range: ${BASE_REF}...${SHIFT_REF}"
echo "previous-commit range: ${SHIFT_REF}~1 ${SHIFT_REF}"
echo
echo "## Files in the merge-request-style diff"
git fetch origin --quiet || true
git diff --name-only "${BASE_REF}...${SHIFT_REF}" || {
echo "Could not diff ${BASE_REF}...${SHIFT_REF}"
exit 1
}
echo
echo "## Files in the last-push-style diff"
if git rev-parse "${SHIFT_REF}~1" >/dev/null 2>&1; then
git diff --name-only "${SHIFT_REF}~1" "${SHIFT_REF}"
else
echo "No parent commit. First commit on this ref."
fi
Run it like this:
chmod +x scripts/rehearse-changes.sh
./scripts/rehearse-changes.sh origin/main HEAD
Now add a tiny matcher for paths you care about.
# example only — GNU grep, unexecuted until you run it
echo
echo "## Paths matching src/ or package.json"
git diff --name-only origin/main...HEAD \
| grep -E '^(src/|package\.json$)' || true
That grep is not GitLab.
It is a flashlight.
If the flashlight shows src/ files, stop celebrating.
A skipped test_unit job now needs a baseline explanation.
Optional YAML lint stays local too:
# example — only if you already install these tools
yamllint .gitlab-ci.yml
git diff origin/main -- .gitlab-ci.yml
No, yamllint cannot expand rules:changes.
It only tells you the file is YAML.
A checklist I actually paste
Copy this into the MR when a job skips:
- Pipeline type: branch, MR, tag, or detached?
- Baseline:
HEAD~1, target branch, orcompare_to? - File list: paste
git diff --name-only. - Glob list: paste the
changes:paths. - Hidden jobs: did
workflow:rulesdrop the pipeline? - Related jobs: did
needspoint at a skipped job?
Six answers. Then you can talk about merging.
Zero answers. Then you are guessing.
I also paste the two ranges in the comment.
origin/main...HEAD and HEAD~1..HEAD must both appear.
If they disagree, pin compare_to before arguing.
If they agree, the glob itself is the suspect.
Where a free draft session still helps
I let a free model rewrite checklist tone.
I let it propose extra globs I forgot.
I do not let it certify the pipeline.
Certification lives in GitLab's job log.
Keep the YAML and the diff side by side.
Keep secrets off any scratch host.
Ask for missing paths, not merge permission.
Permission is still a human job.
If you try that workflow, one ask is enough:
paste compare_to next to every changes: block.
Limitations
This rehearsal does not implement GitLab's matcher.
Negation patterns and some wildcards can diverge.
Shallow clones can hide compare_to history.
Mirrors can lag the default branch.
Monorepos with generated files will flood the diff.
Path filters then become a product decision.
I did not publish timings here.
I did not claim a pass rate.
Those would be fake precision.
Your runner graph is the only scoreboard.
Re-read the current GitLab YAML reference before copying snippets.
Syntax around paths and compare_to has shifted across releases.
Who should not use this approach
Do not use path filters as a security boundary.
A renamed file can dodge a glob.
Do not use a free scratch host as a GitLab runner.
It will not reproduce protected-branch semantics.
Do not skip integration jobs on *.md only rules.
Docs changes still break links and OpenAPI examples.
Do not merge because generated YAML looks idiomatic.
Idiomatic YAML still skips the wrong job.
If you need a required test, mark it required.
Then stop filtering it on globs.
Teams without merge-request pipelines should not copy MR advice.
Your baseline is the previous push until you pin one.
What I want you to remember
rules:changes answers one question.
Did this comparison contain these paths?
It does not answer "is this MR safe?"
It does not answer "did tests run on the full branch?"
When a job skips, open the two diffs.
When the diffs disagree, pin compare_to.
When a model drafts the globs, keep the rehearsal script.
When GitLab still surprises you, believe the pipeline type first.
Top comments (0)