GitLab CI/CD – Rules

In today’s blog post, we will discuss rules in .gitlab-ci.yml. GitLab CI/CD pipeline rules offer a highly flexible and intuitive way to manage conditional job execution, allowing your pipelines to adapt dynamically based on various factors like branch names, commit messages, file changes, and even the source of the pipeline trigger.

Why rules? The Evolution of Conditional Logic

Before rules, GitLab CI/CD primarily relied on the only and except keywords. While these served a basic purpose for including or excluding jobs based on branches or tags, they had notable limitations:

  • Limited Scope: They offered narrow conditions, primarily focused on Git references (branches, tags, merge requests).
  • Order Sensitivity: The order of only/except entries could sometimes lead to confusing or unintended behavior.
  • Redundancy: Similar logic often had to be repeated across multiple jobs.

The rules keyword was introduced to address these challenges, providing a more explicit, ordered, and significantly more powerful mechanism for defining conditional logic within your CI/CD configuration.

Understanding the rules Keyword Structure

The rules keyword is defined at the job level within your .gitlab-ci.yml file. It accepts a list of rule expressions, and GitLab evaluates these rules sequentially, from top to bottom. The first rule that evaluates to true dictates the job’s behavior, and no further rules for that job are processed.

Each rule entry typically consists of the following components:

  • if: The core conditional expression.
  • when: Defines the job’s behavior if the if condition is met.
  • allow_failure: (Optional) Determines if the pipeline should continue upon job failure.
  • variables: (Optional) Allows setting job-specific variables dynamically.

If none of the defined rules for a job evaluate to true, the job is effectively skipped by default (an implicit when: never applies). You can, however, add a final fallback rule to specify a different default behavior.

Let us explore each component with detailed examples.

if: The Conditional Expression

The if keyword holds a conditional expression that GitLab evaluates to true or false. If true, the job adopts the when behavior (and other settings) specified within that particular rule. These expressions often leverage predefined GitLab CI/CD variables and common comparison operators.

Example:

lint_codebase:
  stage: test
  script:
    - echo "Running linter..."
    - npm run lint
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"' # Rule 1: Condition for 'main' branch
      when: on_success
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' # Rule 2: Condition for MR pipelines
      when: on_success
    - if: '$CI_COMMIT_MESSAGE =~ /skip-lint/' # Rule 3: Condition for commit message
      when: never
    - when: manual # Rule 4: Fallback for all other cases

Explanation:

  • Rule 1 (if: '$CI_COMMIT_BRANCH == "main"'): This condition checks if the pipeline is running on the main branch. If a commit is pushed to main, this rule’s if evaluates to true.
  • Rule 2 (if: '$CI_PIPELINE_SOURCE == "merge_request_event"'): This condition checks if the pipeline was triggered by a merge request event. If an MR is created or updated, this if evaluates to true.
  • Rule 3 (if: '$CI_COMMIT_MESSAGE =~ /skip-lint/'): This uses a regular expression (=~) to check if the commit message contains the phrase “skip-lint”. This demonstrates how you can create conditions based on commit content.
  • The first rule that evaluates to true will determine the job’s action. If none of the if conditions are met, the job will fall through to the last rule.

when: The Job’s Behavior

The when keyword dictates what happens to the job if its preceding if condition (or no if condition in a fallback rule) is met.

on_success (default)

Meaning: The job will run automatically. This happens as soon as all jobs in its previous stages (or specific needs dependencies) have completed successfully.

Example:

build_application:
  stage: build
  script:
    - echo "Compiling application..."
    - mvn clean install
  rules:
    - if: '$CI_COMMIT_BRANCH' # Any branch commit
      when: on_success # Job runs automatically
on_failure

Meaning: The job will only run if a job in a previous stage (or a job it explicitly needs) has failed. This is invaluable for error handling.

Example:

send_failure_alert:
  stage: notifications
  script:
    - echo "Pipeline failed! Sending alert to #dev-ops channel."
    - curl -X POST -H 'Content-type: application/json' --data '{"text":"Pipeline failed: $CI_PIELINE_URL"}' $SLACK_WEBHOOK_URL
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"' # Only for main branch pipelines
      when: on_failure # This job runs ONLY if a preceding job failed

Explanation: If any job in an earlier stage of a main branch pipeline fails, this send_failure_alert job (in the notifications stage) will automatically trigger to send an alert.

manual

Meaning: The job will appear in the pipeline UI, but it would not start automatically. Instead, it will display a “play” button, requiring a user to manually click it to initiate execution.

Example:

deploy_to_production:
  stage: deploy
  script:
    - echo "Initiating production deployment for version $CI_COMMIT_TAG..."
    - deploy-prod-script "$CI_COMMIT_TAG"
  rules:
    - if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/' # Triggered by semantic version tags
      when: manual # Requires manual approval to deploy
    - when: never

