Merge Request Pipelines
Rules & Workflow
Avoid Duplicates
Fork Security
Merged Results
Learn how to configure GitLab merge request pipelines. From rules and workflow rules to avoiding duplicate pipelines and handling fork security, build MR-specific pipelines that run the right jobs at the right time.
MR Pipelines
Rules
GitLab CI/CD
What are Merge Request Pipelines?
A merge request pipeline is a pipeline that runs specifically for a merge request, not for every branch push. It runs on the contents of the source branch and is triggered when you create an MR, push a new commit to the MR's source branch, or manually run a pipeline from the MR's Pipelines tab. These pipelines are labeled "merge request" in the pipeline list, making it easy to distinguish them from branch pipelines .
The key difference from a branch pipeline is context. A branch pipeline runs on every push to a branch — even if that branch has no open merge request. A merge request pipeline only runs when there's an active MR, and it has access to additional predefined variables like $CI_MERGE_REQUEST_IID, $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, and $CI_MERGE_REQUEST_TARGET_BRANCH_NAME. These variables let jobs tailor their behavior specifically to the MR context .
Merge request pipelines are not enabled by default. You must configure your .gitlab-ci.yml to run jobs when $CI_PIPELINE_SOURCE == "merge_request_event". This can be done per-job using rules, or for the entire pipeline using workflow:rules. Once configured, MR pipelines become the primary feedback mechanism for code review: every push to the MR triggers the pipeline, and the results appear directly in the MR widget .
Core Principle: Use MR pipelines to validate changes before merge. They run on the source branch, have MR-specific variables, and their status is displayed in the MR. Don't run your full production pipeline on every branch push — that's what MR pipelines are for.
MR Pipeline
Runs when an MR is created, updated, or manually triggered. Runs on source branch only. Labeled "merge request".
rules
Per-job condition. Use if: $CI_PIPELINE_SOURCE == "merge_request_event" to run a job in MR pipelines.
workflow:rules
Pipeline-level condition. Controls whether the entire pipeline runs for MR events, branches, tags, etc.
Duplicate Pipelines
When both branch and MR pipelines run for the same push. Use $CI_OPEN_MERGE_REQUESTS to prevent duplicates.
Merged Results
Runs on the result of merging source into target. Catches integration issues before merge.
Fork Security
Fork MRs run pipelines in the fork project by default, not the parent. Parent members can trigger parent pipelines with warnings.
Configuring with rules
The simplest way to add jobs to merge request pipelines is the rules keyword. A rule with if: $CI_PIPELINE_SOURCE == "merge_request_event" makes the job run only in MR pipelines. You can combine this with changes to run a job only when specific files change — for example, running frontend tests only when frontend files are modified .
Rules are evaluated per-job, which gives you fine-grained control. A build job might run in both branch and MR pipelines, while a deploy job runs only on the default branch. A lint job might run only on MRs. This flexibility lets you design a pipeline that runs the right jobs in the right context, minimizing wasted compute and maximizing relevant feedback.
The only: merge_requests syntax is the legacy equivalent of the rules approach. It still works, but GitLab recommends rules for new configurations because it's more expressive and supports conditions like changes and exists. If you're maintaining an existing pipeline with only, it's worth migrating to rules when you have the chance .
# Run a job only in merge request pipelines
job1:
script:
- echo "This job runs in merge request pipelines"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Run a job only when specific files change in an MR
lint:
script:
- echo "This job runs only when JavaScript files change"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- "*.js"
- "frontend/**/*"
# Run a job in both MR and branch pipelines
build:
script:
- echo "Build runs everywhere"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
# Legacy syntax (only: merge_requests)
legacy_job:
script:
- echo "This job runs in merge request pipelines"
only:
- merge_requests
# Available MR variables in jobs:
# $CI_MERGE_REQUEST_IID - MR number
# $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
# $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
# $CI_MERGE_REQUEST_TITLE
# $CI_MERGE_REQUEST_LABELS
# Example: use MR variables in script
comment_mr:
script:
- echo "Processing MR !$CI_MERGE_REQUEST_IID"
- echo "Source: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
- echo "Target: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
Rules Best Practices:
- Use
rules instead of only/except for new pipelines
- Combine
if: merge_request_event with changes to run jobs only when relevant files change
- Use MR variables (
$CI_MERGE_REQUEST_IID) in scripts for MR-specific behavior
- Test rules with different pipeline sources to ensure jobs run in the right context
Configuring with workflow:rules
While rules controls individual jobs, workflow:rules controls the entire pipeline. If the workflow rules evaluate to when: never, no pipeline runs at all — no jobs are evaluated. This is useful for preventing pipelines in contexts you don't care about, like scheduled pipelines or pushes to certain branches .
A common pattern is to run pipelines only for merge requests, the default branch, and tags. This covers the important contexts (review, integration, release) while skipping feature branch pushes that aren't part of an MR. The workflow rules are evaluated before any jobs, so the pipeline is created or not created based on these rules alone.
Another common pattern is switching from branch pipelines to MR pipelines when an MR is opened. Without this, a push to a branch with an open MR triggers both a branch pipeline and an MR pipeline — duplicate pipelines that waste resources and create confusion. The $CI_OPEN_MERGE_REQUESTS variable lets you detect this situation and suppress the branch pipeline, leaving only the MR pipeline .
# Run pipelines only for MRs, default branch, and tags
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
# Switch from branch to MR pipeline when MR is open
# Prevents duplicate pipelines
workflow:
rules:
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# What this does:
# - If a branch pipeline is triggered but an MR is open
# for that branch, the branch pipeline is suppressed
# - If no MR is open, the branch pipeline runs
# - MR pipelines always run
# Prevent pipelines for draft MRs
workflow:
rules:
- if: $CI_MERGE_REQUEST_TITLE =~ /^Draft:/
when: never
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Prevent infinite loops (e.g., release job creates tag)
workflow:
rules:
- if: $CI_COMMIT_TAG
when: never
- when: always
# Common workflow:rules examples:
# - Run for MRs only
# - Run for MRs and default branch
# - Run for MRs, default branch, and tags
# - Run for MRs, default branch, tags, and schedules
# - Exclude drafts, WIP, or specific commit messages
Workflow Rules Gotchas:
- Evaluated before jobs: If workflow rules say
never, no jobs run — even if job rules would match
- No fallback: If no workflow rule matches, the pipeline is not created
- Duplicate pipelines: Without the
$CI_OPEN_MERGE_REQUESTS rule, both branch and MR pipelines can run
- Infinite loops: A job that creates a tag can trigger a tag pipeline, which creates another tag. Use
when: never for tags
Avoiding Duplicate Pipelines
Duplicate pipelines are one of the most common sources of confusion with merge request pipelines. When you push to a branch that has an open merge request, GitLab can trigger both a branch pipeline (because the branch was pushed) and an MR pipeline (because the MR was updated). Two pipelines run for the same commit, both appear in the MR, and it's unclear which one determines merge eligibility .
The solution is to use workflow:rules with $CI_OPEN_MERGE_REQUESTS. This variable is set when a branch has at least one open merge request. The rule if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never suppresses the branch pipeline when an MR is open, leaving only the MR pipeline. When no MR is open, the branch pipeline runs normally .
It's important to understand that the MR pipeline and branch pipeline are different pipeline types, even for the same commit. The MR pipeline has MR-specific variables and is associated with the MR. The branch pipeline is associated with the branch. When "Pipelines must succeed" is enabled, GitLab checks the MR pipeline, not the branch pipeline — so suppressing the duplicate branch pipeline also avoids ambiguity about which pipeline blocks or allows merge .
# Prevent duplicate pipelines
workflow:
rules:
# Suppress branch pipeline when MR is open
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
# Run branch pipeline when no MR is open
- if: $CI_COMMIT_BRANCH
# Run MR pipeline
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# How $CI_OPEN_MERGE_REQUESTS works:
# - Set to a comma-separated list of MR IIDs
# if the branch has open MRs
# - Empty if no open MRs
# - Available in branch pipelines, not MR pipelines
# Alternative: use $CI_COMMIT_BRANCH with $CI_PIPELINE_SOURCE
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
# Debug: check which pipelines ran
# MR > Pipelines tab shows all pipelines
# Look for "merge request" label (MR pipeline)
# and branch pipeline (no label, or branch name)
# If both run:
# - Check workflow:rules
# - Check job rules (some jobs may force a pipeline)
# - Check if MR pipeline is configured
Duplicate Pipeline Symptoms:
- Two pipelines appear in the MR Pipelines tab for the same commit
- One is labeled "merge request", the other is a branch pipeline
- The branch pipeline may fail while the MR pipeline passes (or vice versa)
- "Pipelines must succeed" may behave unpredictably
Fork Security
Merge requests from forks have special security considerations. By default, a pipeline triggered by a fork MR runs in the fork project, not the parent project. This protects the parent project's secrets and runners: a malicious fork MR cannot access the parent's CI/CD variables or execute code on the parent's trusted runners .
Parent project members can choose to run a pipeline in the parent project for a fork MR. This uses the parent's CI/CD configuration, settings, and variables, and runs on the parent's runners. This is useful for ensuring the post-merge pipeline will pass in the parent project, but it carries risk: the fork MR may contain malicious code that tries to steal secrets. GitLab shows a warning before running such a pipeline, and the reviewer must accept it .
For organizations that want to prevent fork pipelines from running in the parent project entirely, there's a setting to disable it. This is a trade-off between security and convenience: disabling parent pipelines for forks is safer but means you can't validate fork MRs against the parent's full CI/CD configuration before merge. The right choice depends on your trust model and the sensitivity of your project's secrets .
# Fork MR pipeline behavior (default):
# - Pipeline runs in the fork project
# - Uses fork's CI/CD config, resources, and variables
# - Has access to fork's secrets, not parent's
# - Displayed with "fork" badge in parent MR
# Parent project can trigger a pipeline for a fork MR
# - MR > Pipelines tab > Run pipeline
# - Shows warning about security risks
# - Uses parent's CI/CD settings and variables
# - Runs on parent's runners
# Disable fork pipelines in parent project (API)
curl --request PUT \
--header "PRIVATE-TOKEN: " \
--url "https://gitlab.example.com/api/v4/projects/:id" \
--data "ci_allow_fork_pipelines_to_run_in_parent_project=false"
# Fork pipeline security best practices:
# - Treat fork MRs as untrusted code
# - Review changes before triggering parent pipeline
# - Use protected variables (not available to fork pipelines)
# - Use protected runners (not available to fork pipelines)
# - Consider requiring approval for fork pipelines
# - Don't expose secrets to fork pipelines
# Protected variables and runners:
# - Protected variables are NOT available to fork pipelines
# - Protected runners are NOT available to fork pipelines
# - This is a security boundary by design
# For trusted forks (e.g., internal teams):
# - Allow parent pipelines
# - Use manual trigger with warning
# - Audit fork MRs before running
Fork Security Best Practices:
- Treat fork MRs as untrusted until reviewed
- Use protected variables and runners (they're not available to forks)
- Review the diff before triggering a parent pipeline
- Consider disabling parent pipelines for public forks entirely
- Audit fork MRs for suspicious changes (e.g., to CI config, scripts)
Merged Results Pipelines
A merged results pipeline goes one step further than an MR pipeline. While an MR pipeline runs on the source branch alone, a merged results pipeline runs on the result of merging the source branch into the target branch. This catches integration issues that a source-only pipeline misses: if the target branch has changed in a way that conflicts with the source branch, the merged results pipeline will fail, even if the source branch passes on its own .
Merged results pipelines are configured in project settings: Settings → Merge requests → Merge options → "Enable merged results pipelines." Once enabled, GitLab creates a temporary merge commit and runs the pipeline on that commit. The pipeline is labeled "merged results" in the pipeline list, and its status is displayed in the MR .
The trade-off is speed and cost. Merged results pipelines run an extra merge step and may take longer. For projects with high merge velocity, this overhead can be significant. But for projects where integration issues are a real risk — or where compliance requires verifying the merged state — the extra safety is worth it. Merge trains build on this concept, running pipelines on the combined result of multiple MRs queued for merge.
# Enable merged results pipelines (UI)
# Settings > Merge requests > Merge options
# - Enable merged results pipelines: ✓
# Or via API
curl --request PUT \
--header "PRIVATE-TOKEN: " \
--url "https://gitlab.example.com/api/v4/projects/:id" \
--data "merged_results_pipelines_enabled=true"
# Merged results pipeline behavior:
# - Creates a temporary merge commit
# - Runs pipeline on that commit
# - Labeled "merged results"
# - Status shown in MR
# Differences from MR pipeline:
# MR pipeline: runs on source branch only
# Merged results: runs on source + target merged
# Merge train: runs on source + target + other queued MRs
# When to use merged results:
# - Target branch changes frequently
# - Integration issues are common
# - Compliance requires verifying merged state
# - You want to catch conflicts before merge
# When to skip merged results:
# - Merge velocity is high and overhead is costly
# - Target branch is stable
# - Source-only pipeline is sufficient
# Merge trains (advanced):
# - Enable merged results pipelines first
# - Enable merge trains in settings
# - MRs in the train are merged together
# - Each MR's pipeline runs on the combined result
# - Prevents "merge, then break main" scenarios
Pipeline Types Compared:
- Branch pipeline: Runs on every branch push. No MR context.
- MR pipeline: Runs on source branch when MR is created/updated. Has MR variables.
- Merged results: Runs on source + target merged. Catches integration issues.
- Merge train: Runs on source + target + other queued MRs. Prevents serial merge conflicts.
Complete Merge Request Pipeline Example
Here's a complete .gitlab-ci.yml that configures MR pipelines with workflow rules, job rules, and duplicate prevention.
# Complete merge request pipeline example
stages:
- build
- test
- deploy
# Workflow: control when pipelines run
workflow:
rules:
# Prevent duplicate pipelines when MR is open
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
# Run branch pipelines when no MR is open
- if: $CI_COMMIT_BRANCH
# Run MR pipelines
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Run tag pipelines
- if: $CI_COMMIT_TAG
# Build: runs everywhere
build:
stage: build
script:
- echo "Building..."
- npm ci
- npm run build
artifacts:
paths:
- dist/
# Lint: runs only in MR pipelines when JS files change
lint:
stage: test
script:
- npm run lint
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- "*.js"
- "*.ts"
- "frontend/**/*"
# Unit tests: run in MR and branch pipelines
test_unit:
stage: test
script:
- npm test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
# Integration tests: MR pipelines only, with MR variables
test_integration:
stage: test
script:
- echo "Testing MR !$CI_MERGE_REQUEST_IID"
- echo "Source: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
- echo "Target: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
- npm run test:integration
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Deploy: default branch only, manual
deploy_staging:
stage: deploy
script:
- ./deploy.sh staging
environment:
name: staging
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy_production:
stage: deploy
script:
- ./deploy.sh production
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
# Result:
# - MR pipelines run: build, lint (if JS changes), test_unit, test_integration
# - Branch pipelines run (no MR): build, test_unit, deploy_staging, deploy_production
# - Tag pipelines run: build, deploy_staging, deploy_production
# - No duplicate pipelines when MR is open
Troubleshooting Merge Request Pipelines
# Two pipelines when pushing to a branch
# - Check workflow:rules
# - Add rule: if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
# when: never
# - Check job rules don't force a branch pipeline
# MR pipeline not running
# - Check .gitlab-ci.yml has jobs with
# if: $CI_PIPELINE_SOURCE == "merge_request_event"
# - Check workflow:rules doesn't block MR pipelines
# - Check you have Developer role or higher
# MR pipeline runs but jobs are skipped
# - Check job rules (if: merge_request_event)
# - Check workflow:rules (may block entire pipeline)
# - Check changes: condition (file may not have changed)
# "An error occurred while trying to run a new pipeline"
# - The project may not have MR pipelines enabled
# - The configuration may have been removed
# - Push a new commit instead
# MR pipeline marked as failed but branch pipeline succeeded
# - Both pipelines exist (duplicate)
# - "Pipelines must succeed" checks the MR pipeline
# - Suppress branch pipeline with workflow:rules
# - Check MR pipeline logs for the actual failure
# Fork MR pipeline not running in parent project
# - By default, fork pipelines run in fork project
# - Parent members must manually trigger
# - Check fork pipeline settings
# - Ensure you have permission to run pipelines
# Fork MR pipeline warning not showing
# - Warning is shown when triggering parent pipeline
# - Unless triggered via API or /rebase
# - Review the MR diff before accepting
# Debug: check pipeline source
script:
- echo "Pipeline source: $CI_PIPELINE_SOURCE"
- echo "MR IID: $CI_MERGE_REQUEST_IID"
- echo "Open MRs: $CI_OPEN_MERGE_REQUESTS"
# Debug: list pipelines for an MR
# MR > Pipelines tab
# Look for "merge request" label (MR pipeline)
# vs branch name (branch pipeline)
Frequently Asked Questions
What's the difference between a branch pipeline and an MR pipeline?
A branch pipeline runs on every push to a branch. An MR pipeline runs when an MR is created or updated, runs on the source branch, and has MR-specific variables. MR pipelines are not enabled by default .
How do I enable MR pipelines?
Configure your .gitlab-ci.yml with jobs that have rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event", or use workflow:rules to run the entire pipeline for MR events.
Why do I get two pipelines for one MR?
You're running both branch and MR pipelines. Add a workflow rule: if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never to suppress the branch pipeline when an MR is open .
What's a merged results pipeline?
A pipeline that runs on the result of merging the source branch into the target branch. It catches integration issues that an MR pipeline (source-only) misses. Enable in project settings .
Do fork MRs run pipelines in the parent project?
By default, no — pipelines run in the fork project. Parent project members can manually trigger a pipeline in the parent project, but GitLab shows a security warning because the fork MR may contain malicious code .
What variables are available in MR pipelines?
$CI_MERGE_REQUEST_IID, $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME, $CI_MERGE_REQUEST_TARGET_BRANCH_NAME, $CI_MERGE_REQUEST_TITLE, and others. These are not available in branch pipelines.
How do I run a job only when specific files change in an MR?
Combine if: merge_request_event with changes: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" changes: - "frontend/**/*". The job runs only when those files change.
What's the difference between MR pipelines and merge trains?
MR pipelines validate a single MR. Merge trains validate multiple MRs together, running pipelines on the combined result of all queued MRs. This prevents "merge, then break main" scenarios.