Document and deprecate cancel_merge_when_pipeline_succeeds
What does this MR do and why?
POST /projects/:id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds publishes a contract it does not honour. !255239 (closed) proposed moving the endpoint to match what is written about it; that is a breaking change, so @phikai and @marc_shaw settled on the opposite split: the correct contract arrives on a new cancel_auto_merge endpoint, and this endpoint keeps its behaviour and gets documentation that describes it honestly. !255702 (merged) was the first half and merged on 2026-09-29; this merge request is the second half, and it is now rebased onto it.
The page and the desc block said |
What the endpoint sends | |
|---|---|---|
| Success | a full merge request | {"status":"success"} |
| Cannot cancel | 406, message Can't cancel the automatic merge |
201 carrying {"message":"Can't cancel the automatic merge","status":"error","http_status":406} |
The refusal row is worth reading twice. AutoMergeService#cancel returns error("Can't cancel the automatic merge", 406), and BaseServiceUtility#error builds a plain hash. A plain hash carries no status, so Grape applies its own 201 for a POST and the service's http_status: 406 arrives inside the body instead. A refused cancellation is therefore reported with a success status code, and only the body says otherwise. That is now on the page, because a client branching on the status code gets this wrong today and nothing published warns it.
I found this while maintaining gitlab-mcp-server, an MCP server for the GitLab API, when what this endpoint sends did not match what its annotation and its page said. That project keeps a record of everything it has found and done on its dependencies and on sibling projects in upstream-bugs.md, where this endpoint and both merge requests are entry 46.
Rebased onto !255702 (merged)
!255702 (merged) merged on 2026-09-29, and this branch is rebased onto current master. The rebase did what this description said it would:
- The deprecation warning links to the new section instead of naming it in code font:
Use the [`cancel_auto_merge` endpoint](#cancel-auto-merge) instead., which is the linked form from the technical writing review. - In
doc/api/merge_requests.md, the deprecated section's closing example and itshttp_statusnote come first, and !255702 (merged)'s## Cancel auto mergesection follows them. - This branch's change to
doc/api/merge_trains.mddropped out.masteralready points that link at#cancel-auto-merge, which is the right end state, so this merge request no longer touches that file.
Renaming the topic to Cancel merge when pipeline succeeds (deprecated) still moves its anchor, and nothing on master links to the old #cancel-merge-when-pipeline-succeeds any more. Two commit messages changed so that they stay true on the new base: the first commit no longer says cancel_auto_merge has "the contract this one has always described", since it answers 409 where this one's page said 406, and the technical writing commit now says the warning links to the section. One line of content changed too: until the technical writing commit removes it, the first commit's warning said cancel_auto_merge answers 406 Not Acceptable, which on the new base sat above a section saying 409 Conflict, so it says 409 Conflict now. The final diff is the same.
Checks run after the rebase
The three documentation linters ran in the image docs-lint uses (lint-markdown:alpine-3.24-vale-3.21.0-markdownlint2-0.23.2-lychee-0.24.2-rumdl-0.2.69):
markdownlint-cli2ondoc/api/merge_requests.mdanddoc/api/merge_trains.md: 0 issues.vale --minAlertLevel errorondoc/api/merge_requests.md: 0 errors, 0 warnings and 0 suggestions, and no alert of any level in the changed section.lychee --offline --no-progress --include-fragments doc tooling/docs/api/tags: 0 errors over the whole tree, with#cancel-auto-mergeresolving from both pages that link to it.bundle exec rake gitlab:openapi:v3:check_docs: up to date.bundle exec rspec spec/requests/api/merge_requests_spec.rb, both cancel blocks: 25 examples, 0 failures, 2 pending. The pending pair is the granular token shared example skipping on a namespace with no top-level group, once in each block. After the review changes below, 24 examples, 0 failures, 2 pending, run from a restarted merge request id sequence, andrubocopreports no offenses on the file.
What changed
doc/api/merge_requests.md describes both paths as they behave: one 201 for both outcomes, the status field as the thing to read, an example response for each, and an explicit note that the http_status field is not the status of the response. The example that printed a whole merge request is gone, as is the pointer to the single merge request response notes, because no merge request is returned. The section is marked deprecated with a warning alert that links to cancel_auto_merge, following deprecate a page or topic, which is where REST API deprecations sends you for an endpoint.
The desc block in lib/api/merge_requests.rb loses success Entities::MergeRequest and gains deprecated true, because it is the third place the old claim was published: doc/api/openapi/openapi_v3.yaml is generated from it and gave the 201 an APIEntitiesMergeRequest schema. A Grape desc block is documentation metadata that nothing reads at request time, so this is documentation rather than behaviour. The regenerated OpenAPI document is committed with it, and bundle exec rake gitlab:openapi:v3:check_docs passes.
I also dropped { code: 406, message: 'Not acceptable' } from that block's failure list. not_acceptable! is never called on this route, and 406 is listed on no other endpoint in this file, so it reads as the same mistaken belief that the service's http_status became the response status. Say the word if you would rather keep it and I will put it back.
The endpoint's behaviour is untouched. That is the whole point of this merge request.
Specs that pin the published contract
Two request specs now assert the two response bodies the page documents, so a later change cannot quietly turn the refusal into a real 406 without a test saying so.
Writing them turned up something worth knowing about the specs that were already there: the before block armed the auto-merge with AutoMergeService#execute(merge_request, STRATEGY_MERGE_WHEN_CHECKS_PASS), and on this fixture that call was a no-op. MergeWhenChecksPassService#availability_details refuses a merge request that is already mergeable with no pipeline in progress, which is exactly what the factory builds, so auto_merge_enabled was never set and every example in the block took the refusal branch. Nothing could see it, because this endpoint answers 201 either way and the existing example asserted only the status code. The new success example arms the auto-merge with the :merge_when_checks_pass factory trait instead, which is what the rest of the file uses for this, and asserts both the body and auto_merge_enabled afterwards.
As suggested in review, the before block and that original example are now gone rather than described in a comment, because the two new contexts cover both branches. The block was doing one other thing: it created a merge request in every example, so by the time the example that posts the id in place of the iid ran, earlier examples had moved the id sequence past the iid. Without it, since RSpec runs the block's own examples before its contexts, that example is the first to run that creates a merge request, and with the block run on its own against a freshly prepared test database its id and iid are both 1, so it answers 201. It now creates a merge request in another project first, so it no longer depends on what ran before it.
Judgement calls I would rather state than bury
The first commit carries Changelog: deprecated. I had left it off at first on the grounds that this is a documentation-only change, and that was wrong: it edits lib/api/merge_requests.rb and flips deprecated: true on an OpenAPI operation, so the changelog guidelines' "any client-facing change to our REST and GraphQL APIs must have a changelog entry" applies. Danger said so on this merge request and I read it as noise. The trailer is on the first commit so it survives a squash.
I did not add an entry to doc/api/rest/deprecations.md, on the grounds that the styleguide makes it optional: "To widely announce a deprecation, update the REST API deprecations page." I had first argued that every entry there is a breaking change promising a v5 removal, and that is not true. require_password_to_approve carries no breaking-change label, and restrict_user_defined_variables names no removal at all, so an entry need not promise one. The pull mirroring entry is this merge request's exact shape: an endpoint deprecated, replaced by a new one, with its heading suffixed -deprecated. So the page would accept an entry for this; I just do not think one endpoint gaining a better-named sibling needs a wide announcement. Say the word and I will add it.
The [deprecated](...) link in the warning points at this merge request, the one that performs the deprecation. The first commit had it citing !255239 (closed); that merge request has since been closed without merging, so it deprecates nothing and a reader following the link would land on a withdrawn proposal. This is what the rest of the page already does: the wip filter parameter and reference each cite the merge request that deprecated them.
The old page said 201 meant "Success, or the merge request has already merged". I did not carry that forward: the condition the code actually tests is auto_merge_enabled?, and I could not confirm from the source what that flag holds on an already-merged merge request. The new wording says the merge request is not set to auto-merge, which is exactly what the branch tests.
References
- !255239 (closed), where this split was proposed and agreed.
- The replacement endpoint: !255702 (merged), merged for 19.5.
- Where this came from: gitlab-mcp-server, and its record of this endpoint in upstream-bugs.md.
How to set up and validate locally
- Set a merge request to auto-merge, then call the endpoint and observe
201with{"status":"success"}. - Call it a second time, with no auto-merge set, and observe
201with{"message":"Can't cancel the automatic merge","status":"error","http_status":406}rather than a406response. - Compare both against the rewritten section of
doc/api/merge_requests.md, and follow its warning to thecancel_auto_mergesection below it.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.