Explanation: When a new semantic version tag (e.g., v1.0.0) is pushed, the deploy_to_production job will be added to the pipeline, but it will be paused, awaiting a manual trigger from a user in the GitLab UI before deploying to production.

delayed

Meaning: The job will automatically run after a specified delay once its stage is reached and previous dependencies are met. This is useful for phased rollouts or giving services time to initialize.

Syntax: when: delayed, start_in: <duration> (e.g., 5 minutes, 1 hour, 1 day).

Example:

post_deploy_health_check:
  stage: post_deploy
  script:
    - echo "Waiting 10 minutes before running health checks..."
    - run_health_checks.sh
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: delayed
      start_in: 10 minutes # Job starts 10 minutes after previous stage completes
    - when: never

Explanation: After a successful deployment to main, this post_deploy_health_check job will appear in the pipeline, but its execution will be automatically delayed by 10 minutes.

never

Meaning: The job will never be added to the pipeline if this specific rule is matched. This is frequently used as a final, explicit fallback rule to prevent a job from running if no other specific condition is met.

Example:

run_heavy_integration_tests:
  stage: integration_tests
  script:
    - echo "Running heavy integration tests..."
    - run_all_integration_tests.sh
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"' # Rule 1: Always run on main
      when: on_success
    - if: '$CI_COMMIT_MESSAGE =~ /\[full-ci\]/' # Rule 2: Run if commit message has [full-ci]
      when: on_success
    - when: never # Rule 3: If neither of the above, DO NOT run this job

Explanation: If a pipeline is triggered on a branch other than main and the commit message does not contain [full-ci], the run_heavy_integration_tests job will be entirely excluded from the pipeline.

allow_failure (Optional): Pipeline Continuation

Purpose: If allow_failure: true is set for a job, the overall pipeline will continue to subsequent stages even if this specific job fails. The failing job will be marked with a yellow warning icon in the GitLab UI, indicating a failure but not a pipeline halt.

Syntax: allow_failure: true or allow_failure: false (which is the default behavior if omitted).

Example:

static_code_analysis:
  stage: security_scan
  script:
    - echo "Running static analysis tool..."
    - run_sonarqube_scan.sh
  rules:
    - if: '$CI_COMMIT_BRANCH'
      when: on_success
  allow_failure: true # Pipeline proceeds even if SonarQube scan finds issues

Explanation: Even if the run_sonarqube_scan.sh script returns a non-zero exit code (indicating a failure, perhaps due to quality gate violations), the static_code_analysis job will be marked as “failed with warning,” but the pipeline will continue to the next stage (e.g., deploy). This is useful for non-critical checks that provide feedback but should not block the entire CI/CD flow.

variables (Optional): Rule-Specific Variables

Purpose: You can define a variables block directly within a rules entry. These variables are only set and available to the job if that specific rule’s if condition is met and that rule is the one that causes the job to be included in the pipeline. This enables highly dynamic job configuration based on the matching condition.

Syntax: Nested under the specific rules entry.

Example:

deploy_to_environment:
  stage: deploy
  script:
    - echo "Deploying to environment: $TARGET_ENV with version $APP_VERSION"
    - deploy_tool --env "$TARGET_ENV" --version "$APP_VERSION"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      variables:
        TARGET_ENV: "production" # Variable set if on main branch
        APP_VERSION: "$CI_COMMIT_TAG" # Use commit tag for production version
      when: manual
    - if: '$CI_COMMIT_BRANCH == "develop"'
      variables:
        TARGET_ENV: "staging" # Variable set if on develop branch
        APP_VERSION: "$CI_COMMIT_SHORT_SHA" # Use short SHA for staging version
      when: on_success
    - when: never

Explanation:

  • If a pipeline runs on the main branch, the deploy_to_environment job will become manual. For that specific job, $TARGET_ENV will be set to “production”, and $APP_VERSION will take the value of the Git tag that triggered the pipeline (e.g., v1.0.0).
  • If a pipeline runs on the develop branch, the deploy_to_environment job will run on_success. For that specific job, $TARGET_ENV will be “staging”, and $APP_VERSION will be set to the short commit SHA.
  • This allows the same deploy_tool command to behave distinctly based on the branch, dynamically injecting the correct environment and versioning scheme without needing separate jobs or complex inline if/else logic in the script.

