How to avoid duplicated pipelines in GitLab with the workflow keyword

Imagine the following GitLab scenario. You open a merge request, push a commit, and GitLab starts two pipelines for it. This is not a bug, it is the default behaviour of the rules keyword, and workflow is the keyword that fixes it.

Why it happens

GitLab can create two different kinds of pipeline for one push:

Both pipelines are legitimate as far as GitLab is concerned, so unless you tell it otherwise it runs both.

The fix

workflow is evaluated once, at pipeline level, before any job is considered. If no rule matches, the pipeline is never created at all. That is the right place to decide which of the two pipelines described above you want:

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
      when: never
    - if: $CI_COMMIT_BRANCH

GitLab reads the rules top to bottom with the first match winning:

  1. A merge request pipeline always runs.
  2. A branch pipeline is cancelled when the branch has an open merge request, because rule 1 already covered that commit.
  3. Any other branch pipeline runs, which covers the case of pushing to a branch with no merge request yet.

The $CI_OPEN_MERGE_REQUESTS env variable holds a comma-separated list of merge requests that use the current branch as their source, and is empty when there are none.

The stricter variant

The version above still runs a pipeline on every branch push. If you only want CI on merge requests, the default branch and tags, be explicit and end with a catch-all never:

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    - if: $CI_COMMIT_TAG
    - when: never

The trailing - when: never is what makes the difference now. Without it, a rule set that matches nothing falls through to GitLab’s default.

Two things to keep in mind

Your job-level rules must match the pipeline rules. workflow decides whether a pipeline exists; rules on each job decide whether that job runs inside it. A job whose rules only reference $CI_COMMIT_BRANCH will quietly disappear once you switch to merge request pipelines, because GitLab leaves that variable empty in a merge request pipeline, so the rule evaluates to false with no error. Use $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, which holds the source branch name, there instead, or write rules that handle both sources.

Predefined variables differ between the two pipeline types. $CI_COMMIT_REF_NAME is the branch name in a branch pipeline but the merge request ref (refs/merge-requests/:iid/head) in a merge request pipeline, and anything keyed off it, like a review environment name, will change once you flip the switch:

deploy-review-job:
  stage: review
  script:
    - echo "Deploying to the review environment"
  environment:
    name: review/$CI_COMMIT_REF_NAME

If you rely on that value, pin it to $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME so the environment name stays stable.

Feel free to drop me a comment, if you found this article helpful.

comments powered by Disqus