Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

By updated

Improve this page

GitLab logo

Does your pipeline run twice on every commit? This is the classic trap: one pipeline on the branch AND one on the merge request. This guide teaches you to control precisely when your pipelines run, thanks to pipeline sources and CI_PIPELINE_SOURCE.

The answer fits in a workflow:rules block of three rules, which allows one pipeline per situation: the merge request pipeline during the review, the main pipeline after the merge, the tag pipeline for releases. This filter is detailed in "Avoid branch/MR duplicates" below.

What you will learn

By the end of this module, you will know how to:

  • Understand CI_PIPELINE_SOURCE: identify how a pipeline was triggered
  • Avoid branch/MR duplicates: configure workflow:rules for a single pipeline
  • Configure schedules: run jobs at night or periodically
  • Target tags: trigger releases on version tags
  • Trigger through the API: start a pipeline from an external script
  • Adapt variables: handle the differences between pipeline sources

Prerequisites

Before continuing, make sure you are comfortable with:

Pipeline sources (CI_PIPELINE_SOURCE)

Every pipeline has a source that indicates the event behind its creation, exposed in the predefined variable CI_PIPELINE_SOURCE:

SourceTriggerUse case
pushBranch or tag pushedStandard pipeline
merge_request_eventMR created/updatedReview, pre-merge tests
scheduleCron configured in GitLabNightly tests, scans
web"Run pipeline" button in the UIDebug, manual deployment
apiCall to the /pipeline APIExternal automation
triggerCall with a trigger tokenExternal scripts and tools
pipelinetrigger: job of another projectMulti-project
parent_pipelinetrigger: job that starts a child pipelineParent-child

Use CI_PIPELINE_SOURCE in rules

The variable is available in all pipelines, whatever the source: it is the most direct criterion to test the origin of a pipeline, in workflow:rules as well as in the rules: of a job.

job_only_on_mr:
script: echo "Pre-merge tests"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
job_only_on_schedule:
script: echo "Nightly security scan"
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
job_only_on_tag:
script: echo "Release"
rules:
- if: $CI_COMMIT_TAG

Do not confuse the source with the branch. CI_COMMIT_BRANCH is defined in every branch pipeline, whatever its source: push, but also schedule, web, api or trigger. It is absent from a merge request pipeline and from a tag pipeline. A rule such as if: $CI_COMMIT_BRANCH == "main" therefore tests the branch, never the source: it matches the push on main as well as the nightly schedule that targets main.

Avoid branch/MR duplicates

This is the symptom that brings most readers to this page: two identical pipelines per commit, twice the runner minutes consumed, and an unreadable list of pipelines. The cause is not a bug, it is the normal behavior when the rules: of the jobs accept both branches and MRs, and the .gitlab-ci.yml file contains no workflow:rules to settle the question. Two fixes exist, and the first suits the vast majority of projects.

The problem

By default, when you create an MR, GitLab can create two pipelines:

  1. A branch pipeline (source: push)
  2. An MR pipeline (source: merge_request_event)

This wastes resources and clutters the interface.

workflow:rules is what removes the duplicate: it decides, before the pipeline even exists, which events give rise to a pipeline. Here the MR pipeline is the only one during the review, and the branch pipeline exists only on main. The rules are evaluated from top to bottom and the first one that matches decides the fate of the pipeline. A commit pushed to a working branch matches none of the three conditions: no branch pipeline is created. With no open MR, the Pipelines tab stays empty, with no error message.

workflow:
rules:
# MR pipelines for all branches with an open MR
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Branch pipeline only on main (after the merge)
- if: $CI_COMMIT_BRANCH == "main"
# Release tags
- if: $CI_COMMIT_TAG

This filter has a cost: a schedule, a trigger or an API call that targets a branch other than main matches no rule, so it creates no pipeline. The complete workflow, further down, adds the rules that restore these sources.

Solution 2: use CI_OPEN_MERGE_REQUESTS

This variant keeps a pipeline on branches as long as no MR is open, then switches to the MR pipeline as soon as a review starts. The when: never at the top is what removes the duplicate.

workflow:
rules:
# If an MR is open, let the MR pipeline handle it
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
# MR pipeline
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Branch pipeline (if no MR is open)
- if: $CI_COMMIT_BRANCH

Comparison of the approaches

