GitLab CI Only Keyword Logic and Transition to Rules

The GitLab CI/CD pipeline architecture has historically relied on a set of keywords to control the inclusion of jobs based on specific Git references, event triggers, and file changes. Among these, the only keyword served as the primary mechanism for defining job execution boundaries. However, as GitLab evolved toward a more flexible and powerful logic engine, the only and except keywords were superseded by the rules keyword. Understanding the nuanced behavior of only, its inherent limitations regarding boolean logic, and the migration path to rules is critical for maintaining stable, predictable deployment pipelines.

The Functional Mechanics of the Only Keyword

The only keyword is designed to restrict the execution of a job to specific scenarios, such as specific branches, tags, or pipeline sources. When a job is configured with only, GitLab evaluates the current Git reference to determine if the job should be added to the pipeline.

If a job is defined without only, except, or rules, the system applies a default behavior. By default, such jobs are configured to run on both branches and tags. This is functionally equivalent to explicitly stating:

yaml only: - branches - tags

The only keyword supports several types of references and triggers, which are essential for segmenting pipelines based on the intent of the code push.

  • branches: The job runs on push events to any branch.
  • tags: The job runs when the Git reference for a pipeline is a tag.
  • triggers: The job runs for pipelines created via a trigger token.
  • web: The job runs when a pipeline is manually created via the GitLab UI (Build > Pipelines).
  • merge_requests: The job runs specifically for merge request pipelines.

The impact of using these keywords is that it allows developers to isolate heavy-duty tasks, such as production deployments or comprehensive integration tests, so they do not run on every single feature branch commit, thereby saving runner minutes and reducing noise in the pipeline view.

Boolean Logic Limitations and the OR Behavior

A critical point of failure for many users is the misunderstanding of how only handles multiple conditions, specifically when combining refs and changes. In the legacy only syntax, providing multiple criteria often results in an "OR" logic rather than an "AND" logic.

