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.

  • SubmoduleCommit gains trailers, extended_trailers, web_url, project_id and last_pipeline.
  • TreeNode gains last_commit, and ListTreeOptions gains with_last_commit (GitLab 19.3).
  • JobTokenAccessSettings gains outbound_enabled.
  • PipelineVariable gains raw.
  • ImportStatus gains created_at, failed_relations and stats. Its CreateAt, tagged create_at, never decoded and is deprecated.
  • GroupIssueBoard gains hide_backlog_list, hide_closed_list, assignee and weight, and BoardList gains limit_metric.
  • UpdateGroupIssueBoardOptions gains hide_backlog_list and hide_closed_list.
  • CreateGroupIssueBoardListOptions gains assignee_id, milestone_id and iteration_id, and label_id becomes omitempty.
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.

  1. feat(repository_submodules): add five commit keys SubmoduleCommit misses. PUT /projects/:id/repository/submodules/:submodule presents the new commit with CommitDetail (submodules.rb), which adds status, project_id and last_pipeline (commit_detail.rb) to the trailers, extended_trailers and web_url of Commit (commit.rb), all with no condition. ExtendedTrailerValues is a map[string][]string, the shape Gitlab::Git::Commit#parse_commit_trailers builds. It takes the name !3082 gives the same key on Commit, where ExtendedTrailers is already the map of strings, so extended_trailers has one name and one type on both structs; if the review of !3082 settles on another name, I will follow it here. stats is not added: CommitDetail sends it only with include_stats, which this route never passes.
  2. feat(repositories): add with_last_commit and a tree entry's last_commit. GitLab 19.3 added with_last_commit to GET /projects/:id/repository/tree (repositories.rb), and the tree object then exposes last_commit, rendered as a Commit (tree_object.rb). TreeNode.LastCommit is a *Commit, and it decodes with Commit as it is today, without waiting for !3082, because that commit never carries a trailer: Gitaly builds each entry's last commit with catfile.GetCommit (last_commit.go), which parses no trailers, unlike GetCommitWithTrailers beside it (commit.go), and GitLab builds trailers and extended_trailers from the trailers Gitaly returns (commit_service.rb, commit.rb), so both arrive empty.
  3. feat(job_token_scope): add outbound_enabled to JobTokenAccessSettings. The entity GET /projects/:id/job_token_scope presents exposes inbound_enabled and outbound_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.
  4. feat(pipelines): add raw to PipelineVariable. Ci::Variable exposes raw when the variable responds to raw? (variable.rb). Ci::PipelineVariable and Ci::PipelineScheduleVariable both include Ci::RawVariable and both tables have a non-null raw column, 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.
  5. fix(project_import_export): decode created_at, failed_relations, stats. ProjectImportStatus inherits created_at from ProjectIdentity (project_identity.rb) and exposes failed_relations and stats (project_import_status.rb). ImportStatus tagged its timestamp create_at, so it never decoded; CreatedAt is added and CreateAt deprecated, so no existing field changes what it decodes. ProjectImportFailedRelation leaves out exception_message, which the entity renders from a block returning nil for every relation, and ProjectImportStats holds the fetched and imported counts a GitHub import reports (nil for any other import, for which GitLab sends null).
  6. feat(boards): add the keys GitLab sends on a group board and a list. Board exposes hide_backlog_list and hide_closed_list on every board (board.rb), and its Enterprise module assignee and weight where the group has configurable boards (ee board.rb). IssueBoard already models the four and GroupIssueBoard modelled none. Weight stays an int64 as on IssueBoard. GitLab counts a null or -1 weight as no weight scope (board model) and sends the value it stored, so null reads 0, like a board scoped to weight 0 (create_service.rb treats 0 as a real scope), and its comment says so. List exposes limit_metric beside the two limits BoardList models (ee list.rb).
  7. feat(group_boards): add hide_backlog_list and hide_closed_list to update. update_params_ce declares both for the project and the group board update alike (boards_responses.rb). !2780 (merged) added them to UpdateIssueBoardOptions only.
  8. 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_of counts a key as given whatever its value, so the three new fields only work if an unset label_id stays out of the body, which is why LabelID gains omitempty here 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:

  • ImportStatus stops being comparable with ==, because it gains the FailedRelations slice; gorelease -base=v3.16.1 reports this as the only incompatible change. As with the structs !3063 makes non-comparable, I kept the field.
  • ImportStatus.CreateAt keeps its tag and is marked deprecated in favor of CreatedAt. It never decoded anything GitLab sends, so it keeps reading nil; removing it is a 4.0 item I can add to #2301.
  • Marshaling one of these structs now writes the new keys.
  • CreateGroupIssueBoardListOptions no longer sends "label_id": null when LabelID is unset. !3063 makes the same change to that line in fix: 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_UpdateSubmodule answers with the five keys, extended_trailers holding two values for one trailer.
  • TestRepositoriesService_ListTree_WithLastCommit asserts with_last_commit=true in the query, the decoded last_commit, and an entry without one.
  • TestGetProjectTokenAccessSettings answers with outbound_enabled.
  • TestGetPipelineVariables_Raw, and TestPipelineSchedules_CreatePipelineScheduleVariable answering with raw.
  • TestProjectImportExportService_ImportStatus_CreatedAtFailedRelationsAndStats decodes a GitHub import shaped as the API page's example and a project import whose stats is null.
  • TestGroupIssueBoardsService_GetGroupIssueBoard_ListSwitchesScopeAndLimits decodes a board with both switches, the scope and a list's limit_metric. TestGroupIssueBoardsService_UpdateIssueBoard's fixture, copied from the API page, already carried assignee and weight: 4, which the struct dropped; it now expects them.
  • TestGroupIssueBoardsService_UpdateIssueBoard_ListSwitches and TestGroupIssueBoardsService_CreateGroupIssueBoardList_EachListType assert the request body with testBodyJSON.

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 ./..., and go vet and go test on the root package, and go test work -race (make test) and go test ./... with GOWORK=off pass.
  • With Go 1.26.8, since tests:unit also runs on the golang:1.26 image, go vet and go test -race pass on the root package.
  • golangci-lint run with golangci-lint 2.14.0, the version .tool-versions pins, over both modules reports 0 issues, and gofumpt -l 0.8.0 reports nothing.
  • Merged with !3082 the branch has no conflict, and the merge passes go vet and go test -race on the root package. Applied onto !3063's branch, it conflicts only on the label_id line, and the result passes go vet and go test -race on the root package.

Related to #2300

Merge request reports

Loading
Loading