skip to content

In GitLab CI, why can a single push create two pipelines for a branch that has an open merge request, and how do `workflow:rules` stop it?

level: seniorimportance: should knowfreq 58%

answer

  1. one pipeline per triggering event
  2. two events, one commit
  3. the gate above job rules
  4. branch variable is absent on merge request pipelines
  5. a variable that lists the open merge requests

basics

~20 s

GitLab can create both a branch pipeline and a merge request pipeline for the same commit when job rules accept both sources. workflow:rules decide whether a pipeline is created at all, so one entry keeps merge request pipelines and another rejects branch pipelines while a merge request is open.

solid answer

~50 s

GitLab creates a pipeline per triggering event. A push to a branch is one event (`$CI_PIPELINE_SOURCE == "push"`), and if the branch has an open merge request whose jobs also accept `merge_request_event`, that is a second event for the same commit — so you get two pipelines, double runner cost, and two sets of statuses on the merge request. `workflow:rules` is the pipeline-level gate that fixes it: it is evaluated once, before any job rules, and decides whether the pipeline is created at all. The canonical form keeps merge request pipelines, refuses a branch pipeline when the branch has open merge requests, and otherwise allows branch pipelines: `- if: $CI_PIPELINE_SOURCE == "merge_request_event"` · `- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS` with `when: never` · `- if: $CI_COMMIT_BRANCH`. GitLab ships this as the `Workflows/MergeRequest-Pipelines.gitlab-ci.yml` template. Note `$CI_COMMIT_BRANCH` is not defined in merge request pipelines, which is what makes the branch clauses safe.

code

yaml · 12 lines
yaml
workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH

test:
  script: ./run-tests.sh
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

go deeper

for a junior

Know that GitLab creates a pipeline per event and that a branch pipeline and a merge request pipeline are different things. Recognise the duplicate-pipeline symptom when you see two runs for one commit.

for a middle

Explain the workflow: block line by line, including why the branch clause is guarded by CI_COMMIT_BRANCH and what CI_OPEN_MERGE_REQUESTS contains. Say that workflow rules run before job rules.

for a senior

Diagnose it from the pipeline list and CI_PIPELINE_SOURCE rather than guessing, apply the fix, and then check the second-order effect: which jobs silently stopped running because their branch-only guards no longer match.

for a principal

Decide the org-wide model — merge request pipelines with merge trains versus branch pipelines only — and account for the cost: runner minutes saved, which merge-request-only features you gain, and the migration burden on every repository's existing rules.

## Why there are two pipelines at all GitLab creates a pipeline in response to an *event*, and the same commit can produce more than one event. The important sources, exposed as `$CI_PIPELINE_SOURCE`, include `push`, `merge_request_event`, `web`, `schedule`, `api`, `trigger` and `parent_pipeline`. A **branch pipeline** runs on a push and has `$CI_COMMIT_BRANCH` set. A **merge request pipeline** runs against the merge request and has the `$CI_MERGE_REQUEST_*` variables set — and, importantly, **no** `$CI_COMMIT_BRANCH`. When a job's `rules:` accept both sources (for instance one rule for `merge_request_event` and another that matches any branch), and the branch has an open merge request, GitLab has two valid reasons to create a pipeline and creates both. Symptoms: - Two pipeline entries in the pipeline list for one commit, roughly identical. - Duplicate statuses and duplicate notifications on the merge request. - Runner minutes doubled on the busiest path in the project. - Confusing merge trains and "pipeline must succeed" checks pointing at different runs. ## `workflow:` is the pipeline-level gate Job-level `rules:` decide whether a *job* is added. `workflow:rules` decides whether the *pipeline* exists, and it is evaluated first. Its `when` is limited to `always` (the default when omitted) and `never`; there is no `manual` pipeline. It can also set pipeline-wide `variables`, and — as of GitLab 16.x — `workflow:name` gives the pipeline a human-readable name. The standard anti-duplication block: ```yaml workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - if: $CI_COMMIT_BRANCH ``` Read it with first-match-wins in mind: 1. A merge request pipeline is always allowed. 2. Otherwise, if this is a branch pipeline (`$CI_COMMIT_BRANCH` is set) **and** the branch has open merge requests (`$CI_OPEN_MERGE_REQUESTS` is a non-empty, comma-separated list of merge request references), refuse to create the pipeline. This is the entry that removes the duplicate. 3. Otherwise, allow the branch pipeline — which is what you want on the default branch and on branches with no merge request yet. GitLab distributes this as the `Workflows/MergeRequest-Pipelines.gitlab-ci.yml` template, which is why you see the same three lines in many projects. ## The variables that make it work - `$CI_COMMIT_BRANCH` — **absent** in merge request pipelines and in tag pipelines. Because it is absent, a rule guarded by it can never accidentally match a merge request pipeline. Candidates who write `$CI_COMMIT_REF_NAME` instead lose that property, because `CI_COMMIT_REF_NAME` is set in both. - `$CI_OPEN_MERGE_REQUESTS` — set on branch pipelines when the source branch has open merge requests; empty otherwise. - `$CI_PIPELINE_SOURCE` — the event that caused the pipeline. - `$CI_MERGE_REQUEST_IID`, `$CI_MERGE_REQUEST_TARGET_BRANCH_NAME` — only in merge request pipelines. ## Choosing which model you want The fix above prefers merge request pipelines while a merge request is open. That is the common choice, because merge request pipelines are what the merge request widget, merge trains and approval checks are built around. The opposite policy — always branch pipelines, no merge request pipelines — is also coherent and simpler; it just means merge-request-only features are unavailable. What you cannot do sustainably is leave both enabled and treat the duplicates as noise. Be deliberate about the consequence: once a branch's pipelines are merge request pipelines, jobs guarded by `$CI_COMMIT_BRANCH` stop running there. Guards must be rewritten in terms of `$CI_PIPELINE_SOURCE` or `$CI_MERGE_REQUEST_*`. Missing that is the second-order bug that follows the fix — the duplicates disappear and so, quietly, does the deploy-preview job. ## Adjacent controls worth naming - A `workflow:rules` block that matches nothing means **no pipeline at all**; a push then reports "no pipeline" rather than a failure, which is disorienting the first time. - Recent GitLab versions add `workflow:auto_cancel` options for superseding in-progress pipelines on new commits; check your version before relying on the exact keyword. - `workflow:rules` cannot see anything a job computes; like all rules it is evaluated at creation time. ## How to verify After changing the workflow block, push to a branch with an open merge request and confirm exactly one pipeline appears, that it is the merge request one, and that the jobs you expect are present in it. Then push to the default branch and confirm a branch pipeline is created there. Both directions matter — over-tightening the block is the usual way this fix goes wrong.

  • After adopting merge request pipelines, a job guarded by `$CI_COMMIT_BRANCH` stops running on feature branches. Why?
    `$CI_COMMIT_BRANCH` is not defined in merge request pipelines, so any rule that requires it can never match there. Once the workflow block suppresses branch pipelines for branches with an open merge request, that job simply has no pipeline it can appear in. Rewrite the guard in terms of `$CI_PIPELINE_SOURCE == "merge_request_event"` or the `$CI_MERGE_REQUEST_*` variables.
  • What is the difference between `when: never` in `workflow:rules` and in a job's `rules:`?
    In `workflow:rules` it prevents the whole pipeline from being created — the push produces no pipeline at all. In a job's `rules:` it removes that single job while the pipeline still runs. `workflow:rules` is also evaluated first and only accepts `always` or `never` as its `when` value.
  • Why is `$CI_COMMIT_REF_NAME` a worse guard than `$CI_COMMIT_BRANCH` in this block?
    `CI_COMMIT_REF_NAME` is set in branch, tag and merge request pipelines alike, so a rule keyed on it cannot distinguish the pipeline type — the very distinction the anti-duplication block depends on. `CI_COMMIT_BRANCH` exists only in branch pipelines, which makes it a reliable test for "this is a branch pipeline".

saying these in an interview costs you the question

  • Blames the runner or a webhook for the second pipeline
  • Deletes one pipeline manually instead of gating creation
  • Thinks job-level rules can prevent pipeline creation
  • Uses CI_COMMIT_REF_NAME as a branch-pipeline test
  • Assumes CI_COMMIT_BRANCH is set in merge request pipelines

context