Accept MR API (PUT /merge) bypasses merge trains when pipeline already succeeded
<!--IssueSummary start-->
<details>
<summary>
Everyone can contribute. [Help move this issue forward](https://handbook.gitlab.com/handbook/marketing/developer-relations/contributor-success/community-contributors-workflows/#contributor-links) while earning points, leveling up and collecting rewards.
</summary>
- [Close this issue](https://contributors.gitlab.com/manage-issue?action=close&projectId=278964&issueIid=593465)
</details>
<!--IssueSummary end-->
:robot: AI-generated
## Summary
The "Merge a merge request" REST API (`PUT /projects/:id/merge_requests/:iid/merge`) allows bypassing merge trains on projects that have merge trains enabled. When `auto_merge=true` is passed and the MR pipeline has already succeeded, the API merges the MR **directly into the target branch** without adding it to the merge train. No merge-train pipeline runs to verify compatibility with the current HEAD.
This effectively circumvents the merge train safety mechanism that projects rely on to ensure sequential verification of changes.
## Steps to reproduce
1. Have a project with **merge trains enabled** and **pipelines must succeed** checked
2. Create an MR, let the pipeline pass (status: `success`)
3. Approve the MR
4. Call the Accept MR API:
```
PUT /projects/:id/merge_requests/:iid/merge?auto_merge=true&squash=true
```
5. The MR is merged **immediately and directly** — no merge-train pipeline is created
## Root cause
In `lib/api/merge_requests.rb`, the `execute_merge_with_fix` helper (behind the `fix_merge_api_mergeability_check` feature flag) has this logic:
```ruby
def execute_merge_with_fix(merge_request, auto_merge)
if auto_merge
strategy_available = AutoMergeService.new(...)
.available_strategies(merge_request)
.include?(merge_request.default_auto_merge_strategy)
if strategy_available
AutoMergeService.new(...).execute(merge_request, ...)
elsif merge_request.diff_head_pipeline_success?
# <-- THIS PATH: direct merge, no merge train check!
MergeRequests::MergeService.new(...).execute(merge_request)
else
not_allowed!
end
# ...
end
```
On merge-train projects, `default_auto_merge_strategy` returns `merge_when_checks_pass`, but the available strategies are the merge-train strategies (`merge_train`, `add_to_merge_train_when_checks_pass`). Since the default strategy is not in the available list, the code falls through to the `elsif` branch which calls `MergeService` directly — bypassing the merge train entirely.
The legacy code path (`execute_merge_legacy`) has the same issue: `immediately_mergeable_legacy` returns `true` when `auto_merge && pipeline_success`, leading to a direct `MergeService.execute` call.
## Expected behavior
When merge trains are enabled on a project, the Accept MR API with `auto_merge=true` should either:
- Add the MR to the merge train (delegating to the merge train strategy), or
- Return an error (e.g. 405) indicating that the merge train API should be used instead
It should **not** merge directly, as this defeats the purpose of merge trains.
## Impact
Any API client (CI bot, CLI tool, automation) calling `PUT /merge_requests/:iid/merge` with `auto_merge=true` on a merge-train project will silently bypass the merge train when the pipeline is green. The user gets a "Merged!" response with no indication that the merge train was skipped. This can introduce broken commits to the target branch — exactly what merge trains are designed to prevent.
## Workaround
Use the Merge Train API (`POST /projects/:id/merge_trains/merge_requests/:iid`) instead of the Accept MR API. Note: passing `auto_merge=true` to the merge train API also has a separate bug when checks already passed (tracked in #592889).
issue