feat(group_serviceaccounts): add PublicEmail and UnconfirmedEmail to GroupServiceAccount

What does this MR do?

It adds PublicEmail and UnconfirmedEmail to GroupServiceAccount, the two keys the group service account endpoints send that the struct did not carry.

Every group endpoint that returns a service account presents Entities::ServiceAccount: create, list, get and update, at lines 34, 71, 101 and 158 of lib/api/group_service_accounts.rb. That entity is four lines long and inherits UserSafe, which is where the two missing keys hide, so the response has six of them:

Key Exposed at Condition
id user_safe.rb#L8 always
username user_safe.rb#L9 always
public_email user_safe.rb#L10 always
name user_safe.rb#L11 always
email service_account.rb#L6 always
unconfirmed_email service_account.rb#L7 only when unconfirmed_email.present?

The documentation agrees: Retrieve a group service account lists email, id, name, public_email, unconfirmed_email and username as the response attributes, and describes unconfirmed_email as returned only when a confirmation is pending.

On placement, that documentation table is alphabetical, so it cannot be a struct order without moving the four fields that are already there. I put each new field where the entity exposes it relative to its neighbours instead, and not at the end of the struct: PublicEmail after UserName and before Email, UnconfirmedEmail after Email. The existing four keep their positions, which also keeps the diff to the two added lines. unconfirmed_email carries omitempty because the entity exposes it conditionally, matching ProjectServiceAccount and ServiceAccount; public_email is exposed unconditionally, so it is tagged like User.PublicEmail.

One detail that explains why this was easy to miss: three structs model this one entity, and only the group one disagreed. ServiceAccount for the instance scope and ProjectServiceAccount for the project scope both already carry unconfirmed_email, so the group scope was the odd one out rather than the whole concept being unmodelled.

I found the gap while developing a GitLab MCP server, https://github.com/jmrplens/gitlab-mcp-server, which reads both keys off the raw response for the group endpoints precisely because this struct drops them. That workaround retires once this lands.

Is this a breaking change?

No. These are two added fields on a response struct, with no signature change anywhere, so existing callers compile untouched and the generated mocks in testing/ are unaffected.

It is worth one sentence of detail: a program that marshals a GroupServiceAccount back to JSON will now emit public_email on every value, and unconfirmed_email whenever it is set. Values decoded before this change had both keys silently discarded, so nothing that works today can read differently, only more completely.

How was this tested?

TestUpdateServiceAccount_PublicAndUnconfirmedEmail decodes an update response carrying both new keys and asserts the whole struct with testify, so a missing or misspelled tag fails it rather than passing with a zero value. It follows the shape of the existing tests in the file, and the existing group service account tests pass unchanged.

I ran go test -race over the module (green), golangci-lint run (0 issues, gofumpt included as a formatter) and gofmt -l (clean). The full make reviewable also runs buf generate and the mock generators; since this change adds struct fields and touches no interface signature, the generated files do not move, and I confirmed testing/groups_mock.go references GroupServiceAccount only as a pointer type.

Related to #2300

Edited by José M. Requena Plens

Merge request reports

Loading
Loading