For instance, consider a configuration intended to run a job only when a tag is pushed AND specific files in a folder (e.g., tests/*) have been modified:

yaml only: changes: - tests/* refs: - tags

In this scenario, the user's intent is a logical intersection (AND). However, GitLab interprets this as a union (OR). The job will appear in the pipeline if the pipeline is a tag, regardless of whether the tests folder changed, or if the tests folder changed, regardless of whether it is a tag.

The real-world consequence of this behavior is catastrophic in pipelines where allow_failure: false is set. If a tag is pushed without changes to the specified folder, the job still appears due to the "OR" logic. Because the job is present but potentially lacks the context to run successfully, or is set to manual but required for the pipeline to progress, the pipeline becomes stuck. This forces developers to either manually skip jobs or weaken their failure constraints, compromising the integrity of the CI/CD process.

Deprecated Keywords and the Shift to Rules

GitLab has officially deprecated several aspects of the only and except keywords to streamline the YAML configuration and provide more granular control. Specifically, only:variables and except:variables are deprecated. These were previously used to control job inclusion based on the status of CI/CD variables.

The industry standard has shifted toward rules:if, which provides a far more robust way to handle conditional logic using predefined variables. The transition is not merely a change in syntax but a shift in how GitLab evaluates the pipeline graph.

Legacy Keyword Modern Replacement Logic Capability
only: refs rules: - if: $CI_COMMIT_BRANCH == ... Complex Boolean (AND/OR)
only: changes rules: - changes: [...] Combined with if for AND logic
only: variables rules: - if: $VARIABLE == "value" Full variable expression support
except rules: - when: never Explicit exclusion

The contextual layer here is that mixing only/except with rules in the same pipeline is strictly forbidden. While it may not trigger a YAML syntax error, it leads to inconsistent default behaviors. For example, jobs without rules default to except: merge_requests. If one job uses rules and another uses only, you may encounter "double pipelines" where both a branch pipeline and a merge request pipeline are triggered for the same commit, leading to redundant resource consumption and potential race conditions during deployment.

Advanced Implementation of Rules for Tag and Change Constraints

To achieve the "AND" logic that only fails to provide—specifically running a job only when a tag is present AND certain files have changed—developers must use the rules keyword. In rules, multiple conditions within a single list item are evaluated as a logical AND.

To implement a job that runs only on tags and only when files in tests/* are modified, the configuration should be:

yaml test: stage: test rules: - if: '$CI_COMMIT_TAG' changes: - tests/* when: manual - when: never

In this configuration:
1. The if: '$CI_COMMIT_TAG' ensures the pipeline is a tag pipeline.
2. The changes: - tests/* ensures the specified files were modified.
3. Both must be true for the when: manual trigger to apply.
4. The final - when: never ensures that if the above conditions are not met, the job is excluded entirely from the pipeline rather than defaulting to "on success".

This approach solves the "stuck pipeline" problem because the job simply will not exist in the pipeline graph unless both conditions are met, removing the need to toggle allow_failure.

Pipeline Source Variables and Trigger Control

The precision of rules depends heavily on the use of predefined CI/CD variables. The CI_PIPELINE_SOURCE variable is the primary tool for identifying how a pipeline was started.

The following table details the values of CI_PIPELINE_SOURCE and their implications:

Value Description Impact on Job Execution
api Pipelines triggered by the pipelines API Allows external system integration to trigger jobs
chat Pipelines created by GitLab ChatOps Enables conversational triggers for CI tasks
external CI services other than GitLab Connects third-party CI providers
external_pull_request_event External GitHub pull requests Syncs GitHub PRs with GitLab pipelines
merge_request_event MR created or updated Isolates MR logic from branch logic
push Standard git push The most common trigger for branch/tag pipelines
schedule Scheduled pipelines Controls nightly or periodic tasks
web Manually started via UI Allows developers to trigger "on-demand" pipelines

By utilizing these variables, a developer can create a sophisticated workflow:rules block at the top of the .gitlab-ci.yml file. This prevents the creation of duplicate pipelines. For example, to prevent a pipeline from running for merge requests while allowing all other types:

yaml workflow: rules: - if: $CI_MERGE_REQUEST_ID when: never - when: always

This global configuration ensures that the pipeline doesn't trigger both for the push and for the associated merge request, which is a common source of confusion for users who find their jobs running twice.

Resolving Branch-Specific Pipeline Failures

A common point of confusion for "noobs" and enthusiasts is the scope of the .gitlab-ci.yml file. A frequent error is assuming that a single master .gitlab-ci.yml applies globally across all branches regardless of the branch's content. In reality, GitLab evaluates the version of the .gitlab-ci.yml file present on the branch being built.

If a user finds that a pipeline runs on the master branch but never on other branches (like dev or uat), it is often because the .gitlab-ci.yml file on those branches does not contain the necessary job definitions or has restrictive only keywords.

One workaround discovered by users is to maintain discrete .gitlab-ci.yml files on each branch. However, this is a maintenance nightmare. The professional approach is to make the configuration branch-agnostic using rules.

Example of a branch-agnostic publish job using rules:

yaml publish: stage: publish rules: - if: '$CI_COMMIT_BRANCH == "master"' when: on_success - if: '$CI_COMMIT_BRANCH == "uat"' when: on_success - if: '$CI_COMMIT_BRANCH == "test"' when: on_success - if: '$CI_COMMIT_BRANCH == "dev"' when: on_success - when: never script: - echo "Publishing ..."

This configuration replaces the legacy only: - master - uat - test - dev syntax. It explicitly checks the branch name and decides whether to run the job. By ending the list with - when: never, the developer ensures the job is excluded from any branch not listed (such as feature branches), maintaining a clean and efficient pipeline.

Analysis of Rule-Based vs. Only-Based Workflows

The transition from only to rules represents a move from a declarative "where to run" model to a conditional "when to run" model. The only keyword was essentially a shortcut for a few common scenarios. rules, however, provides a full logic engine.

The most significant advantage of rules is the ability to define the when state (manual, on_success, always, never) based on the evaluation of the if statement. In the legacy only system, the when keyword was global to the job. If you set when: manual with only, the job would always be manual if the only conditions were met. With rules, you can have a job that is automatic if it's a tag and a specific file changed, but manual in all other scenarios:

yaml test: stage: test rules: - if: '$CI_COMMIT_TAG' changes: - tests/* when: on_success - when: manual script: - echo "test"

This level of granularity is impossible with the only keyword. The impact for the user is a drastic reduction in manual intervention and a more automated "golden path" to production.

Sources

  1. GitLab Forum: How to run a job only when on tags and if specific folder has changed
  2. GitLab Docs: Deprecated Keywords
  3. GitLab Docs: Job Rules
  4. GitLab Forum: CICD pipeline only runs for the master branch and never for other branches

Related Posts