GitLab CI/CD Job Execution Logic and Conditional Rule Orchestration

The orchestration of continuous integration and continuous delivery pipelines within GitLab relies heavily on the ability to define precisely when a specific job should enter the execution phase and when it should be discarded. This logic is primarily managed through the rules keyword, which provides a sophisticated mechanism for including or excluding jobs based on a variety of triggers, environment variables, and pipeline contexts. At its core, the rules keyword allows developers to move beyond static pipeline definitions into dynamic workflows where the pipeline adapts its behavior based on whether a commit is a standard push, a merge request event, a scheduled trigger, or an API call. This flexibility is critical for implementing DevSecOps workflows, where security scans might only run on merge requests, while deployment jobs are restricted to specific protected branches.

The fundamental principle of rules evaluation is sequential order. GitLab evaluates each rule in the order it is listed. Once a match is found, the evaluation stops, and the job is either added to the pipeline or excluded entirely, depending on the configuration of that specific rule. This behavior differs significantly from older keywords like only and except, and understanding this distinction is paramount to avoiding the "double pipeline" phenomenon, where both a branch pipeline and a merge request pipeline trigger for the same commit, leading to redundant resource consumption and confusing status indicators.

The Mechanics of the Rules Keyword

The rules keyword is available across all GitLab tiers, including Free, Premium, and Ultimate, and is supported on GitLab.com, Self-Managed, and Dedicated offerings. Its primary purpose is to determine the eligibility of a job to run within a given pipeline context. Because rules are evaluated before any jobs are actually executed, they cannot rely on dotenv variables created during the script execution of a previous job; they must rely on predefined variables or variables defined at the project/group level.

The behavior of a job under rules is determined by the first match. For instance, if a job is configured with multiple if statements, GitLab will check them one by one. If the first if condition is true, the job is processed according to the attributes of that rule (such as when: manual or allow_failure: true), and no further rules in that list are checked.

Rules Evaluation and Job Attributes

When a rule matches, it can modify the job's behavior using specific attributes:

  • when: manual: The job is added to the pipeline but will not start automatically. A user must manually trigger it via the GitLab UI.
  • allow_failure: true: If the job is manual and not run, or if it fails, the pipeline continues to the next stage regardless.
  • when: never: The job is explicitly excluded from the pipeline if this rule matches.
  • when: always: The job is included in the pipeline regardless of other conditions, provided this is the first matching rule.

The interaction between if and when is critical. For example, a job defined to run only during a merge request event might be set to when: manual to ensure that a human operator verifies the code before a specific expensive test suite is triggered.

Precision Control with CIPIPELINESOURCE

To implement granular control, GitLab provides the CI_PIPELINE_SOURCE predefined variable. This variable identifies the exact mechanism that triggered the pipeline, allowing developers to isolate jobs to specific event types.

Detailed Mapping of Pipeline Sources

The following table outlines the various values of CI_PIPELINE_SOURCE and their corresponding triggers:

Value Description
api Pipelines triggered via the GitLab Pipelines API.
chat Pipelines initiated through GitLab ChatOps commands.
external Pipelines triggered by external CI services outside of GitLab.
external_pull_request_event Pipelines created when an external pull request from GitHub is updated.
merge_request_event Pipelines created specifically for merge requests (MR pipelines).
push Pipelines triggered by a Git push to a branch or tag.
schedule Pipelines triggered by a predefined schedule.

Practical Implementation of Source Filtering

If a requirement exists to run a job only for merge requests and scheduled pipelines, but explicitly exclude them from standard push or tag pipelines, the configuration must be explicit. A common mistake is assuming a job won't run on a push if a merge request rule is present. To be safe, an explicit when: never must be applied to the push source.

Example configuration for restricted execution:

yaml job1: script: - echo "Running specific logic" rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_PIPELINE_SOURCE == "schedule" - if: $CI_PIPELINE_SOURCE == "push" when: never

