GitLab CI Job Execution Control and the Transition from Only to Rules

The orchestration of Continuous Integration and Continuous Deployment (CI/CD) pipelines within GitLab relies heavily on the ability to define exactly when a specific unit of work—a job—should be added to a pipeline. At the core of this logic is the mechanism for conditional execution, historically managed by the only keyword and now evolved into the more robust rules system. In GitLab CI/CD, jobs are the fundamental elements of a pipeline, configured within the .gitlab-ci.yml file. These jobs execute on runners, often within Docker containers, and are designed to run independently, providing a full execution log for transparency and debugging. To manage the complexity of these pipelines, GitLab utilizes stages to group jobs; while stages run in a strict sequence, all jobs assigned to a single stage can execute in parallel, maximizing resource utilization and reducing total pipeline duration.

The historical implementation of job control via only and except provided a straightforward way to filter jobs based on branches, tags, or merge requests. However, as pipeline requirements grew more complex—requiring logical AND operations or dependencies on specific variable states—the limitations of only became apparent. Modern GitLab configurations favor rules, which offer a more granular approach to pipeline construction. The tension between these two systems often leads to troubleshooting challenges, especially when they are mixed within the same pipeline, as they possess different default behaviors that can trigger duplicate pipelines or unexpected job omissions.

The Legacy Mechanism: Only and Except

The only keyword serves as a filter to determine if a job should be added to the pipeline based on the Git reference or specific triggers. When only is used without additional keywords, it behaves identically to only: refs.

  • Ref-based filtering: This allows developers to restrict jobs to specific branches, such as main, or use regular expressions to match branch patterns, such as /^issue-.*$/.
  • Tag-based execution: By specifying tags within the only block, a job will only trigger when a new Git tag is pushed to a branch.
  • Merge request integration: The merge_requests keyword ensures a job only appears in pipelines specifically created for merge requests.
  • Default behavior: If a job definition does not include only, except, or rules, GitLab applies a default setting of only: - branches - tags. This means the job will run on any branch pipeline or tag pipeline by default.

The except keyword operates as the inverse of only. It prevents a job from being added to the pipeline if the conditions are met. For example, a job might be configured to run on all branches except for those matching a stable branch pattern, such as /^stable-branch.*$/.

A critical interaction occurs with scheduled pipelines. Because scheduled pipelines are configured to run on specific branches, any job using only: branches will also execute during a scheduled pipeline. To prevent this, developers must explicitly add except: schedules to the job configuration.

Transitioning to Rules and Logical Constraints

