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
stagesor global variables. - Stages: These define the sequential grouping of jobs. Stages run in a strict linear order. For instance, a
buildstage must complete successfully before ateststage 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:
- Build Stage: A job such as
compileexecutes to transform source code into binaries. - Test Stage: Multiple jobs, such as
test1andtest2, run concurrently to validate the build. These only execute if thecompilejob succeeds. - 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.