In this scenario, the first two rules act as "allow" filters. If neither matches, but the source is push, the final rule ensures the job is excluded.

Solving the Double Pipeline Dilemma

A frequent point of failure in GitLab CI configuration is the "double pipeline" issue. This occurs when a commit is pushed to a branch that has an active merge request. By default, GitLab may trigger both a branch pipeline (triggered by the push event) and a merge request pipeline (triggered by the merge_request_event).

The Cause of Duplication

Double pipelines typically happen when jobs are defined using a mix of rules and the absence of rules, or when rules are too broad. For example, a job with no rules defaults to except: merge_requests, meaning it runs in all cases except merge requests. Meanwhile, a job with a rule specifically for merge_request_event will run in the MR pipeline. When both exist in one pipeline definition, two separate pipelines are spawned for a single push.

Example of a failing configuration that causes double pipelines:

```yaml
job-with-no-rules:
script: echo "This job runs in branch pipelines."

job-with-rules:
script: echo "This job runs in merge request pipelines."
rules:
- if: $CIPIPELINESOURCE == "mergerequestevent"
```

In the above case, the job-with-no-rules executes in the branch pipeline, and job-with-rules executes in the merge request pipeline.

Mitigation via workflow:rules

The most effective way to prevent duplicate pipelines is the use of workflow:rules. While job-level rules control whether a specific job is included, workflow:rules control whether the entire pipeline is created. workflow:rules are evaluated before any individual job rules and take precedence.

If a workflow:rules block sets a condition to when: never, the pipeline is not created, regardless of what the individual jobs specify. This is the primary mechanism for ensuring that only one type of pipeline (either branch or merge request) is active at a time.

Example of an optimized workflow to prevent duplicates:

yaml workflow: rules: - if: $CI_PIPELINE_SOURCE == "schedule" when: never - if: $CI_PIPELINE_SOURCE == "push" when: never - when: always

In this configuration, pipelines triggered by schedules or pushes are blocked, while all other triggers (such as merge requests) are allowed.

Dynamic Variables and Contextual Execution

GitLab allows for the definition of variables that only exist when specific conditions are met through the rules:variables element. This enables the creation of dynamic execution environments where a variable's value changes based on the pipeline context.

Case Study: Environment-Specific Deployments

Consider a scenario where an application must be deployed to a "stable" environment if the commit is on the default branch, but to a "dev" environment for any other branch.

yaml job: variables: DEPLOY_VERSION: "dev" rules: - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH variables: DEPLOY_VERSION: "stable" script: - echo "Deploying $DEPLOY_VERSION version"

In this setup, the default DEPLOY_VERSION is "dev". However, if the CI_COMMIT_REF_NAME matches the default branch, the rules:variables block overrides the value to "stable". This eliminates the need for complex shell scripting within the script block to determine the target environment.

Advanced Conditional Logic: Shell Scripts vs. YAML Rules

There is a fundamental limitation in GitLab CI's YAML configuration: it is a static declaration. You cannot perform complex "if-then-else" logic within the YAML structure itself to decide which script lines to run. All logic in the YAML is used to determine if a job exists in the pipeline, not how the script behaves internally.

The Bash Script Approach for Internal Logic

When a user needs to execute a specific command only if a condition is met inside a job that has already started, they must move that logic into a shell script. For example, if a job should always run, but only perform a specific action if it is part of a merge request, a bash script is required.

Implementation steps for a helper script:

  1. Create a script file in a dedicated directory, such as .gitlab/ci/run.sh.
  2. Define the logic using environment variables like CI_MERGE_REQUEST_ID.
  3. Ensure the script is executable.

Example script content (.gitlab/ci/run.sh):

```bash

!/bin/bash

if [ "$CIMERGEREQUEST_ID" != "" ]; then
echo "Test: This is a merge request pipeline"
fi
```

To integrate this into the pipeline:

yaml myjob: script: - ./.gitlab/ci/run.sh

This approach allows for complex logic and local testing. A developer can test the script locally before committing it by simulating the environment variable:

