The architectural complexity of continuous integration and continuous deployment (CI/CD) often necessitates a granular approach to job execution. In GitLab CI, the transition from simple job execution to conditional orchestration is handled primarily through the rules keyword. Unlike basic scripting, the rules section provides a declarative mechanism to determine whether a job should be added to a pipeline, whether it should be skipped, or whether it should be assigned a specific state such as always, manual, or never. Mastering the implementation of multiple if statements within these rules is critical for optimizing pipeline resource usage and ensuring that deployment logic remains airtight across diverse environments, such as development, staging, and production.
The core of GitLab CI's conditional logic revolves around the evaluation of predefined variables. These variables, such as $CI_COMMIT_BRANCH and $CI_PIPELINE_SOURCE, provide the context necessary for the runner to decide if a job is applicable to the current git event. When a developer implements multiple conditions, they are essentially creating a logic gate that filters out irrelevant jobs, preventing unnecessary compute spend and reducing the "noise" in the pipeline graph. This level of control is essential when managing complex branching strategies, such as GitFlow or Trunk-Based Development, where specific jobs must only trigger on the main branch or during specific merge request events.
Logic Gate Implementation and Boolean Operators
When implementing multiple conditions within a single if clause, GitLab CI utilizes syntax that closely mirrors Bash. This allows for the creation of complex logical expressions using AND (&&) and OR (||) operators.
The use of the OR operator is particularly common when a job needs to be executed across multiple specific branches. For instance, if a job must run on both the main and prod branches, but nowhere else, the logic must be grouped into a single expression. A common failure point for users is attempting to use the word or instead of the symbol ||.
The following table illustrates the correct syntax for boolean operations within rules:if:
| Operator | Logic Type | Syntax Example | Effect | |||
|---|---|---|---|---|---|---|
&& |
Logical AND | $A == "val" && $B == "val" |
Both conditions must be true | |||
| `| | ` | Logical OR | `$A == "val" | $B == "val"` | At least one condition must be true | |
=~ |
Regex Match | $CI_COMMIT_MESSAGE =~ /pattern/ |
Variable matches the regular expression |
If a developer attempts to use a natural language operator like or, the .gitlab-ci.yml file will trigger a syntax error. The correct approach for checking if a commit is on either one of two branches is:
yaml
rules:
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "prod"'
changes:
- requirements.txt
In this scenario, the job is only added to the pipeline if the commit is on main OR prod, AND the requirements.txt file has been modified. This dual-layer filtering ensures that dependency updates are only processed on critical branches, preventing wasteful test runs on feature branches that do not touch dependencies.
Order of Evaluation and Short-Circuiting
A fundamental characteristic of the rules keyword is that it is evaluated in the order defined. GitLab processes each rule from top to bottom; the first rule that matches the current state of the pipeline is the one that is applied. Once a match is found, all subsequent rules in the list are ignored.
This behavior creates a "first-match-wins" logic, which is critical when designing pipelines with both inclusion and exclusion criteria. If a user places a general rule (one that matches most scenarios) at the top of the list, more specific rules located below it will never be reached.
Consider a workflow where a pipeline should always be triggered via the API, regardless of the commit message, but otherwise should be blocked if the commit message contains "Setting version" on the default branch. If the rules are ordered incorrectly, the API trigger may be blocked by the commit message filter.
Incorrect ordering:
yaml
workflow:
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_MESSAGE =~ /Setting version/'
changes:
- version.sbt
when: never
- if: '$CI_PIPELINE_SOURCE == "trigger"'
In the example above, if the CI_PIPELINE_SOURCE is trigger but the commit message also contains "Setting version" on the default branch, the when: never rule matches first, and the pipeline is killed. To ensure the API trigger always works, the trigger rule must be moved to the top:
yaml
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "trigger"'
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_MESSAGE =~ /Setting version/'
changes:
- version.sbt
when: never
By rearranging the entry, the trigger rule is evaluated first. If it matches, the pipeline proceeds, and the "Setting version" exclusion is bypassed entirely.
Complex Conditional Grouping and Linter Optimization
When a job requires multiple conditions to be met simultaneously—such as requiring a specific branch, a specific pipeline source, and the presence of a tag—the syntax can become cumbersome. This often leads to very long lines in the YAML file, which can be difficult to read and maintain.
The standard way to enforce that multiple conditions are all true is to chain them using the && operator. For example, if a job should only run when the branch is development, the source is either push or web, and a tag is present, the logic looks like this:
yaml
- if: $CI_COMMIT_BRANCH == "development" && ($CI_PIPELINE_SOURCE == "push" || $CI_PIPELINE_SOURCE == "web") && $CI_COMMIT_TAG
This a single-line approach is logically sound but visually cluttered. While GitLab's YAML parser requires these conditions to be in a single if statement to be treated as a combined requirement, users can use !reference tags to reuse common rule sets across different jobs. This allows the developer to define a complex set of rules once and apply them to multiple jobs without duplicating the long strings of logic, thereby improving the maintainability of the CI configuration.
Analysis of CIPIPELINESOURCE and Trigger Variables
The CI_PIPELINE_SOURCE variable is the primary mechanism for controlling the type of pipeline being executed. Understanding its possible values is essential for constructing accurate if statements.
The following table details the available values for CI_PIPELINE_SOURCE and their corresponding contexts:
| Value | Description |
|---|---|
api |
Pipelines triggered via the GitLab Pipelines API |
chat |
Pipelines created through GitLab ChatOps commands |
external |
Pipelines triggered by external CI services |
external_pull_request_event |
Pull requests created or updated on GitHub |
merge_request_event |
Pipelines created when a merge request is updated or created |
push |
Standard pipelines triggered by a git push |
schedule |
Pipelines triggered by a scheduled trigger |
The interaction between these variables and other context variables like $CI_COMMIT_BRANCH or $CI_COMMIT_TAG defines the precision of the pipeline. For instance, a job configured for both merge_request_event and schedule would look like this:
yaml
job1:
script:
- echo "Running scheduled or MR job"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "push"
when: never
In this configuration, the first two rules act as "allow" lists, and the final rule acts as a "deny" list for standard pushes. This ensures that the job does not run on every single push, which would waste runners' minutes.
Variable Expansion Limitations in Rules
A critical technical limitation exists regarding the expansion of nested variables within the rules:if clause. In GitLab CI, if a variable is defined in the variables section and its value contains another variable (nested expansion), that expansion occurs successfully within the script section but does not occur within the rules:if clause.
This means that if VAR_A is defined as hello and VAR_B is defined as $VAR_A-world, a check like if: $VAR_B == "hello-world" may fail because the rules:if logic does not expand the reference to VAR_A during the initial pipeline creation phase. This is not categorized as a bug but as a design limitation of the GitLab CI engine, and it requires developers to avoid relying on nested variables when defining conditional job logic.
Prevention of Duplicate Pipelines
A common issue in GitLab CI is the creation of "double pipelines." This occurs when a job is configured to run on both a push event and a merge_request_event without a global workflow:rules definition to arbitrate between them.
If a job is defined as follows:
yaml
job:
script: echo "This job creates double pipelines!"
rules:
- if: $CI_PIPELINE_SOURCE == "push"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Whenever a developer pushes code to a branch that has an open merge request, GitLab will trigger two separate pipelines: one for the push and one for the merge request. This results in the same job running twice, which is inefficient.
To resolve this, developers must use workflow:rules. The workflow keyword defines the rules for the entire pipeline. If the workflow rules determine that the pipeline should not run, no jobs are created, regardless of individual job rules. Alternatively, using a when: never rule within the job's rules can prevent the job from appearing in certain contexts, but the most robust solution is a global workflow policy that explicitly separates push and merge request pipelines.
Static YAML Limitations and Bash Workarounds
It is important to distinguish between the YAML configuration layer and the execution layer. The rules:if statements are part of the static YAML configuration and are evaluated by the GitLab server before the job is ever sent to a runner. Consequently, it is impossible to use complex programming structures like if/else blocks or for loops directly within the YAML structure to control job inclusion.
For example, a user cannot write:
```yaml
THIS IS INVALID YAML SYNTAX
if ($CITYPE == "mergerequest") {
- echo "Test"
}
```
Because YAML is a data serialization language and not a programming language, logic must be handled in one of two ways:
1. Use the rules:if syntax to control whether the job exists in the pipeline.
2. Use a Bash script within the script section to handle the logic during execution.
If a job must always be part of the pipeline but should only perform certain actions based on a variable, the logic should be moved into the script:
yaml
job_execution:
script:
- |
if [ "$CI_MERGE_REQUEST_IID" != "" ]; then
echo "This is a merge request pipeline"
else
echo "This is a standard pipeline"
fi
This approach allows the use of full Bash capabilities, including reading environment variables and executing conditional commands, which is far more flexible than the restricted syntax of rules:if.
Conclusion
The mastery of multiple if statements in GitLab CI is the difference between a rigid, wasteful pipeline and a dynamic, professional orchestration system. By utilizing boolean operators like && and ||, developers can create precise triggers that respond only to the correct git events. However, the "first-match-wins" nature of rule evaluation requires a strategic ordering of conditions, where specific exclusions (when: never) and high-priority triggers (like API calls) are placed carefully to avoid being overshadowed by general rules.
While the system is powerful, it is constrained by the static nature of YAML and the lack of nested variable expansion in the rules context. These limitations force a clear separation between pipeline orchestration (handled by rules:if) and runtime logic (handled by Bash scripts). Avoiding the pitfalls of double pipelines through workflow:rules and leveraging !reference tags for maintainability ensures that as the project grows, the CI configuration remains scalable and legible. The ability to combine CI_PIPELINE_SOURCE with branch and tag checks provides a comprehensive toolkit for automating the software delivery lifecycle with surgical precision.