GitLab CI Pipeline Control and the When Always Logic

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: always ensures 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 rules block, when: always determines the execution state once the if condition is satisfied. If no rules are specified, jobs default to when: 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 if statements in a rules block are true, the job is not added to the pipeline.
  • Impact Layer: A user attempting to use when: never to 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 rules to trigger a job on both $CI_PIPELINE_SOURCE == "push" and $CI_PIPELINE_SOURCE == "merge_request_event" without workflow: rules causes 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 changes and when: always, followed by a when: manual rule, 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: true in a manual rule can change the pipeline's blocking behavior. If allow_failure is 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 extends keyword. 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 !reference tag 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_SOURCE determines 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: always will trigger on every single event, regardless of whether that event is relevant to the job's purpose.
  • Contextual Layer: The integration of CI_PIPELINE_SOURCE with when: never allows for precise exclusion. For example, a job can be set to run always, except when the source is a trigger event.

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 with rules follow the logic defined in the rules block.
  • Impact Layer: Mixing only/except and rules in 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.

Sources

  1. GitLab Forum - Conditional Rule Always Trigger Job
  2. GitLab Documentation - Job Rules
  3. GitLab Forum - Job with When Manual Runs Every Time
  4. Dev.to - GitLab CI Best Practices

Related Posts