The choice comes down to one question: do you want commits pushed to a branch with no open MR to trigger tests? If so, solution 2 avoids the duplicate without giving up fast feedback; if not, solution 1 remains the simplest to read.

ApproachAdvantageDrawback
MR onlySimple, clearNo pipeline on branches without an MR
CI_OPEN_MERGE_REQUESTSBranch pipeline if no MRMore complex

Tag pipelines

Tags are often used for releases. Target them specifically with $CI_COMMIT_TAG, which is defined only in a tag pipeline:

release:
stage: deploy
script:
- echo "Building release $CI_COMMIT_TAG"
- ./build-release.sh
rules:
- if: $CI_COMMIT_TAG

Useful variables for tags

In a tag pipeline, CI_COMMIT_BRANCH is absent: the tag does not point to a branch. The variables below are the ones to rely on to build a version number or to check a protection.

VariableContent
CI_COMMIT_TAGTag name (e.g. v1.2.3)
CI_COMMIT_REF_NAMETag or branch
CI_COMMIT_REF_PROTECTEDtrue if the tag/branch is protected

Pattern matching on tags

Regular expressions are written between slashes, without quotes, and are explicitly anchored with ^ and $: without anchors, v1.0.0-rc1 would also match the pattern of final versions.

release_production:
rules:
# Release tags (v1.0.0, v2.1.3, etc.)
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/
release_prerelease:
rules:
# Prerelease tags (v1.0.0-rc1, v2.0.0-beta)
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+-.+$/

Scheduled pipelines

A scheduled pipeline is not declared in the .gitlab-ci.yml file but in the project interface: the file only reacts to the schedule source. This is the mechanism behind nightly security scans, periodic reports and maintenance tasks, everything that must not depend on a commit.

Configuration in GitLab

The schedule is attached to a target branch and runs with the permissions of its creator: if that person loses access to the protected branch, the schedule silently stops working.

  1. Open the schedules

    Build > Pipeline schedules > New schedule

  2. Configure the cron

    • Description: "Nightly security scan"
    • Interval pattern: 0 2 * * * (every day at 2 AM)
    • Target branch: main
  3. Add variables (optional)

    • SCAN_TYPE = full
    • These variables are available in the pipeline

Target scheduled jobs

Variables defined in the schedule override those of the job. Declaring a default value at the job level, like SCAN_TYPE below, prevents the script from receiving an empty variable when the schedule does not define it.

security_scan:
stage: test
script:
- ./security-scan.sh --type $SCAN_TYPE
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
variables:
SCAN_TYPE: "quick" # Default value if not set

Useful schedule patterns

GitLab uses the standard five-field cron syntax, interpreted in the time zone chosen for the schedule ("Cron timezone" field). Here are the most common rhythms.

CronDescription
0 2 * * *Every day at 2 AM
0 2 * * 1-5Mon-Fri at 2 AM
0 */6 * * *Every 6 hours
0 0 * * 0Sunday at midnight
0 0 1 * *First day of the month

Triggering through the API

External triggering links GitLab to what lives outside it: a release tool, another repository, a ticketing system. Two paths exist and they do not produce the same CI_PIPELINE_SOURCE value, which has direct consequences on your rules:.

Trigger a pipeline

The call accepts variables through the variables[NAME]=value syntax, which arrive in the pipeline as ordinary CI variables. The --fail option makes curl fail on an HTTP error response, without which an invalid token would go unnoticed in a script.

Terminal window
curl -X POST \
--fail \
-F "token=$TRIGGER_TOKEN" \
-F "ref=main" \
-F "variables[DEPLOY_ENV]=staging" \
"https://gitlab.example.com/api/v4/projects/123/trigger/pipeline"

Create a trigger token

The token is displayed only once, at creation: if it is lost, you have to generate a new one and update every caller.

  1. Settings > CI/CD > Pipeline trigger tokens
  2. Add new token
  3. Copy the token (displayed only once)

A job that detects an API trigger

Testing both sources in the same condition saves you from having to know which call mechanism was used on the client side.

deploy_from_api:
script:
- echo "Deployment triggered through the API"
- echo "Environment: $DEPLOY_ENV"
rules:
- if: $CI_PIPELINE_SOURCE == "trigger" || $CI_PIPELINE_SOURCE == "api"

Variables by source

