feat(namespaces): add the eight fields the namespace entity sends to Namespace

What does this MR do?

API::Entities::Namespace, together with the Enterprise module prepended onto it, exposes eight fields the Namespace struct does not carry. None of them is unconditional: each is sent to a caller with a particular permission, or on a namespace with a subscription, which is why the table below says who gets each one rather than only naming a line.

Read from gitlab-org/gitlab at commit e5d23f6d, the tip of master when this was written:

Field Exposed at Sent
root_repository_size namespace.rb:14 on a group, to a caller allowed :admin_group
projects_count namespace.rb:18 the same condition
shared_runners_minutes_limit ee namespace.rb:14 to a caller allowed :update_subscription_limit
extra_shared_runners_minutes_limit ee namespace.rb:15 the same condition
additional_purchased_storage_size ee namespace.rb:16 the same condition
additional_purchased_storage_ends_on ee namespace.rb:17 the same condition
max_seats_used_changed_at ee namespace.rb:33 on a namespace with a gitlab_subscription, and null unless the caller may :read_billing
end_date ee namespace.rb:38 the same condition

The last two share their condition with max_seats_used and seats_in_use, which the struct already carries, so a caller who is already getting those two is getting these as well and decoding nothing.

On the types. The five numeric fields are plain int64, per review: a response struct uses primitives unless a null carries something the zero value does not. additional_purchased_storage_ends_on and end_date are dates, both declared type: 'Date' and both backed by date columns, so they are *ISOTime like the trial_ends_on beside them. max_seats_used_changed_at is a timestamp despite its type: 'Date' annotation, and the documentation page's own example shows "2025-05-15T12:00:02.000Z", so it is a *time.Time.

One of the five is worth naming rather than leaving for someone to find later: a null shared_runners_minutes_limit is how GitLab says there is no limit, which as an int64 reads as a limit of zero minutes. It is only sent to a caller allowed :update_subscription_limit, who is reading the namespace to set those limits rather than to enforce them, so the convention costs nothing here. I am recording it in case a future caller wants to enforce one.

Every field is placed where the entity exposes it and no existing field moves.

I found this while developing an MCP server on top of this library, https://github.com/jmrplens/gitlab-mcp-server, which currently reads all eight keys out of the raw response beside the SDK decode.

Is this a breaking change?

No. Eight additive fields on one response struct. Nothing is renamed, retyped, removed or moved, and namespaces.go gains a time import it did not have.

Worth stating plainly: because every one of these is permission-gated, a caller without the permission sees the same zero values as before, and the fields are not a way to obtain data the API was not already willing to send.

How was this tested?

TestGetNamespace sends all eight keys in its fixture and asserts every decoded value, including the two ISOTime dates and the time.Time timestamp.

TestListNamespaces is untouched and its fixture carries none of the eight, so the absent case is asserted too: every new pointer stays nil and ProjectsCount stays zero.

go build ./..., go test . and golangci-lint run ./... all pass locally, and gofumpt -l reports nothing on the changed files.

Related to #2300

Edited by José M. Requena Plens

Merge request reports

Loading
Loading