feat: add the fields GitLab sends and accepts that ten structs miss
What does this MR do?
I maintain gitlab-mcp-server, an MCP server built on this library. This adds the response fields and request parameters it needs that the library does not model, all recorded after !3063 opened and so not in it: eight commits, one per struct or per change, each with its own tests and, in its message, the GitLab source it rests on. Every response field is exposed by its GitLab entity on master and every parameter declared by its route there, and every one appears in the record the server keeps of what a booted 19.4.1-ee says each entity sends and each route takes.
On grouping: #2300 settled on one merge request for what the server needed and then fields on demand, and !3063 is that one. These came after it opened, so I am sending them as one chunk of field syncs rather than growing !3063 while its review is open. None of them is a library-level change. If you would rather have them in !3063, they apply to its branch with one conflict, on the label_id line both change the same way, and I will move them there.
SubmoduleCommitgainstrailers,extended_trailers,web_url,project_idandlast_pipeline.TreeNodegainslast_commit, andListTreeOptionsgainswith_last_commit(GitLab 19.3).JobTokenAccessSettingsgainsoutbound_enabled.PipelineVariablegainsraw.ImportStatusgainscreated_at,failed_relationsandstats. ItsCreateAt, taggedcreate_at, never decoded and is deprecated.GroupIssueBoardgainshide_backlog_list,hide_closed_list,assigneeandweight, andBoardListgainslimit_metric.UpdateGroupIssueBoardOptionsgainshide_backlog_listandhide_closed_list.CreateGroupIssueBoardListOptionsgainsassignee_id,milestone_idanditeration_id, andlabel_idbecomesomitempty.
The eight commits, with the GitLab source each rests on
Links are to GitLab's master at 7f4e1ed0 and, in commit 2, to Gitaly's master at 4a0247f4.
- feat(repository_submodules): add five commit keys SubmoduleCommit misses.
PUT /projects/:id/repository/submodules/:submodulepresents the new commit withCommitDetail(submodules.rb), which addsstatus,project_idandlast_pipeline(commit_detail.rb) to thetrailers,extended_trailersandweb_urlofCommit(commit.rb), all with no condition.ExtendedTrailerValuesis amap[string][]string, the shapeGitlab::Git::Commit#parse_commit_trailersbuilds. It takes the name !3082 gives the same key onCommit, whereExtendedTrailersis already the map of strings, soextended_trailershas one name and one type on both structs; if the review of !3082 settles on another name, I will follow it here.statsis not added:CommitDetailsends it only withinclude_stats, which this route never passes. - feat(repositories): add with_last_commit and a tree entry's last_commit. GitLab 19.3 added
with_last_committoGET /projects/:id/repository/tree(repositories.rb), and the tree object then exposeslast_commit, rendered as aCommit(tree_object.rb).TreeNode.LastCommitis a*Commit, and it decodes withCommitas it is today, without waiting for !3082, because that commit never carries a trailer: Gitaly builds each entry's last commit withcatfile.GetCommit(last_commit.go), which parses no trailers, unlikeGetCommitWithTrailersbeside it (commit.go), and GitLab buildstrailersandextended_trailersfrom the trailers Gitaly returns (commit_service.rb, commit.rb), so both arrive empty. - feat(job_token_scope): add outbound_enabled to JobTokenAccessSettings. The entity
GET /projects/:id/job_token_scopepresents exposesinbound_enabledandoutbound_enabled(project_job_token_scope.rb), and the API page documents both, the second as deprecated for removal in 18.0. GitLab still sends it. - feat(pipelines): add raw to PipelineVariable.
Ci::Variableexposesrawwhen the variable responds toraw?(variable.rb).Ci::PipelineVariableandCi::PipelineScheduleVariableboth includeCi::RawVariableand both tables have a non-nullrawcolumn, so the pipeline variables route, the three schedule variable writes and a schedule's variables all send it. The entity's other conditional keys wait on methods neither model has. - fix(project_import_export): decode created_at, failed_relations, stats.
ProjectImportStatusinheritscreated_atfromProjectIdentity(project_identity.rb) and exposesfailed_relationsandstats(project_import_status.rb).ImportStatustagged its timestampcreate_at, so it never decoded;CreatedAtis added andCreateAtdeprecated, so no existing field changes what it decodes.ProjectImportFailedRelationleaves outexception_message, which the entity renders from a block returningnilfor every relation, andProjectImportStatsholds the fetched and imported counts a GitHub import reports (nilfor any other import, for which GitLab sendsnull). - feat(boards): add the keys GitLab sends on a group board and a list.
Boardexposeshide_backlog_listandhide_closed_liston every board (board.rb), and its Enterprise moduleassigneeandweightwhere the group has configurable boards (ee board.rb).IssueBoardalready models the four andGroupIssueBoardmodelled none.Weightstays anint64as onIssueBoard. GitLab counts anullor-1weight as no weight scope (board model) and sends the value it stored, sonullreads 0, like a board scoped to weight 0 (create_service.rb treats 0 as a real scope), and its comment says so.Listexposeslimit_metricbeside the two limitsBoardListmodels (ee list.rb). - feat(group_boards): add hide_backlog_list and hide_closed_list to update.
update_params_cedeclares both for the project and the group board update alike (boards_responses.rb). !2780 (merged) added them toUpdateIssueBoardOptionsonly. - feat(group_boards): create assignee, milestone and iteration lists. The Enterprise build takes the four list types and requires exactly one (ee boards_responses.rb). Grape's
exactly_one_ofcounts a key as given whatever its value, so the three new fields only work if an unsetlabel_idstays out of the body, which is whyLabelIDgainsomitemptyhere too.
gitlab-mcp-server keeps the record of these gaps, and of everything else it finds and contributes in the projects it depends on, in upstream-bugs.md (entries 34, 96, 98 and 101). Until a release carries them, the server reads the response fields from the raw response, and does not offer the group board list switches or the three other list types at all.
Some of these fields are missing from their own API pages, though each is documented elsewhere for the same entity: the submodule page's example shows none of the five commit keys, while the commits page shows trailers, extended_trailers, web_url and last_pipeline for the same Commit and CommitDetail entities and the search page shows project_id for CommitDetail; the pipeline variables example and the pipeline schedule variable create, retrieve, update and delete examples show no raw, which the "Retrieve a pipeline schedule" example shows for the same Ci::Variable entity; and the group boards page shows no limit_metric, which the project boards page shows for the same List entity. The import status examples also show an exception_message that GitLab always sends as null. I can send the documentation merge request to gitlab-org/gitlab if you want it alongside this one.
Is this a breaking change?
No exported name is removed or renamed and no field type or method signature changes, so no service interface or mock changes. Four things change what existing code observes:
ImportStatusstops being comparable with==, because it gains theFailedRelationsslice;gorelease -base=v3.16.1reports this as the only incompatible change. As with the structs !3063 makes non-comparable, I kept the field.ImportStatus.CreateAtkeeps its tag and is marked deprecated in favor ofCreatedAt. It never decoded anything GitLab sends, so it keeps readingnil; removing it is a 4.0 item I can add to #2301.- Marshaling one of these structs now writes the new keys.
CreateGroupIssueBoardListOptionsno longer sends"label_id": nullwhenLabelIDis unset. !3063 makes the same change to that line infix: omit unset optional params from seven more option structs, so whichever of the two merges second drops it.
How was this tested?
Tests and lint
New or extended tests, one set per commit:
TestRepositorySubmodulesService_UpdateSubmoduleanswers with the five keys,extended_trailersholding two values for one trailer.TestRepositoriesService_ListTree_WithLastCommitassertswith_last_commit=truein the query, the decodedlast_commit, and an entry without one.TestGetProjectTokenAccessSettingsanswers withoutbound_enabled.TestGetPipelineVariables_Raw, andTestPipelineSchedules_CreatePipelineScheduleVariableanswering withraw.TestProjectImportExportService_ImportStatus_CreatedAtFailedRelationsAndStatsdecodes a GitHub import shaped as the API page's example and a project import whosestatsisnull.TestGroupIssueBoardsService_GetGroupIssueBoard_ListSwitchesScopeAndLimitsdecodes a board with both switches, the scope and a list'slimit_metric.TestGroupIssueBoardsService_UpdateIssueBoard's fixture, copied from the API page, already carriedassigneeandweight: 4, which the struct dropped; it now expects them.TestGroupIssueBoardsService_UpdateIssueBoard_ListSwitchesandTestGroupIssueBoardsService_CreateGroupIssueBoardList_EachListTypeassert the request body withtestBodyJSON.
Against main, every new test fails to compile on the missing fields. With the three list fields added and LabelID still without omitempty, the create-list test fails for the assignee, milestone and iteration cases, the body carrying "label_id": null beside the one that was set.
Measured on gitlab.com: a gitlab-org group board answers with hide_backlog_list, hide_closed_list, assignee and weight (the last two null on both boards I read, which have no scope), and each of its lists with limit_metric (null on lists whose two limits are 0); a job token scope answers {"inbound_enabled":true,"outbound_enabled":false}; and a gitlab-org/gitlab tree asked for with_last_commit=true answered 20 entries whose last_commit carries "trailers": {} and "extended_trailers": {}, including entries whose commit message ends in Merged-by:, Approved-by: and Reviewed-by: lines, which is what the Gitaly code in commit 2 produces.
On the branch:
- With Go 1.27.1, each of the eight commits, in order, passes
go build ./..., andgo vetandgo teston the root package, andgo test work -race(make test) andgo test ./...withGOWORK=offpass. - With Go 1.26.8, since
tests:unitalso runs on thegolang:1.26image,go vetandgo test -racepass on the root package. golangci-lint runwith golangci-lint 2.14.0, the version.tool-versionspins, over both modules reports 0 issues, andgofumpt -l0.8.0 reports nothing.- Merged with !3082 the branch has no conflict, and the merge passes
go vetandgo test -raceon the root package. Applied onto !3063's branch, it conflicts only on thelabel_idline, and the result passesgo vetandgo test -raceon the root package.
Related to #2300