Create at most 1 pipeline when in the context of merge requests
## Status Update
The Skip branch pipelines for merge requests feature is now available in BETA as of GitLab 19.2. [Learn more](https://docs.gitlab.com/ci/pipelines/settings/#skip-branch-pipelines-for-merge-requests)
## Problem Statement
Currently, when pushing to a branch with an open merge request, GitLab creates **two pipelines** - both a branch pipeline and a merge request pipeline. This creates several problems:
1. **Confusion about which pipeline is the source of truth** for merge request status
2. **Compliance enforcement gaps** - security policies can't reliably target "merge request context" pipelines
3. **Resource waste** from running duplicate jobs
4. **Split-brain scenarios** where mergeability checks may come from different pipelines
As highlighted in recent customer feedback, this particularly impacts Pipeline Security Policies where compliance teams cannot consistently enforce security scans across all merge requests due to the unpredictable pipeline source.
## Current Behavior (pre-fix)
* `git push` to a branch with open MR → Creates both branch pipeline AND merge request pipeline
* Projects must carefully configure rules to avoid job duplication
* Policy enforcement becomes inconsistent across projects using different pipeline strategies
## Original Phased Plan (from prior epic description)
| Phase | Original scope | Status |
|---|---|---|
| Phase 1: New opt-in setting | New project setting "Prevent duplicate pipelines in merge requests" (not enabled by default for new projects). When enabled, a `git push` to a branch with an open MR creates only the MR pipeline. Existing projects keep current behavior until they opt in. | **Shipped (BETA)** - !241086, !241641, !244815, !243637 |
| Phase 2: Opinionated default for new projects | Ensure the setting is readable and writable via REST and GraphQL API. Update documentation. Change the default to **enabled** for newly created projects. | **Open** - API access and new-default items carried forward into Phase 4 below |
| Phase 3: Migration path | Provide clear migration guidance for projects currently relying on branch pipelines. Update documentation and templates. Eventually deprecate the old workflow in a future major version. | **Open** - migration guidance carried forward into Phase 4 below |
## What "done" means for this epic
Two related but distinct guarantees are in scope, and both are now shipped in BETA:
1. **Suppression** - once an MR is open, a `git push` to its source branch does not create a new branch pipeline.
2. **Non-authority** - regardless of how a branch pipeline came to exist (first-push race, manual/API/scheduled trigger), its result never counts toward whether the MR can merge. Only the MR pipeline's result decides mergeability once an MR is open.
The success criterion is **not** "exactly one pipeline is ever created per push" — that's unachievable in the first-push race and in the manual/API-triggered case (see Visual 1). The actual guarantee is: **exactly one pipeline's result decides the merge, once an MR is open.**
## Implementation
A new opt-in project setting, `skip_branch_pipelines_for_mrs`. This ships as two complementary mechanisms:
1. **Suppression (going forward only)** - When enabled, a `git push` to a branch that is already the source branch of an open (or locked) MR skips creating the branch pipeline entirely.
2. **Non-authority (covers what suppression can't)** - Suppression can only apply to pushes that happen *after* an MR exists; it cannot retroactively remove a branch pipeline created before the MR existed (first-push race) or one created by a manual/API/scheduled trigger. This second mechanism ensures a branch pipeline's result is never counted toward the merge decision, regardless of how or when it was created.
### Design Decisions (suppression mechanism)
| Decision | Choice | Rationale |
|----------|--------|-----------|
| Suppression point | `BaseHooksService#create_pipeline?` | Earliest, cheapest point. No CI config parsing, no Sidekiq job. |
| Pipeline sources affected | `push` only | Manual/API/scheduled/triggered pipelines unaffected. This method is only called in the push hook path. |
| MR existence check | Reuse existing `params[:merge_request_branches]`, scoped to open **or locked** MRs | Already computed by `PushedBranchesService` and plumbed through `ProcessRefChangesService` → `BranchPushService` → `BranchHooksService`. No additional DB queries. Locked MRs (mid-merge) are included so we don't create unnecessary branch pipelines for MRs about to merge. |
| Default | `false` (opt-in) | Backward compatible. Existing projects keep current behavior. |
### Call Chain
```
git push
→ PostReceive
→ Git::ProcessRefChangesService#execute
→ Git::BranchPushService.new(params: { merge_request_branches: ...})
#execute_related_hooks
→ Git::BranchHooksService.new(project, user, params)
→ Git::BaseHooksService#create_pipeline
→ create_pipeline? ← skip_pipeline_for_merge_request?
→ Ci::CreatePipelineService#execute_async(:push) ← skipped when suppressed
```
### Non-authority mechanism
When the setting is **ON** and "Pipelines must succeed" is enabled, only MR pipelines are considered as the head pipeline for mergeability. Branch pipelines - including any created on the first push before the MR existed - are ignored:
| CI config | Head pipeline | Mergeable? |
|---|---|---|
| No `workflow:rules`, or `workflow:rules` matching both contexts | MR pipeline (succeeds) | Yes |
| `workflow:rules` matching only MR pipelines | MR pipeline (succeeds) | Yes |
| `workflow:rules` matching only branch pipelines | None (branch pipeline ignored) | No (`ci_must_pass`) |
## Visual 1: Which pipeline's result decides whether the MR can merge, by scenario
| Scenario | Setting enabled? | Pipeline(s) created on this push | Which pipeline's pass/fail decides the merge |
|---|---|---|---|
| Push to branch, no MR exists yet | n/a | Branch pipeline only | - |
| MR opened on a commit that already has a branch pipeline (no new push) | On | Branch pipeline (pre-existing) + new MR pipeline on same commit | The MR pipeline - the branch pipeline's result is not counted |
| Push to branch, MR already exists | Off (today's default) | Branch pipeline + MR pipeline | Ambiguous - either one might end up deciding it (the split-brain problem this epic exists to fix) |
| Push to branch, MR already exists | On | MR pipeline only (branch pipeline isn't created) | The MR pipeline |
| Manual/API/scheduled pipeline triggered on the branch while MR is open | On | Branch pipeline still gets created (suppression only applies to the `git push` path) | The MR pipeline - the branch pipeline's result still isn't counted, however it was created |
## Visual 2: The first-push race - push happens, then MR opened on the same commit
```mermaid
sequenceDiagram
participant Dev
participant Git
participant CI as Pipeline Creation
participant MR as Merge Request
Dev->>Git: git push (commit A) - no MR yet
Git->>CI: create pipeline (push event)
CI-->>Git: Branch Pipeline #1 (commit A)
Note over CI: Only pipeline that exists<br/>no MR to check, so no merge decision applies
Dev->>MR: Open MR from branch (still commit A)
MR->>CI: create pipeline (merge request opened)
CI-->>MR: MR Pipeline #2 (commit A)
Note over CI,MR: Setting enabled - MR Pipeline #2's result is what<br/>decides the merge. Branch Pipeline #1 is not cancelled,<br/>it keeps running and its result still exists, it's just<br/>no longer counted toward whether the MR can merge.
Dev->>Git: git push (commit B) - MR now exists
Git->>CI: create pipeline (push event)
Note over CI: Setting enabled - no branch pipeline is created<br/>for this push. Only an MR pipeline is created for commit B.
CI-->>MR: MR Pipeline #3 (commit B)
Note over MR: MR Pipeline #3's result decides the merge
```
## Known edge case: required jobs kept branch-only
If a security-required job is intentionally set up to run only in the branch pipeline (e.g., to avoid re-running expensive/long jobs on MR events with no code change), and the branch pipeline's result no longer counts toward the merge decision, that job silently stops blocking bad merges - the same failure mode this epic exists to fix, reintroduced through a different door.
This is not a gap in the fix itself. It's a pattern we should document against rather than design around: required jobs need to run in the pipeline that governs merge (the MR pipeline, under this setting). Cost/duration concerns should be handled with `rules:`/`changes:` conditions inside a single pipeline, not by splitting required checks across two pipelines. Documentation should call this out explicitly, alongside closing gitlab#385841 as won't-do once this is published.
## Research Questions (carried over, still open)
1. **Why do customers choose branch pipelines over merge request pipelines today?**
* Performance differences?
* Feature limitations in MR pipelines?
* Configuration complexity?
2. **How does this impact existing integrations and downstream pipeline triggers?**
## Status
BETA. Phase 1 (opt-in setting, suppression + non-authority) is merged and released:
* Add `skip_branch_pipelines_for_mrs` project setting (gitlab!241086 - merged)
* Expose skipping branch pipelines project setting (gitlab!241641 - merged)
* Ignore branch pipelines for MRs when skip setting is on, covering the first-push race (gitlab!244815 - merged)
* Include locked MRs in skip branch pipelines logic (gitlab!243637 - merged)
This epic stays open to track the remaining phases below rather than being closed and re-opened as a new epic - the remaining work is a continuation of the same problem, following the same playbook used for the pipeline-variables deprecation.
## BETA → GA Readiness
Not yet defined.
**GitLab's formal Beta → GA requirements.** Per [Support for features in different stages of development](https://docs.gitlab.com/policy/development_stages_support/), moving from Beta to GA requires: a documented, stakeholder-aligned security release process (how vulnerabilities are found, tracked, fixed, and disclosed); a documented audit logging plan (what's logged, retention, how security teams access it); confirmed tenant isolation on multi-tenant platforms (GitLab.com); and a completed security review with no unresolved S1/S2 vulnerabilities. None of these currently apply for `skip_branch_pipelines_for_mrs`. Moving to GA without them requires a documented, e-group-approved exception.
**Dependency risk: intersecting Experiment-status features.** The "required jobs kept branch-only" edge case above leans partly on `security_policy_pipeline_check` as a mitigation for PEP/SEP customers. That feature carries **Experiment** status (Ultimate tier only) - per the same policy, Experiment features "might not mature to general availability" and "could be removed at any time," with no support commitment. GA readiness for this epic should not implicitly depend on an Experiment-status feature remaining available.
Open questions to resolve before declaring GA:
* [ ] What adoption/usage target or timeframe defines BETA success?
* [ ] Who owns the security release process and audit logging plans required before GA?
* [ ] Has a security review been scheduled or completed?
* [ ] Is GA readiness decoupled from `security_policy_pipeline_check`'s Experiment status, or does it need to wait on that feature's own maturity?
## Phase 4: Post-BETA hardening (open)
Supersedes the original Phase 2 and Phase 3 above, following the same playbook used for the pipeline-variables deprecation:
* Make the setting available via the REST API *(from Phase 2)*
* Group-level setting/enforcement and/or a [migration for existing projects](https://docs.gitlab.com/ci/variables/#enable-pipeline-variable-restriction-for-multiple-projects) *(from Phase 3)*
* A [blog post](https://about.gitlab.com/blog/migrate-from-pipeline-variables-to-pipeline-inputs-for-better-security/) to evangelize the change *(new)*
* Make the setting the new default for newly created projects (reconcile with [Backend: Make the workflow to avoid duplicate p... (gitlab#300146 - closed)](https://gitlab.com/gitlab-org/gitlab/-/work_items/300146), which predates and isn't yet aligned with this work) *(from Phase 2)*
Not yet addressed: Phase 3's "eventually deprecate the old workflow in a future major version." Not on Manu's list - worth a call on whether it's still in scope for this epic or intentionally dropped.
## Success Criteria
* [x] Exactly one pipeline's result decides the merge, once an MR is open (not "exactly one pipeline is ever created per push" - see Visual 1 for the accepted exceptions)
* [x] Security policies can reliably target the pipeline that governs merge
* [ ] Setting available via API
* [ ] Clear migration path for existing projects (group-level setting/migration)
* [ ] New default for newly created projects
* [ ] No regression in pipeline functionality
* [ ] Improved resource utilization
---
_This issue is part of the broader effort to eliminate unexpected duplicate pipelines and provide a more predictable CI/CD experience._
epic