Document background transfers in the projects and groups API
What does this MR do?
Since GitLab 19.4, PUT /projects/:id/transfer and POST /groups/:id/transfer schedule the transfer and respond with the project or group as it was before the move. The user documentation says transfers are asynchronous, but the two API sections still read as if the response followed the move, and they do not say how a client learns that the move finished or failed. This MR documents both on the two sections, as master behaves since !251614 (merged), and completes the action and type lists of GET /todos, which leave out transfer_failed among others.
On each transfer section, it says:
- A successful request returns the object as it was before the move (
200 OKfor a project,201 Createdfor a group, in the sentence the API style guide places after the attributes table), with history items for the feature flag (19.0) and its removal (19.4), and on the project page one for the 19.5 change to the name and path check. - How to confirm the move: read the project or group back until its
namespace.idorparent_idis the new one. - How a failure is reported: a
transfer_failedto-do item for the user who asked, and how to list it withGET /todos.
On the to-do page, the action and type values move into footnotes below the table, and the lists gain the eleven values Todo.action_names accepts and the five types TodosFinder.todo_types accepts that the page left out. I kept the page's convention of not marking the Enterprise Edition values.
Evidence, file and line on master at 5ad13467
The transfer runs in the background, and the response shows the object before the move
- Project:
lib/api/projects.rb:1239(put ":id/transfer") callsenqueue_async_transfer(lib/api/projects.rb:333-342), which callsProjects::TransferService#schedule_async_transferand presentsprojectunchanged.app/services/projects/transfer_service.rb:40-70runsensure_allowed_transfer, moves the project namespace totransfer_scheduledand enqueuesProjects::TransferWorker.spec/requests/api/projects_spec.rb:7055expects200 OK. - Group:
lib/api/groups.rb:712(post ':id/transfer') callsenqueue_async_transfer(lib/api/groups.rb:275-285), which callsGroups::TransferService#schedule_async_transfer(app/services/groups/transfer_service.rb:36-64) and presentsgroup.spec/requests/api/groups_spec.rb:4074expects201 Created, which the page now states. - Versions: the REST routes became asynchronous behind
groups_and_projects_async_transferin 19.0 (!232368 (merged), for work item 586549;lib/api/projects.rbatv18.11.12-eehas noenqueue_async_transfer, atv19.0.0-eeit has the flag check), and the flag was removed in 19.4 by !250913 (merged) (merge4b4c25f4,VERSION19.4.0-pre). The user pages already carry the 19.4 line (doc/user/project/working_with_projects.md:492,doc/user/group/manage.md:179). They date the flag to 18.11 (:491,:178, work item 594575), when it gated only the UI (app/controllers/projects_controller.rb:147andapp/controllers/groups_controller.rb:233atv18.11.0-ee). The API pages date the REST change to 19.0, so each page is right about its own entry point. POST /groups/:id/projects/:project_id(lib/api/groups.rb:664-677) still callsProjects::TransferService#executeinline and responds after the move, so its section needs nothing.
Which checks run in the request
- Project:
ensure_allowed_transfer(app/services/projects/transfer_service.rb:148-168) refuses a project already in the namespace (line 152), a caller who cannot change the project's namespace (156; the route'sauthorize! :change_namespaceanswers403first), a caller who cannot transfer projects into the namespace (160), and, since !251614 (merged), a project with the same name or path there (164,project_with_same_name_or_path_exists?at 171-175). The route turns the error into400(lib/api/projects.rb:340). - In 19.4 the collision check was in
#transferonly (app/services/projects/transfer_service.rbatv19.4.0-ee, line 175), which only the worker runs; !251614 (merged) (merge87449438,VERSION19.5.0-pre) moved it. - Project checks that still run only in the worker, in
#transfer(app/services/projects/transfer_service.rb:178-199): a project with the same name or path pending deletion in the namespace (185, 244-250), container registry tags (191, 201-237), npm packages scoped to the old root namespace (193-196), and the save itself (save_project!, 309-313). - Group:
ensure_allowed_transfer(app/services/groups/transfer_service.rb:233-243, EE adds SAML or SCIM and the top-level group limit atee/app/services/ee/groups/transfer_service.rb:11-17) runs in the request, including the path collision (namespace_with_same_path?, 299-301). The worker runs it again and then moves the group, so a group transfer can still fail in the background:Groups::TransferService#executerescues a Service Desk address conflict (79-80, raised atapp/services/service_desk/refresh_project_key_address_slugs_service.rb:24, whichapp/services/routes/rename_descendants_service.rb:83calls when the group's path changes), validation errors (85-88) and anyTransferError(81-84).
How a failure is reported
- Both workers call
create_transfer_failure_todowhenexecutereturns false or raises after the transfer started (app/workers/projects/transfer_worker.rb:73,78,app/workers/namespaces/groups/transfer_worker.rb:71,77), which callsTodoService#transfer_failed(app/workers/concerns/namespaces/transfer_worker_helper.rb:30-31). That item has actiontransfer_failed(app/models/todo.rb:33,49),project_idset to the project orgroup_idto the group (app/services/todo_service.rb:224-229), soGET /todoswithaction=transfer_failedand the project's numeric ID asproject_idfinds a project's (the filter is anInteger,lib/api/todos.rb:56, so a URL-encoded path is refused), and its target type isNamespacefor a group. A later successful transfer by the same user resolves it (transfer_worker_helper.rb:42-46). The item first appeared in 19.3 (TRANSFER_FAILEDis inapp/models/todo.rbatv19.3.0-eeand not atv19.2.0-ee). - The reason: the worker passes
service.errortocancel_transfer!(app/workers/projects/transfer_worker.rb:74,app/workers/namespaces/groups/transfer_worker.rb:72), which stores it astransfer_last_error(app/models/concerns/namespaces/stateful/transition_callbacks.rb:75-80).Todo#transfer_failure_reasonreads it (app/models/todo.rb:322-326) andTypes::TodoTypeexposes it astransferFailureReason(app/graphql/types/todo_type.rb:71-74,doc/api/graphql/reference/_index.md:64896), both from !251614 (merged) for #597297 (closed). The REST entity has no such field:lib/api/entities/todo.rbexposesbody(line 20), which for this action is the destination path (app/models/todo.rb:365-366).
Sending the request again
- After
ensure_allowed_transfer,schedule_async_transfercallscancel_stale_transfer_state(app/services/projects/transfer_service.rb:132-146,app/services/groups/transfer_service.rb:216-230). It returns early only while a worker holds the object'sGitlab::ExclusiveLease; otherwise it cancels the scheduled state, and the request schedules the transfer again and enqueues a second worker. With the same destination, that worker fails after the first one moved the object ("Project is already in this namespace.",transfer_service.rb:152, or the group'ssame_parent?) and creates atransfer_faileditem. - While a worker holds the lease, the state machine refuses
schedule_transfer(app/models/concerns/namespaces/stateful.rb:101-103transitions only fromancestor_inheritedorarchived), and the request answers400"Unable to initiate transfer. The project may already have a transfer in progress." (app/services/projects/transfer_service.rb:49; the group's atapp/services/groups/transfer_service.rb:45).spec/requests/api/projects_spec.rb:7104andspec/requests/api/groups_spec.rb:4089hold that answer.
The to-do lists
lib/api/todos.rb:54acceptsTodo.action_namesandlib/api/todos.rb:59acceptsTodosFinder.todo_types.Todo::ACTION_NAMES(app/models/todo.rb:36-50) has 13 values, and EE adds seven (ee/app/models/ee/todo.rb:8-16). The page listed nine; it left outreview_requested,review_submitted,ssh_key_expired,ssh_key_expiring_soonandtransfer_failed, and on EEokr_checkin_requested,added_approver,duo_pro_access_granted,duo_enterprise_access_granted,duo_core_access_grantedandduo_workflow_input_required.TodosFinder::TODO_TYPES(app/finders/todos_finder.rb:28-31) has 10 types and EE adds five (ee/app/finders/ee/todos_finder.rb:7-8). The page listed ten; it left outWorkItemandKey, and on EEUser,ComplianceManagement::Projects::ComplianceViolationandAi::DuoWorkflows::Workflow.
What I left out of the pages
- After the technical writing review, the request checks that answer
400 Bad Request, what sending the request again does, and the GraphQLtransferFailureReasonfield. The evidence above still describes them. TodoServicekeeps one pending item per user, target and action unless the action is inTodo::ACTIONS_MULTIPLE_ALLOWED(app/services/todo_service.rb:399-407,app/models/todo.rb:52), so a second failure while the first item is pending adds nothing, andtransferFailureReasonis the namespace's last error, which the next scheduled transfer clears (transition_callbacks.rb:27-28). Both are true and I did not think either belongs on the reference page; I can add them if you think they do.- A second worker that starts while the first one holds the lease does nothing (
app/workers/projects/transfer_worker.rb:113-122), so a resend sometimes produces no second transfer at all. - Carrying the reason on the REST to-do entity would be an API change, not a docs one.
Overlap with open merge requests
I checked the diffs of all 555 open merge requests labeled documentation, and of the open merge requests a title search for transfer, to-do and the two API pages returns, for changes to these three pages, and again the 93 open merge requests updated after that check. Eight change doc/api/projects.md or doc/api/groups.md (!251281, !251599 (closed), !254980 (merged), !259313 (merged), !259314 (merged), !259316 (merged), !259710 (merged), !260133 (merged)), all in other sections, and none changes doc/api/todos.md.
Lint
- markdownlint-cli2 0.23.2 with the repository configuration: 0 issues on the three pages.
- Vale 3.21.0 with the repository
.vale.ini, asscripts/lint-doc.shruns it (--minAlertLevel error): 0 errors. At every level compared with master: no new alerts, and both transfer pages' reading level goes down slightly (projects 7.89 to 7.81, groups 7.45 to 7.39), and the to-do page's from 6.19 to 5.36. lychee --offline --no-progress --include-fragmentson the three pages, in the lint image thedocs-lint linksjob uses: 0 errors, including the new anchors.
Where this comes from
I maintain gitlab-mcp-server, an MCP server that exposes the GitLab API to AI assistants. Its project and group transfer tools read the object back until it moves, and look for a transfer_failed to-do item on each read that finds it unmoved, because the response alone says neither, and it took reading the routes and the workers to learn that. The project keeps a record of everything it finds in its dependencies in upstream-bugs.md, and this MR is the docs change that record proposed, adjusted to what !251614 (merged) changed.
The projects and groups pages belong to Tenant Scale: Organizations and the to-do page to Plan: Work Items. In lib/tasks/gitlab/tw/codeowners.rake the technical writers for those groups are z_painter and brendan777, so they seem the right reviewers.
Related issues
Related to #597297 (closed)
Author's checklist
- Optional. Consider taking the GitLab Technical Writing Fundamentals course.
- Follow the:
- If you're adding a new page, add the product availability details under the H1 topic title.
- If you are a GitLab team member, request a review based on:
- The documentation page's metadata.
- The associated Technical Writer.
If you are a GitLab team member and only adding documentation, do not add any of the following labels:
~"frontend"~"backend"~"type::bug"~"database"
These labels cause the MR to be added to code verification QA issues.
Reviewer's checklist
Documentation-related MRs should be reviewed by a Technical Writer for a non-blocking review, based on Documentation Guidelines and the Style Guide.
If you aren't sure which tech writer to ask, use roulette or ask in the #docs Slack channel.
- If the content requires it, ensure the information is reviewed by a subject matter expert.
- Technical writer review items:
- Ensure docs metadata is present and up-to-date.
- Ensure the appropriate labels are added to this MR.
- Ensure a release milestone is set.
- If relevant to this MR, ensure content topic type principles are in use, including:
- The headings should be something you'd do a Google search for. Instead of
Default behavior, say something likeDefault behavior when you close an issue. - The headings (other than the page title) should be active. Instead of
Configuring GDK, say something likeConfigure GDK. - Any task steps should be written as a numbered list.
- If the content still needs to be edited for topic types, you can create a follow-up issue with the docs-technical-debt label.
- The headings should be something you'd do a Google search for. Instead of
- Review by assigned maintainer, who can always request/require the reviews above. Maintainer's review can occur before or after a technical writer review.