Best Practices for Using rules

  • Order Matters: Always arrange your rules from the most specific condition to the most general. The first matching rule wins.
  • Be Explicit with Fallbacks: Include a final when: never or a default when: rule to clearly define what happens if none of the preceding conditions are met. This avoids ambiguity.
  • Leverage Predefined Variables: Familiarize yourself with GitLab’s extensive list of predefined CI/CD variables (e.g., $CI_COMMIT_REF_NAME, $CI_PIPELINE_SOURCE, $CI_COMMIT_MESSAGE, $CI_MERGE_REQUEST_IID) to craft precise conditions.
  • Optimize with changes: For large repositories or monorepos, use changes within your rules to trigger jobs only when specific files or directories have been modified, significantly speeding up pipelines.
  • Prioritize Readability: While rules can be complex, aim for clear and concise if conditions. If they become too unwieldy, consider breaking them down or using helper variables.
  • Test Thoroughly: Given their power, rules require careful testing. Push commits that should and should not trigger certain jobs to verify your logic. Utilize the GitLab CI/CD Lint tool (CI/CD -> Editor -> Lint) to check for syntax errors.
  • Consider workflow: rules: For controlling whether an entire pipeline runs at all (rather than just individual jobs), explore the top-level workflow: rules keyword. This acts as a pipeline-level gate.

FAQs – Rules


What is the rules keyword in GitLab CI/CD?
The rules keyword is a flexible mechanism used in .gitlab-ci.yml to define conditions for when a job should run. It allows you to control job execution based on factors like branch names, file changes, pipeline sources, variables, and more.
It replaces the older only and except keywords with more powerful, readable logic.


How do I use the rules keyword to run jobs only on a specific branch?
You can run a job only on a specific branch using the if expression inside rules:

deploy:
  script: ./deploy.sh
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

This job will only run when the pipeline is triggered on the main branch.


Can I use multiple rules for a single job?
Yes. You can list multiple rules in priority order. GitLab evaluates them top to bottom and executes the first matching rule.

test:
  script: npm test
  rules:
    - if: '$CI_COMMIT_BRANCH == "develop"'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

If the branch is develop, the first rule matches and the second is ignored.


What are common conditions I can use with rules?
You can use conditions based on:

  • $CI_COMMIT_BRANCH
  • $CI_COMMIT_TAG
  • $CI_PIPELINE_SOURCE
  • $CI_MERGE_REQUEST_ID
  • $CI_COMMIT_MESSAGE
  • File changes using changes

Example:

lint:
  script: npm run lint
  rules:
    - if: '$CI_COMMIT_MESSAGE =~ /lint/i'

How do I use the changes rule to run a job only if certain files are modified?
Use the changes rule to trigger a job when specific files or paths change:

unit-tests:
  script: npm test
  rules:
    - changes:
        - src/**/*.js
        - package.json

This job only runs if any JavaScript file in src/ or package.json is modified.


What is the difference between if and changes in rules?

  • if: Evaluates expressions using environment variables and operators.
  • changes: Triggers a job if any listed files have changed in the current commit or merge request.

You can also combine them:

job:
  script: echo "Triggered"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      changes:
        - config/*

This job runs only if the branch is main and config/* files changed.


How do I skip a job based on rules?
Use when: never to explicitly skip the job if a rule matches:

job:
  script: echo "Never runs"
  rules:
    - if: '$CI_COMMIT_BRANCH == "feature/*"'
      when: never

This job will be skipped for feature branches.


What does when do in a rules entry?
The when field in a rule controls how the job behaves when the rule matches:

  • on_success (default): run only if prior jobs succeed.
  • always: run regardless of pipeline status.
  • manual: require manual trigger from the UI.
  • never: skip the job.

Example:

notify:
  script: send_notification.sh
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
      when: manual

Can I use rules to trigger jobs only in merge requests?
Yes. GitLab provides the $CI_PIPELINE_SOURCE and $CI_MERGE_REQUEST_ID variables:

review-app:
  script: ./review.sh
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

This ensures the job runs only when triggered by a merge request pipeline.


How do rules compare to only and except?

  • rules is more powerful and flexible, supporting multiple conditions, expressions, and file-based changes.
  • only/except are older and support simpler conditions.

Example comparison:

# with only
job:
  only:
    - branches

# with rules
job:
  rules:
    - if: '$CI_COMMIT_BRANCH'

GitLab recommends using rules for all new configurations.


Can I run jobs on scheduled pipelines using rules?
Yes. Use the $CI_PIPELINE_SOURCE variable:

job:
  script: run_scheduled_task.sh
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'

This job runs only when the pipeline is started via a scheduled trigger.


Can I mix rules and only/except in the same job?
No. You cannot use rules together with only or except in the same job. It will result in a YAML validation error. You must choose one approach per job.


How do I make a job manual using rules?
Set when: manual inside the matching rule:

deploy:
  script: ./deploy.sh
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual

This adds a “Play” button in the pipeline UI, requiring a user to manually start the job.


What is the evaluation order of rules?
GitLab evaluates rules top to bottom, and the first matching rule is applied. Subsequent rules are ignored. If no rules match, the job is skipped.


Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com

Leave a Comment