bash CI_MERGE_REQUEST_ID=42 bash run.sh

Integrating Shell Logic with YAML Rules

While helper scripts handle internal job logic, they can be combined with rules for maximum efficiency. If a job should only run when CI_MERGE_REQUEST_ID is present, and then perform a specific action, the combination looks like this:

yaml myjob: rules: - if: $CI_MERGE_REQUEST_ID when: always - when: never script: - echo "Test"

Comparison of Pipeline Trigger Variables

Understanding which variables are available in which pipeline context is essential for writing correct if statements.

Variable Branch Pipeline Tag Pipeline Merge Request Pipeline Scheduled Pipeline
CI_COMMIT_BRANCH Yes Yes No No
CI_COMMIT_TAG Yes Yes (if configured) No No
CI_PIPELINE_SOURCE = push Yes Yes No No
CI_PIPELINE_SOURCE = schedule No No No Yes
CI_PIPELINE_SOURCE = merge_request_event No No Yes No
CI_MERGE_REQUEST_IID No No Yes No

Rule Reuse and Maintenance

As pipelines grow in complexity, repeating the same rules blocks across dozens of jobs leads to maintenance challenges. GitLab provides the !reference tag to allow for the reuse of rule definitions. This allows a developer to define a set of standard rules in a hidden job (starting with a dot) and reference them in multiple active jobs.

Example of rule reuse:

```yaml
.standardrules:
rules:
- if: $CI
PIPELINESOURCE == "mergerequestevent"
- if: $CI
PIPELINE_SOURCE == "push"
when: never

testjob:
rules: !reference [.standard
rules, rules]
script:
- echo "Running tests"

lintjob:
rules: !reference [.standard
rules, rules]
script:
- echo "Running linter"
```

Detailed Analysis of Configuration Pitfalls

The interaction between different GitLab CI keywords can create subtle bugs that are difficult to troubleshoot.

The Risk of Mixing only/except with rules

It is strongly advised not to mix only/except and rules within the same pipeline. While this may not trigger a YAML syntax error, the two systems have different default behaviors.

  • Jobs with rules are evaluated using the new logic.
  • Jobs without rules (or using only/except) default to except: merge_requests.

When these are mixed, a developer might find that some jobs run in a branch pipeline while others run in a merge request pipeline for the same commit, leading to fragmented execution and "ghost" jobs that appear to be missing from the pipeline view but are actually excluded by conflicting logic.

Warning on when: always without workflow:rules

If a job uses a - when: always rule without a corresponding workflow:rules block, GitLab may display a pipeline warning. Although the pipeline might still function and avoid double pipelines in some specific cases (such as having a when: never for push events), it is considered a non-recommended practice. The architectural standard is to use workflow:rules to define the pipeline's existence and job-level rules to define the job's participation.

Conclusion

The mastery of if and when logic in GitLab CI/CD is the dividing line between basic automation and professional-grade pipeline engineering. By leveraging the rules keyword, developers can create highly conditional workflows that optimize resource usage and ensure that the right tests and deployments happen at the right time. The transition from static pipelines to dynamic ones requires a deep understanding of CI_PIPELINE_SOURCE, the precedence of workflow:rules over job rules, and the tactical use of external bash scripts for logic that exceeds the capabilities of YAML.

The ability to map specific variables like CI_MERGE_REQUEST_IID to execution states allows for the implementation of advanced patterns such as Directed Acyclic Graphs (DAG) using the needs keyword, where jobs can trigger based on a complex web of dependencies and conditional matches. Ultimately, the goal is to eliminate redundancy (double pipelines) and increase predictability, ensuring that the CI/CD pipeline serves as a reliable gatekeeper for code quality and deployment stability.

Sources

  1. GitLab Documentation: Job rules
  2. GitLab Forum: How to make an if statement in the CI file
  3. MDN Blog: Optimizing DevSecOps workflows with GitLab conditional CI/CD pipelines

Related Posts