Add closing_merge_requests endpoint to WorkItem REST API

What does this MR do and why?

Adds a paginated closing_merge_requests sub-endpoint for the WorkItem development widget, exposing the merge requests that close a work item when merged — matching the GraphQL development widget (Types::WorkItems::Widgets::DevelopmentType#closing_merge_requests).

Part of implementing the full development widget for the WorkItem REST API (#601071 (closed)). Collection fields live on dedicated paginated sub-endpoints (like award_emoji) rather than inline on the widget entity.

Changes

  • GET /namespaces/:id/-/work_items/:iid/closing_merge_requests (+ project and group scopes).
  • New ClosingMergeRequest entity exposing id, from_mr_description, and the merge_request (via MergeRequestBasic).
  • Visibility is filtered in SQL through MergeRequestsFinder, deliberately not scoped to the work item's project so cross-project closing merge requests are preserved. Pagination therefore runs on an already-authorized relation and X-Total stays accurate. GraphQL reaches the same result per row via the read_merge_request_closing_issue policy (can_read_issue & can_read_merge_request); a comment in render_closing_merge_requests_for records that divergence so the two implementations stay connected.

Set up and validate locally

Prerequisites: a running GDK with the work_item_rest_api feature flag enabled:

# rails console
Feature.enable(:work_item_rest_api)

Seed data — link a merge request to a work item as a closing MR (or reuse an issue that already has one):

project   = Project.find_by_full_path('your-group/your-project')
work_item = project.issues.find_by(iid: <WORK_ITEM_IID>)
mr        = project.merge_requests.find_by(iid: <MR_IID>)
MergeRequestsClosingIssues.create!(issue: work_item, merge_request: mr,
                                   link_type: :closes, from_mr_description: true)

Create a personal access token with the api scope and export TOKEN=<token>.

Call the endpoint (project, namespace, and group scopes):

# project scope
curl --header "PRIVATE-TOKEN: $TOKEN" \
  "https://gdk.test:3000/api/v4/projects/<PROJECT_ID>/-/work_items/<IID>/closing_merge_requests"

# namespace scope (URL-encode the full path)
curl --header "PRIVATE-TOKEN: $TOKEN" \
  "https://gdk.test:3000/api/v4/namespaces/<group%2Fproject>/-/work_items/<IID>/closing_merge_requests"

# group scope — a group-level work item has no closing MRs, so returns []
curl --header "PRIVATE-TOKEN: $TOKEN" \
  "https://gdk.test:3000/api/v4/groups/<GROUP_ID>/-/work_items/<IID>/closing_merge_requests"

Each row exposes id, from_mr_description, and the nested merge_request (MergeRequestBasic). Pagination headers (X-Total, X-Next-Page, …) reflect only the merge requests the caller can read. Edge cases: flag disabled → 403; unknown work item → 404.

Screenshots

https://gdk.test:3000/api/v4/projects/47/-/work_items/1/closing_merge_requests Project-scope response — two closing MRs, both from_mr_description values, nested MergeRequestBasic:

Screenshot_2026-07-30_at_4.26.31_PM

https://gdk.test:3000/api/v4/projects/47/-/work_items/1/closing_merge_requests?per_page=1 per_page=1 — one row returned:

Screenshot_2026-07-30_at_4.23.57_PM

Edited by Daniyal Arshad

Merge request reports

Loading
Loading