A GitLab CI rules:if Cookbook Built on Predefined Variables
GitLab exposes dozens of predefined CI variables. Here's a practical rules:if cookbook that uses them to run the right jobs on tags, MRs, default branch, and scheduled runs.
- #gitlab-cicd
- #ai
- #rules
- #variables
- #workflow
Most rules:if mistakes come from not knowing which predefined variable describes the situation you’re trying to match. People reach for $CI_COMMIT_BRANCH when they mean “is this an MR pipeline,” or check $CI_COMMIT_TAG when the cleaner signal is $CI_PIPELINE_SOURCE. The result is rules that work in testing and surprise you in production. This is the cookbook I keep handy — common conditions matched to the right predefined variable, with copy-paste rules:if blocks.
The variables worth memorizing
A handful of predefined variables cover the vast majority of routing decisions:
$CI_PIPELINE_SOURCE— why the pipeline ran:push,merge_request_event,schedule,web,trigger,pipeline,api.$CI_COMMIT_BRANCH— set on branch pipelines, empty on MR pipelines and tags.$CI_COMMIT_TAG— set only when building a tag.$CI_DEFAULT_BRANCH— your repo’s default branch name, so rules stay portable.$CI_MERGE_REQUEST_*— populated only in merge-request pipelines.$CI_COMMIT_REF_PROTECTED—"true"on protected refs.
The trap that catches everyone: $CI_COMMIT_BRANCH is empty in an MR pipeline. If you write if: '$CI_COMMIT_BRANCH == "main"' expecting it to fire during a merge request targeting main, it won’t.
Recipe 1: only on the default branch
deploy-staging:
stage: deploy
script: ./deploy.sh staging
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
Using $CI_DEFAULT_BRANCH instead of a hardcoded "main" means this keeps working if you rename the branch. Small thing, saves a future headache.
Recipe 2: only on tags (releases)
publish-release:
stage: release
script: ./publish.sh
rules:
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
Anchoring to a semver pattern means a stray nightly tag won’t accidentally publish a release.
Recipe 3: merge request pipelines only
review-app:
stage: review
script: ./deploy-review.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
This is the correct signal for “we’re in an MR pipeline” — far more reliable than poking at branch variables.
Recipe 4: scheduled (nightly) runs only
nightly-scan:
stage: scan
script: ./full-scan.sh
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
Pair this with a named schedule and a custom variable if you have several schedules that should each trigger different jobs.
Recipe 5: protected-branch-only secrets work
deploy-production:
stage: deploy
script: ./deploy.sh production
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_REF_PROTECTED == "true"'
when: manual
Checking $CI_COMMIT_REF_PROTECTED ensures a job that needs protected variables never even tries to run somewhere those variables won’t exist. Combine it with when: manual for a deploy gate.
Recipe 6: skip duplicate branch+MR pipelines
The most common workflow:rules pattern stops a push from creating both a branch pipeline and an MR pipeline:
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never
- if: '$CI_COMMIT_BRANCH'
This says: always run MR pipelines; suppress the branch pipeline when an MR is already open for that branch; otherwise run the branch pipeline. It’s the antidote to the duplicate-pipeline noise everyone hits.
Let AI assemble the rules — then verify the matrix
Predefined variables interact in ways that are easy to get subtly wrong, so I draft complex rule sets with an LLM and then test them. A prompt I reuse:
Prompt: “Write GitLab CI
rules:for a job that should run: on merge requests targeting the default branch, on the default branch after merge, and on semver tags — but NOT on scheduled pipelines or on feature-branch pushes that have no open MR. Use only predefined variables, anchor the tag to semver, and use $CI_DEFAULT_BRANCH not a literal. Then give me a test matrix: for each trigger (MR-to-main, push-to-feature-no-MR, push-to-main, tag v1.2.3, schedule), state whether the job runs.”
The test matrix is the part that matters:
Output (excerpt):
Trigger Job runs? MR targeting main yes push to feature, no MR no push to main yes tag v1.2.3 yes scheduled run no
Don’t trust that table — generate the real pipelines and confirm each row. The model reasons about rules: better than most humans, but it still occasionally mixes up the empty-on-MR behavior of $CI_COMMIT_BRANCH. The matrix gives you something concrete to check against the pipeline’s created/skipped view.
Where to go deeper
Once the if conditions are right, the next layer is path-scoping with changes: so jobs only run when relevant files move, and gating whole pipelines with workflow:rules. I’ve covered both in the GitLab CI/CD category, and the reusable rules:if expression cookbook prompt is where I keep the parameterized version of these recipes.
The bottom line
Good rules:if is mostly about picking the variable that actually describes your situation: $CI_PIPELINE_SOURCE for why a pipeline ran, $CI_COMMIT_TAG for tags, $CI_DEFAULT_BRANCH for portability, and $CI_COMMIT_REF_PROTECTED for “is this trusted.” Remember that $CI_COMMIT_BRANCH is empty in MR pipelines, anchor your tag patterns, and always validate the rule set against a trigger-by-trigger matrix before you ship it. Let AI draft; let the pipeline graph confirm.
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.