GitLab CI/CD Job Control via the Only Keyword and Pipeline Logic

The orchestration of continuous integration and continuous deployment within GitLab requires a granular understanding of how jobs are admitted into a pipeline. At the core of this mechanism is the ability to restrict job execution based on the Git reference, the pipeline source, or specific environmental conditions. Historically, this was managed primarily through the only and except keywords, which provide a declarative method for defining the conditions under which a job should be added to a pipeline. While modern GitLab versions emphasize the transition toward the more flexible rules syntax, the only keyword remains a foundational element in many legacy configurations and specific use cases across Free, Premium, and Ultimate tiers.

The fundamental purpose of the only keyword is to prevent unnecessary resource consumption and avoid deployment errors by ensuring that specific tasks—such as production deployments or heavy integration tests—occur only on designated branches, such as main or stable. Without these controls, every push to every feature branch would trigger a full suite of jobs, leading to "pipeline noise," increased runner queue times, and potential instability in production environments.

The Architecture of GitLab CI/CD Pipelines

To understand the application of the only keyword, one must first comprehend the structural hierarchy of a GitLab pipeline. A pipeline is the primary execution unit, configured via the .gitlab-ci.yml file. This file serves as the blueprint for the entire automation process.

Pipelines are composed of several layers of abstraction:

  • Global YAML keywords: These are top-level configurations that dictate the overall behavior of the pipeline, such as defining the stages or global variables.
  • Stages: These define the sequential grouping of jobs. Stages run in a strict linear order. For instance, a build stage must complete successfully before a test stage begins. If any job within a stage fails, the pipeline typically terminates early, preventing the execution of subsequent stages.
  • Jobs: These are the smallest units of execution. A job is a set of instructions (scripts) executed by a GitLab Runner, often within a Docker container. Unlike stages, jobs within the same stage run in parallel, maximizing the efficiency of the available runner infrastructure.

In a typical three-stage pipeline, the flow is as follows:

  1. Build Stage: A job such as compile executes to transform source code into binaries.
  2. Test Stage: Multiple jobs, such as test1 and test2, run concurrently to validate the build. These only execute if the compile job succeeds.
  3. Deploy Stage: Jobs that move the validated code into a target environment.

Functional Mechanics of the Only Keyword

The only keyword is a job-level configuration used to determine if a job should be included in the pipeline based on the Git reference (branch, tag, or merge request). When only is used without any accompanying sub-keywords, it is functionally equivalent to only: refs.

The primary utility of only is to isolate job execution. For example, a developer may want a specific job to run exclusively on the main branch. The configuration would look like this:

yaml some_job_only_run_on_main_branch: script: echo "hello from main" only: - main

This ensures that the job is ignored for all other branches, preventing the echo "hello from main" command from executing on feature branches where it would be irrelevant.

Default Behaviors and Implicit Assignments

It is critical to understand what happens when the only keyword is omitted entirely. If a job definition does not utilize only, except, or rules, GitLab applies a default configuration. In these instances, the job is implicitly set to run on both branches and tags.

For example, the following two configurations are logically identical in the eyes of the GitLab CI engine:

yaml job1: script: echo "test"

and

yaml job2: script: echo "test" only: - branches - tags

This default behavior ensures that basic jobs are always integrated into the pipeline regardless of whether the trigger was a branch push or a tag creation, providing a baseline of visibility for all changes.

Advanced Filtering with Refs and Regular Expressions

The only keyword supports a wide array of filters to accommodate complex branching strategies. Beyond simple branch names, it allows for the use of regular expressions and predefined categories.

Using Refs and Regex

The refs keyword allows users to specify a list of branches, tags, or merge requests. This can be a literal string or a regular expression. For example, a job can be configured to run on the main branch and any branch that starts with the prefix issue-:

yaml job1: script: echo only: - main - /^issue-.*$/ - merge_requests

In this scenario, the regular expression /^issue-.*$/ ensures that any branch created to track a specific issue (e.g., issue-123-fix-login) will trigger the job, while unrelated branches will not.

The Complementary Except Keyword

The except keyword functions as the inverse of only. While only defines the "allow list," except defines the "block list." This is particularly useful for excluding specific events, such as scheduled pipelines, from running jobs that are otherwise intended for all branches.

For instance, if a job is configured with only: branches, it will also run on scheduled pipelines because those pipelines are associated with a specific branch. To prevent this, the except: schedules keyword must be added:

yaml job2: script: echo except: - main - /^stable-branch.*$/ - schedules

The Transition from Only/Except to Rules

GitLab has introduced the rules keyword as a more powerful and flexible alternative to only and except. While only is limited to Git references, rules can evaluate complex logical expressions, including the state of CI/CD variables and the pipeline source.

Comparison of Logic and Behavior

The most significant danger in a .gitlab-ci.yml file is the mixing of only/except and rules within the same pipeline. While this may not trigger a YAML syntax error, it creates divergent default behaviors that are notoriously difficult to troubleshoot.

Consider a scenario where one job uses no rules and another uses rules:

```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 this configuration:
- job-with-no-rules defaults to except: merge_requests. It runs on all branch pipelines.
- job-with-rules is explicitly tied to merge request events.

If a branch has an open merge request, a push to that branch will trigger two separate pipelines: one branch pipeline and one merge request pipeline. This results in duplicate execution, where job-with-no-rules runs in the branch pipeline and job-with-rules runs in the merge request pipeline.

Managing Double Pipelines

To avoid duplicate pipelines, GitLab recommends the use of workflow: rules. If a user implements a - when: always rule without a global workflow configuration, GitLab will display a pipeline warning.

A non-recommended configuration that avoids double pipelines but lacks proper workflow control is:

yaml job: script: echo "This job does NOT create double pipelines!" rules: - if: $CI_PIPELINE_SOURCE == "push" when: never - when: always

The proper approach is to use workflow: rules to define the top-level conditions for the entire pipeline's existence, rather than managing it at the individual job level.

Understanding CIPIPELINESOURCE and Variable Control

The precision of job filtering is heavily dependent on the CI_PIPELINE_SOURCE predefined variable. This variable informs the rules engine (and by extension, the logic replacing only) exactly how the pipeline was triggered.

Pipeline Source Values

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

Value Description
api Pipelines triggered via the GitLab Pipelines API.
chat Pipelines created through GitLab ChatOps commands.
external Pipelines initiated by external CI services outside of GitLab.
external_pull_request_event Triggered when a GitHub pull request is created or updated.
merge_request_event Triggered when a merge request is created or updated.
push Triggered by a standard git push to a branch or tag.
schedule Triggered by a predefined pipeline schedule.
web Triggered by selecting "New pipeline" in the GitLab UI.

Strategic Application of Source Variables

By leveraging these variables within rules: if blocks, users can create highly specific execution paths. For example, if a job should run for merge requests and scheduled pipelines, but must be explicitly blocked for standard push events, the configuration would be:

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

Deprecated Keywords and Modern Alternatives

As the GitLab ecosystem evolves, certain keywords associated with job control have been deprecated to streamline the YAML schema.

only:variables and except:variables

The keywords only:variables and except:variables are now deprecated. These were previously used to control job inclusion based on the presence or value of CI/CD variables.

The modern replacement is the rules: if syntax. Instead of using a deprecated variable check, users should utilize the if keyword to evaluate the variable's state. This shift allows for more complex boolean logic (AND/OR) and integration with other pipeline conditions.

Troubleshooting Common Configuration Failures

A frequent point of failure in GitLab CI/CD occurs when splitting the .gitlab-ci.yml file into multiple smaller files using the include keyword.

The "First Job Only" Include Problem

Users have reported issues where include seems to only bring in the first job from an external file. This is often not a bug in the include mechanism itself, but rather a failure in stage alignment.

Consider this scenario:
Main .gitlab-ci.yml:
yaml stages: - build - test - deploy include: - local: ci/build.gitlab-ci.yml

Included build.gitlab-ci.yml:
yaml build: script: echo "Build" another-build: script: echo "Another build"

If the merged YAML view only shows the build job and omits another-build, the cause is typically that the jobs in the included file are not assigned to a stage that exists in the main file. In GitLab, if a job is not explicitly assigned to a stage defined in the global stages list, it may be dropped or misplaced in the pipeline execution plan. To resolve this, ensure every job in the included file is mapped to a valid stage:

yaml build: stage: build script: echo "Build" another-build: stage: build script: echo "Another build"

Summary of Pipeline Variables across Events

The following table summarizes the availability of key variables across different pipeline types, which is essential for writing accurate only or rules logic:

Variable Branch Tag Merge request Scheduled
CI_COMMIT_BRANCH Yes Yes No No
CI_COMMIT_TAG No Yes No Yes (if configured)
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

Conclusion

The mastery of the only keyword and its successor, rules, is what separates a basic CI/CD setup from a professional, enterprise-grade automation pipeline. The ability to precisely control which jobs run on which branches—and under what conditions—directly impacts the speed of the development cycle and the reliability of the deployment process.

While only provides a simple, declarative way to handle Git references, the industry is moving toward the rules syntax due to its superior ability to handle merge request pipelines and avoid the "double pipeline" trap. The critical takeaway for any DevOps engineer is to avoid mixing these two paradigms within a single project. By strictly adhering to one method—preferably rules combined with workflow configurations—one can ensure a deterministic and maintainable pipeline. Furthermore, the integration of external YAML files via include must be handled with care, ensuring that all jobs are correctly mapped to the global stages to avoid the silent omission of jobs during the YAML merge process.

Sources

  1. GitLab Forum: GitLab-ci.yml include only includes the first job from the included file
  2. GitLab Forum: can all branch use only one gitlab-ci-yml
  3. GitLab Documentation: CI/CD pipelines
  4. GitLab Documentation: CI/CD Jobs
  5. GitLab Documentation: Job Rules
  6. GitLab Documentation: Deprecated Keywords

Related Posts