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