
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:rulesfor 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:
| Source | Trigger | Use case |
|---|---|---|
push | Branch or tag pushed | Standard pipeline |
merge_request_event | MR created/updated | Review, pre-merge tests |
schedule | Cron configured in GitLab | Nightly tests, scans |
web | "Run pipeline" button in the UI | Debug, manual deployment |
api | Call to the /pipeline API | External automation |
trigger | Call with a trigger token | External scripts and tools |
pipeline | trigger: job of another project | Multi-project |
parent_pipeline | trigger: job that starts a child pipeline | Parent-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_TAGDo 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:
- A branch pipeline (source:
push) - An MR pipeline (source:
merge_request_event)
This wastes resources and clutters the interface.
Solution 1: MR pipeline only (recommended)
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_TAGThis 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_BRANCHComparison 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.
| Approach | Advantage | Drawback |
|---|---|---|
| MR only | Simple, clear | No pipeline on branches without an MR |
CI_OPEN_MERGE_REQUESTS | Branch pipeline if no MR | More 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_TAGUseful 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.
| Variable | Content |
|---|---|
CI_COMMIT_TAG | Tag name (e.g. v1.2.3) |
CI_COMMIT_REF_NAME | Tag or branch |
CI_COMMIT_REF_PROTECTED | true 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.
-
Open the schedules
Build > Pipeline schedules > New schedule
-
Configure the cron
- Description: "Nightly security scan"
- Interval pattern:
0 2 * * *(every day at 2 AM) - Target branch:
main
-
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 setUseful 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.
| Cron | Description |
|---|---|
0 2 * * * | Every day at 2 AM |
0 2 * * 1-5 | Mon-Fri at 2 AM |
0 */6 * * * | Every 6 hours |
0 0 * * 0 | Sunday 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.
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.
- Settings > CI/CD > Pipeline trigger tokens
- Add new token
- 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.
| Variable | push | merge_request_event | schedule |
|---|---|---|---|
CI_COMMIT_BRANCH | Yes | No | Yes |
CI_MERGE_REQUEST_IID | No | Yes | No |
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME | No | Yes | No |
CI_PIPELINE_SOURCE | Yes | Yes | Yes |
Recommended complete workflow
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 sourcetest: 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.
| Error | Cause | Solution |
|---|---|---|
| Pipeline runs twice | No workflow:rules to filter | Add an MR-first workflow |
CI_COMMIT_BRANCH empty | MR pipeline or tag pipeline | Use CI_MERGE_REQUEST_SOURCE_BRANCH_NAME for an MR |
| No pipeline, empty Pipelines tab | No workflow: rule matches | Add the rule for the source or the branch concerned |
| Schedule does not run | Protected branch without permission | Check the creator's permissions |
| Tag not detected | rules: too restrictive | Add if: $CI_COMMIT_TAG |
| API returns 401 | Invalid token | Regenerate 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
| Concept | Key |
|---|---|
| Pipeline source | CI_PIPELINE_SOURCE identifies the trigger |
| Branch or source | CI_COMMIT_BRANCH is defined in every branch pipeline (push, schedule, API, trigger), not in an MR or a tag |
| Duplicates | MR-first workflow:rules avoids 2 pipelines |
| No rule matches | No pipeline is created, the Pipelines tab stays empty |
| Schedules | For nightly scans, periodic maintenance |
| Tags | $CI_COMMIT_TAG for releases |
| MR variables | CI_COMMIT_BRANCH does not exist, use CI_MERGE_REQUEST_SOURCE_BRANCH_NAME |
| Open MR | CI_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
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
Review
(0/0)Score by domain
A domain groups several guides and gets several questions, so its score is more reliable than a per-topic score, which often rests on a single question.
Summary
Strengths
No topic passed at 80% on at least two questions yet.
Weak areas
No topic below the pass mark.
Recommended next
Hints before you try again?
Continue the course: GitLab CI/CD environments (deploy targets)
A new full quiz with random questions
Work again only on the questions you missed