The industry shift from only to rules is driven by the need for complex conditional logic. A recurring point of failure for users of the only keyword is the inability to perform a logical AND operation. For instance, if a user requires a job to run only when a pipeline is a tag AND specific files in a folder (e.g., tests/*) have changed, the only keyword fails. In such a configuration, only treats the conditions as an OR operation, meaning the job appears if it is a tag OR if the files changed. This leads to pipelines becoming "stuck" if the job is set to allow_failure: false but the conditions for execution are only partially met.

The rules keyword solves this by allowing the use of if statements and predefined variables. By leveraging CI_PIPELINE_SOURCE, users can precisely control job inclusion.

Variable Description
CI_PIPELINE_SOURCE Controls when to add jobs based on the trigger source (e.g., push, schedule, merge_request_event).
CI_COMMIT_TAG Present when the pipeline is running on a Git tag.
CI_COMMIT_BRANCH Present when the pipeline is running on a branch.
CI_MERGE_REQUEST_IID Present only in merge request pipelines.

The CI_PIPELINE_SOURCE variable is particularly powerful for eliminating duplicate pipelines. For example, a job configured with both a push and a merge_request_event rule without a corresponding workflow: rules configuration will create double pipelines—one for the branch and one for the merge request. To avoid this, the when: never keyword can be used to explicitly exclude certain sources.

Predefined Variables and Pipeline Sources

To implement advanced logic within rules, GitLab provides a set of predefined variables that describe the context of the pipeline.

  • api: Used for pipelines triggered via the GitLab Pipelines API.
  • chat: Used for pipelines created through GitLab ChatOps commands.
  • external: Used when CI services other than GitLab are utilized.
  • external_pull_request_event: Used when an external pull request is created or updated on GitHub.
  • merge_request_event: Specifically for pipelines created during the creation or update of a merge request.
  • push: Triggered when code is pushed to a branch or tag.
  • schedule: Triggered by a predefined schedule.
  • web: Triggered by selecting "New pipeline" in the GitLab UI under the Build > Pipelines section.

These variables allow for high-precision targeting. For instance, if a developer wants a job to run for merge requests and scheduled pipelines, but specifically exclude branch or tag pipelines, they would use:

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

Advanced Job Configuration and State Management

Beyond the logic of when a job starts, GitLab CI/CD manages how jobs behave throughout their lifecycle. Every job is assigned a status that indicates its current state in the pipeline.

  • created: The job has been initialized but not yet processed by the system.
  • pending: The job is in the queue, awaiting an available runner.
  • preparing: The runner is setting up the execution environment (e.g., pulling a Docker image).
  • running: The job is actively executing its script on the runner.
  • success: The job completed successfully.
  • failed: The execution failed.
  • canceled: The job was aborted manually or automatically.
  • canceling: The job is in the process of being canceled, while the after_script is still running.
  • manual: The job is paused and requires a human operator to trigger its start.
  • skipped: The job was bypassed due to specific conditions or dependencies.
  • scheduled: The job is timed for a future execution.
  • waiting_for_callback: The job is paused awaiting a response from an external service.
  • waiting_for_resource: The job is paused until a required resource becomes available.

The use of manual jobs combined with rules is a common pattern for deployment stages. By setting when: manual, the job is added to the pipeline but does not execute until a user clicks the "play" button. If this is combined with allow_failure: false, the pipeline status will remain in a state that reflects the pending manual action.

Integration Challenges and Pitfalls

A critical failure point in GitLab CI/CD 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 difficult to debug.

Jobs using only/except and jobs using rules are handled differently by the GitLab pipeline engine. For example, a job with no rules defaults to running in branch pipelines (except: merge_requests). If another job in the same pipeline uses rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event", a push to a branch with an open merge request will trigger two separate pipelines: one branch pipeline running the "no-rules" job and one merge request pipeline running the "rules" job.

Another common issue involves the include keyword. When splitting .gitlab-ci.yml into multiple files for better organization, users have reported issues where only the first job of an included file is recognized. This often happens when the jobs in the included file are not explicitly assigned to a stage that is defined in the main (outer) configuration file. For instance, if the outer file defines:

yaml stages: - build - test - deploy

And the included file contains:

yaml build_job: stage: build script: echo "Build" another_build_job: script: echo "Another build"

The another_build_job may be omitted because it lacks a stage assignment that matches the outer file's stage list. To ensure all jobs are included, every job must be explicitly mapped to a valid stage.

Comparison of Job Control Methods

The following table provides a detailed comparison between the deprecated only approach and the modern rules approach.

Feature only / except rules
Logical AND Not supported (acts as OR) Supported via if conditions
Pipeline Source Limited to refs, variables Full access via CI_PIPELINE_SOURCE
Default Behavior Defaults to branches and tags Explicitly defined by the user
Flexibility Simple, but rigid Highly flexible and granular
Status Deprecated (some keywords) Current standard
Trigger Support Basic Comprehensive (API, Web, Chat, etc.)

Strategic Implementation of Job Rules

To achieve a professional-grade CI/CD architecture, developers should utilize the !reference tag to reuse rules across different jobs. This prevents the duplication of complex logic and ensures consistency across the pipeline.

When designing for merge requests, the recommended approach is to define workflow: rules at the top level of the configuration. This governs the entire pipeline's creation, preventing the "double pipeline" problem by specifying exactly which conditions should trigger a pipeline.

For those still using only: variables or except: variables, these are officially deprecated. The correct migration path is to transition these to rules: if statements. This transition not only ensures future compatibility with newer GitLab versions but also allows the use of more complex logical operators that were previously impossible.

Conclusion

The evolution of job control in GitLab CI/CD from the only keyword to the rules framework represents a shift toward programmatic precision. The only keyword, while intuitive for simple branch and tag filtering, lacks the logical depth required for modern, complex deployment pipelines. The inability to perform an AND operation—such as requiring both a tag and a specific file change—creates significant bottlenecks and unstable pipelines.

By adopting rules and leveraging the CI_PIPELINE_SOURCE variable, teams can eliminate redundant pipelines and ensure that jobs only execute under the exact conditions required. However, the transition requires a disciplined approach: developers must avoid mixing only and rules in a single pipeline and ensure that all included YAML files strictly adhere to the stages defined in the root configuration. The move toward rules allows for a more scalable and maintainable CI/CD architecture, transforming the pipeline from a simple sequence of scripts into a sophisticated, event-driven automation engine.

Sources

  1. GitLab Forum: GitLab-ci.yml include only includes the first job
  2. GitLab Forum: How to run a job only when on tags and if specific folder has changed
  3. GitLab Docs: Job Rules
  4. GitLab Docs: CI/CD Jobs
  5. GitLab Docs: Deprecated Keywords

Related Posts