The orchestration of continuous integration pipelines in GitLab requires a granular understanding of how jobs are evaluated, added, and executed. At the heart of this logic lies the rules keyword and the when clause, specifically the when: always directive. The behavior of when: always is not isolated; it interacts dynamically with pipeline sources, workflow configurations, and job-level constraints. Understanding this interaction is critical for preventing common architectural failures such as duplicate pipelines or blocking manual jobs. In GitLab CI, the determination of whether a job enters a pipeline is a sequential evaluation process. When rules are employed, GitLab evaluates each condition in order. If a condition is met, the associated action—such as when: always or when: never—is applied, and the evaluation for that specific job ceases. This deterministic approach ensures that the pipeline reflects the exact state of the repository and the intent of the developer.
The Mechanics of the When Always Directive
The when: always directive serves as a definitive instruction to the GitLab runner that a job should be executed regardless of the status of previous stages. This is fundamentally different from the default on_success behavior.
- Direct Fact:
when: alwaysensures a job is added to the pipeline and executed even if preceding jobs have failed. - Impact Layer: This allows developers to implement "cleanup" or "notification" jobs that must run regardless of whether the build succeeded or failed, ensuring that environments are not left in a corrupted state.
- Contextual Layer: When used within a
rulesblock,when: alwaysdetermines the execution state once theifcondition is satisfied. If norulesare specified, jobs default towhen: on_success, meaning a failure in an earlier stage would prevent the job from running.
Evaluation Logic and Rule Interactions
The application of when: always within the rules syntax creates a specific set of behaviors that can lead to unexpected outcomes if not properly configured.
- Direct Fact: If no
ifstatements in arulesblock are true, the job is not added to the pipeline. - Impact Layer: A user attempting to use
when: neverto exclude a job may find the job missing entirely if they do not provide a fallback rule that allows the job to run under other conditions. - Contextual Layer: This behavior forces a shift in how developers write CI YAML. Instead of thinking about what to exclude, developers must explicitly define when a job should be included.
The following table details the predefined variables used to control these rules:
| Variable | Description |
|---|---|
CI_PIPELINE_SOURCE = push |
True for branch and tag pipelines. |
CI_PIPELINE_SOURCE = schedule |
True for scheduled pipelines. |
CI_PIPELINE_SOURCE = merge_request_event |
True for pipelines created by merge requests. |
CI_PIPELINE_SOURCE = api |
True for pipelines triggered via the API. |
CI_PIPELINE_SOURCE = chat |
True for ChatOps command pipelines. |
CI_PIPELINE_SOURCE = external |
True when using non-GitLab CI services. |
CI_PIPELINE_SOURCE = external_pull_request_event |
True for GitHub external pull requests. |
Preventing Duplicate Pipelines
A common failure mode when utilizing when: always is the creation of duplicate pipelines. This occurs when a job is configured to run on both push events and merge request events without a governing workflow.
- Direct Fact: Using
rulesto trigger a job on both$CI_PIPELINE_SOURCE == "push"and$CI_PIPELINE_SOURCE == "merge_request_event"withoutworkflow: rulescauses double pipelines. - Impact Layer: Duplicate pipelines consume excessive runner resources, increase build times, and create confusion in the GitLab UI, as two separate pipeline instances are triggered for a single commit.
- Contextual Layer: To mitigate this, GitLab recommends the use of
workflow: rules, which defines whether a pipeline should be created at all, rather than just whether a specific job should be included.
An example of a configuration that avoids double pipelines:
yaml
job:
script: echo "This job does NOT create double pipelines!"
rules:
- if: $CI_PIPELINE_SOURCE == "push"
when: never
- when: always
Interaction Between Manual Triggers and Always Logic
Developers often attempt to combine automated validation with manual overrides. This is frequently seen when integrating file-change detection with when: always.
- Direct Fact: A job configured with
changesandwhen: always, followed by awhen: manualrule, may still run unexpectedly in branch pipelines. - Impact Layer: Users expecting a job to remain dormant unless a file changes or they manually trigger it may find the job executing every time a branch is created, leading to unnecessary compute costs.
- Contextual Layer: The presence of
allow_failure: truein a manual rule can change the pipeline's blocking behavior. Ifallow_failureis removed, the manual job becomes a blocking action, preventing the pipeline from completing until the job is manually executed.
Avoiding Anti-Patterns in Rule Configuration
The repetition of rules across multiple jobs is a significant anti-pattern that degrades the maintainability of the CI configuration.
- Direct Fact: Duplicating
rules: - changes: - path/**/*across multiple jobs (e.g., fmt, validate, build, deploy) creates redundant code. - Impact Layer: When the directory structure changes, developers must update the path in every single job, increasing the likelihood of human error and configuration drift.
- Contextual Layer: This can be solved using job templates (hidden jobs) and the
extendskeyword. By defining a hidden job (starting with a dot) that contains the rules, multiple jobs can inherit the logic without duplication.
Example of refactored rules using extends:
```yaml
.dev:
rules:
- changes:
- dev/*/
fmt-dev:
extends:
- .fmt
- .dev
validate-dev:
extends:
- .validate
- .dev
build-dev:
extends:
- .build
- .dev
deploy-dev:
extends:
- .deploy
- .dev
```
Advanced Rule Composition and Reference Tags
For more complex configurations, GitLab provides the !reference tag to allow for the reuse of specific rule blocks across different jobs.
- Direct Fact: The
!referencetag allows for the reuse of rules in different jobs. - Impact Layer: This provides a higher level of composition than
extends, allowing developers to pick and choose specific configuration blocks to inject into jobs. - Contextual Layer: This prevents the "YAML anchor" approach, which is limited to the current file and is often harder for teams to follow.
Infrastructure and Runner Optimization
The execution of jobs triggered by when: always is heavily dependent on the underlying runner configuration and image retrieval strategy.
- Direct Fact: Using public Docker images can lead to rate limiting.
- Impact Layer: Pipelines may fail unexpectedly when the runner cannot pull the required image, stalling the entire CI/CD process.
- Contextual Layer: This is mitigated by configuring runners with
--docker-pull-policy "if-not-present"or utilizing the GitLab Dependency Proxy.
To implement the Dependency Proxy, the following image syntax is used:
yaml
image: ${CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX}/alpine:latest
Analysis of Pipeline Source Constraints
The behavior of when: always is fundamentally steered by the CI_PIPELINE_SOURCE variable. This variable acts as the primary filter for determining the context of the execution.
- Direct Fact:
CI_PIPELINE_SOURCEdetermines if a pipeline is triggered by an API, a schedule, a push, or a merge request. - Impact Layer: Without precise filtering, a job set to
when: alwayswill trigger on every single event, regardless of whether that event is relevant to the job's purpose. - Contextual Layer: The integration of
CI_PIPELINE_SOURCEwithwhen: neverallows for precise exclusion. For example, a job can be set to run always, except when the source is atriggerevent.
Example of excluding a specific source:
yaml
rules:
- if: '$CI_PIPELINE_SOURCE == "trigger"'
when: never
- when: always
Comprehensive Analysis of Logic Failures
The transition from only/except to rules introduced a shift in default behaviors that can lead to troubleshooting difficulties.
- Direct Fact: Jobs with no rules default to
except: merge_requests, whereas jobs withrulesfollow the logic defined in therulesblock. - Impact Layer: Mixing
only/exceptandrulesin the same pipeline creates a hybrid environment where some jobs run in branch pipelines and others in merge request pipelines, leading to inconsistent pipeline results. - Contextual Layer: This inconsistency is a primary driver for the "duplicate pipeline" phenomenon, as one pipeline is triggered by the branch logic and another by the merge request logic.
Final Technical Analysis
The implementation of when: always in GitLab CI is not a simple toggle but a component of a complex conditional evaluation engine. The primary risk associated with when: always is the lack of constraints; when used without workflow: rules or specific if conditions, it can lead to resource exhaustion and pipeline duplication.
The most robust architecture for implementing when: always involves three layers of protection:
1. A workflow: rules block to define the global pipeline trigger.
2. Hidden job templates using extends to standardize rule sets.
3. Explicit CI_PIPELINE_SOURCE checks to ensure the job only executes in the appropriate context.
Failure to adhere to these patterns results in "anti-patterns" where CI code becomes a source of technical debt, characterized by redundant paths and unpredictable job executions. The shift toward rules over only/except provides more power but requires a disciplined approach to ensure that when: always does not inadvertently trigger unnecessary compute cycles.