Day 12: Rules and Conditions in GitLab CI/CD Pipelines
Conditional execution is a cornerstone of production-grade DevOps. It ensures that pipelines run efficiently by executing only the necessary jobs under specific circumstances, saving computational resources and preventing accidental deployments. This guide explores how to control job execution in GitLab CI/CD using both legacy and modern conditional patterns.
1. The Problem: Default Pipeline Inefficiency
By default, GitLab CI/CD triggers a pipeline for every single push to any branch in your repository. Within that pipeline, every defined job will attempt to run. This default behavior introduces several critical inefficiencies in a team environment:
- Unnecessary Resource Consumption: Running heavy build or test suites on minor documentation updates (e.g., editing a
README.mdfile) wastes runner concurrency and increases cloud costs. - Accidental Deployments: You do not want a push to a temporary feature branch or a developer's personal branch to trigger a deployment to production.
- Lack of Environment Isolation: Test jobs should ideally run on QA or development branches, while deployment jobs must be strictly restricted to stable branches like
mainorrelease.
Real-Life Analogy
Think of a manufacturing factory. If the factory started the entire heavy assembly line, packaging, and shipping process every time a designer sketched a minor tweak on a notepad, the company would quickly go bankrupt. Instead, the factory uses gates: sketches stay in the design phase, prototypes are built only on the testing floor, and the full assembly line runs only when the final blueprint is officially signed off by the director.
2. Core Mental Model: Conditional Pipeline Execution
Conditional pipeline execution means defining explicit rules that dictate whether a job should run, skip, or wait for manual intervention based on the context of the Git event.
By applying conditions at the job level rather than the global pipeline level, you can build highly dynamic workflows where different jobs react differently to the same Git event.
3. The Legacy Approach: only and except
In older versions of GitLab CI/CD, execution control was managed using the only and except keywords.
only: Defines the branches or tags for which the job will run.except: Defines the branches or tags for which the job will not run (it will be skipped).
Legacy Code Example
deploy_job:
stage: deploy
script:
- echo "Deploying changes to the production environment..."
only:
- main
Limitations of only / except
While simple to write, this legacy approach has significant drawbacks:
- Lack of Flexibility: It is heavily dependent on branch names and cannot easily evaluate complex environmental variables.
- No Complex Logic: You cannot combine multiple logical operators (e.g., "Run if branch is
mainAND the commit message contains a specific keyword AND it was triggered by a merge request"). - Deprecation: This method is legacy and has been superseded by the modern
rulesengine. It should be avoided in new pipelines.
4. The Modern Approach: The rules Keyword
The rules keyword is the modern, highly flexible standard for controlling job execution in GitLab CI/CD. It allows you to evaluate a list of conditional clauses sequentially.
Sequential Evaluation (Top-to-Bottom)
GitLab evaluates rules in the order they are defined. The first rule that evaluates to true is applied, and all subsequent rules are ignored. This is a "first-match-wins" strategy.
job_name:
script: echo "Hello World"
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
- when: never
In this example:
- GitLab checks if the current branch is
main. - If yes, the job is set to run
always. Evaluation stops. - If no, GitLab moves to the next rule, which is
when: never(skip the job).
5. Understanding if-else Logic in Rules
The combination of multiple - if statements and when clauses allows you to construct robust if-else decision trees directly in your YAML configuration.
build_job:
stage: build
script:
- echo "Building application..."
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
- if: $CI_COMMIT_BRANCH == "dev"
when: manual
- when: never
How GitLab Evaluates This Block:
Note on Dual
whenUsage: The firstwhenis bound to theifcondition (the "then" action). The finalwhen: neveracts as the catch-all "else" block.
6. Common Rule Conditions & Variables
GitLab provides pre-defined CI/CD variables that can be evaluated within your rules.
A. Branch-Based Execution
Triggers jobs based on the branch receiving the commit.
rules:
- if: $CI_COMMIT_BRANCH == "main"
B. Merge Request Events
Triggers jobs only when a merge request is created or updated, preventing runs on standard branch pushes.
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
C. Git Tags
Triggers jobs when a release tag (e.g., v1.0.0) is pushed to the repository.
rules:
- if: $CI_COMMIT_TAG
D. File Changes (changes keyword)
Triggers jobs only when specific files or file extensions are modified. This is highly useful for monorepos or separating documentation from code.
rules:
- changes:
- "**/*.js"
If a JavaScript file (.js) changes, the job runs. If only a markdown file (.md) changes, the job is skipped.
7. Job Control States: always, never, and manual
The when keyword inside a rule defines the execution state of the job if the condition matches.
8. Step-by-Step Evolution: Code Implementations
Naive Configuration (No Rules)
Every job runs on every push, regardless of the branch.
stages:
- build
- deploy
build_job:
stage: build
script:
- echo "Building..."
deploy_job:
stage: deploy
script:
- echo "Deploying to Production..."
Legacy Decoupled Configuration (Using only/except)
Restricts deployment to main and skips build on dev.
stages:
- build
- deploy
build_job:
stage: build
script:
- echo "Building..."
except:
- dev
deploy_job:
stage: deploy
script:
- echo "Deploying..."
only:
- main
Modern Production-Ready Configuration (Using rules)
Implements clean, readable, and highly maintainable conditional logic.
stages:
- build
- test
- deploy
build_job:
stage: build
script:
- echo "Compiling application..."
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
- if: $CI_COMMIT_BRANCH == "dev"
when: manual
- when: never
test_job:
stage: test
script:
- echo "Running test suite..."
# No rules defined: This job runs on all branches by default
9. Practical Hands-on Walkthrough
Follow these steps to implement and verify rules in a live GitLab repository.
Step 1: Clone the Repository
Clone your GitLab project locally and navigate into the directory:
git clone git@gitlab.com:your-username/day-12-rules-and-conditions.git
cd day-12-rules-and-conditions
Step 2: Create the Initial Pipeline Configuration
Create a .gitlab-ci.yml file with a restricted build job:
stages:
- build
build_job:
stage: build
script:
- echo "This job only runs on the main branch."
only:
- main
Commit and push this to the main branch:
git add .gitlab-ci.yml
git commit -m "feat: add main-only build job"
git push origin main
Observation: Navigate to Build > Pipelines in GitLab. You will see the pipeline trigger and the build_job execute successfully.
Step 3: Test Branch Exclusion
Create and switch to a new development branch:
git checkout -b dev
Create a dummy text file to trigger a change:
echo "Working on dev branch" > dev_notes.txt
git add dev_notes.txt
git commit -m "doc: add dev notes"
git push origin dev
Observation: Check the GitLab Pipelines page. No pipeline is triggered for the dev branch because the build_job is restricted to main via the only directive.
Step 4: Implement Complex Rules with Manual Gates
Update your .gitlab-ci.yml file to use modern rules with a manual gate for non-main branches:
stages:
- build
- test
build_job:
stage: build
script:
- echo "Running build step..."
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
- if: $CI_COMMIT_BRANCH == "new-feature"
when: manual
- when: never
test_job:
stage: test
script:
- echo "Running tests unconditionally..."
Commit and push this configuration to a new branch named new-feature:
git checkout -b new-feature
git add .gitlab-ci.yml
git commit -m "feat: migrate to modern rules engine"
git push origin new-feature
Step 5: Verify the Manual Gate in GitLab UI
- Navigate to Build > Pipelines in your GitLab project.
- Select the pipeline running for the
new-featurebranch. - Observe the pipeline graph:
- The
test_jobruns automatically. - The
build_jobdoes not run automatically. Instead, it displays a Play (▶) icon.
- The
- Click the Play button to manually trigger the execution of the
build_job.
10. Production Pitfalls & Debugging Workflows
Pitfall 1: Unintended Job Skipping (The Fallback Trap)
If you write a rule block without a final fallback, jobs might skip unexpectedly.
# DANGEROUS CONFIGURATION
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: always
What happens on other branches? If none of the rules match, GitLab's default behavior is to skip the job. Always define an explicit fallback if you want the job to run under other conditions, or use when: never to make the skip explicit.
Pitfall 2: Conflicting Rules in Multi-Job Pipelines
If Job_B depends on Job_A (via needs or stages), but Job_A is skipped due to a rule while Job_B is set to run, the entire pipeline will fail with a YAML configuration error.
Solution: Ensure dependent jobs share identical or compatible rule blocks.
Debugging Workflow for Skipped Jobs
If a job is not appearing in your pipeline:
- Navigate to Build > Pipeline Editor in the GitLab UI.
- Use the Lint tab to validate your YAML syntax.
- Check the Visualize tab to see which jobs are active for your current branch context.
- Verify your environment variables: Remember that
$CI_COMMIT_BRANCHis empty during tag pipelines (use$CI_COMMIT_TAGinstead).
11. Summary and Key Takeaways
- Default Behavior Inefficiency: Without rules, GitLab runs every job on every push, wasting runner minutes and risking accidental deployments.
- Legacy vs. Modern: Avoid using
onlyandexcept. Use the modernrulesengine for all new pipelines. - First-Match-Wins: Rules are evaluated sequentially from top to bottom. Once a rule matches, evaluation stops.
- Implicit Skip: If a job has a
rulesblock and none of the rules evaluate to true, the job is skipped by default. - Manual Gates: Use
when: manualto create interactive approval steps in your pipeline (e.g., for staging or production deployments). - File-Level Triggers: The
changeskeyword allows you to optimize pipelines by only running jobs when relevant source code files are modified. - Variable Evaluation: Use GitLab's built-in environment variables (like
$CI_COMMIT_BRANCHand$CI_PIPELINE_SOURCE) to write precise execution conditions.