Some variables are available only for certain sources. This table shows the effect on the three most common sources: the branch exists only in branch pipelines, and the merge request variables only in MR pipelines.

Variablepushmerge_request_eventschedule
CI_COMMIT_BRANCHYesNoYes
CI_MERGE_REQUEST_IIDNoYesNo
CI_MERGE_REQUEST_SOURCE_BRANCH_NAMENoYesNo
CI_PIPELINE_SOURCEYesYesYes

This example gathers the five cases covered so far in a single file. The workflow: block filters upstream: a pipeline that matches none of its rules is never created, so the job rules: only have to tell the remaining cases apart. That is also why the test job has no rule: everything that reaches it has already been validated by workflow:.

workflow:
rules:
# 1. MR pipelines (review)
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# 2. Integration branches, whatever the source
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_COMMIT_BRANCH == "develop"
# 3. Release tags
- if: $CI_COMMIT_TAG
# 4. Schedules (nightly scans), on any branch
- if: $CI_PIPELINE_SOURCE == "schedule"
# 5. Manual run, API or trigger token
- if: $CI_PIPELINE_SOURCE == "web"
- if: $CI_PIPELINE_SOURCE == "api"
- if: $CI_PIPELINE_SOURCE == "trigger"
# Conditional jobs depending on the source
test:
stage: test
script: npm test
# Always runs (workflow has already filtered it)
security_scan:
stage: test
script: ./security-scan.sh
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
allow_failure: true
deploy_staging:
stage: deploy
script: ./deploy.sh staging
rules:
- if: $CI_COMMIT_BRANCH == "develop"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: manual
release:
stage: deploy
script: ./release.sh
rules:
- if: $CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/

Common mistakes

These six situations are diagnosed by printing the pipeline source (echo $CI_PIPELINE_SOURCE) and the branch (echo $CI_COMMIT_BRANCH). The common point of the first three: the configuration does exactly what it was written to do, it is the reading of the source or of the branch that is wrong.

ErrorCauseSolution
Pipeline runs twiceNo workflow:rules to filterAdd an MR-first workflow
CI_COMMIT_BRANCH emptyMR pipeline or tag pipelineUse CI_MERGE_REQUEST_SOURCE_BRANCH_NAME for an MR
No pipeline, empty Pipelines tabNo workflow: rule matchesAdd the rule for the source or the branch concerned
Schedule does not runProtected branch without permissionCheck the creator's permissions
Tag not detectedrules: too restrictiveAdd if: $CI_COMMIT_TAG
API returns 401Invalid tokenRegenerate the trigger token

Hands-on lab

The lab "Start a pipeline without a push" is played on a GitLab CE forge of your own, set up on a virtual machine with its runner and its registry: no minute quota, no identity verification, and a project you can break as often as you need. dsoxlab run prepares the starting project there.

The pipeline only knows how to be born from a push. You make it accept a schedule, an API call and a trigger token, then reserve one job to a single one of those sources. Grading is taken on the schedules, the tokens and the pipelines that actually ran: a declared source that never fired proves nothing. A schedule is not waited for, it is played: the play button under Build > Pipeline schedules starts a pipeline right away that carries the schedule source, so you can check your rule without waiting for the appointed hour.

The lab starts with dsoxlab run gl-08-declencher-autrement, after dsoxlab provision prepares the virtual machine, its runner and its registry. Installing DSOXLab and its prerequisites are described in the DSOXLab documentation. The sections above explain each trigger source: keep them at hand during the lab.

Key takeaways

ConceptKey
Pipeline sourceCI_PIPELINE_SOURCE identifies the trigger
Branch or sourceCI_COMMIT_BRANCH is defined in every branch pipeline (push, schedule, API, trigger), not in an MR or a tag
DuplicatesMR-first workflow:rules avoids 2 pipelines
No rule matchesNo pipeline is created, the Pipelines tab stays empty
SchedulesFor nightly scans, periodic maintenance
Tags$CI_COMMIT_TAG for releases
MR variablesCI_COMMIT_BRANCH does not exist, use CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
Open MRCI_OPEN_MERGE_REQUESTS lists the open MRs whose branch is the source

Knowledge check

Test your knowledge of GitLab pipeline triggers.

Knowledge check

Check what you have learned with this interactive quiz

10 questions
5 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn