feat(protected_branches): add member_role_id to branch access levels
Context and motivation
The Protected branches API accepts a custom role (member_role_id, Ultimate only) in allowed_to_push, allowed_to_merge and allowed_to_unprotect, and returns it in push_access_levels, merge_access_levels and unprotect_access_levels. This applies to both the project and the group endpoints:
- https://docs.gitlab.com/api/protected_branches/
- https://docs.gitlab.com/api/group_protected_branches/
The client does not model the field yet. A custom role cannot be sent, and the response drops it. A consumer then reads a role entry as a plain access level, because the API reports the base access level of the role next to it.
This is the client-go part of gitlab-org/terraform-provider-gitlab#6913.
Behavior change
| Scenario | Before | After |
|---|---|---|
Send a custom role in BranchPermissionOptions or GroupBranchPermissionOptions |
Field does not exist | Serialized as member_role_id in the allowed_to_* entry |
MemberRoleID not set |
n/a | Key is omitted from the request body |
| Read an access level that has a custom role | member_role_id is dropped |
Available in MemberRoleID of BranchAccessDescription and GroupBranchAccessDescription, 0 when the entry has no role |
Scope and solution
protected_branches.goandgroup_protected_branches.go: addMemberRoleID int64(json:"member_role_id") to the access description structs andMemberRoleID *int64(urlandjsonmember_role_id,omitempty) to the permission options structs. The types and tags followUserID,GroupIDandDeployKeyID, and the option field sits in the order of the public API docs.- The change is additive. No service interface changes, so the generated mocks under
testing/are untouched. - Out of scope:
member_role_name, which the group API returns next tomember_role_id.
Acceptance criteria
- CA-01:
member_role_idis sent inallowed_to_push,allowed_to_mergeandallowed_to_unprotectfor project and group protected branches. - CA-02: An update can add a role entry and destroy a stale entry (
idwith_destroy) in the same request. - CA-03: The key is omitted from the request body when
MemberRoleIDis not set. - CA-04:
member_role_idis decoded from project and group responses, and entries without a role keep0. - CA-05: No existing signature or interface changes.
Validation and evidence
| Check | Result | Evidence |
|---|---|---|
| New tests fail without the change (CA-01 to CA-04) | Passed | Compiling the new test files against main fails with unknown field MemberRoleID in the four structs |
| Protected branch tests with the change | Passed | go test -race -run 'ProtectedBranch|ProtectRepositoryBranches|UpdateRepositoryBranches|UnprotectRepositoryBranches' . runs 20 tests, 8 of them new (project and group) |
make reviewable |
Passed | buf, gofumpt, golangci-lint 2.14.0 (0 issues.) and go test work -race, with Go 1.26.10 |
| Request body assertions | Passed | testBodyJSON compares the decoded body exactly, so an unexpected member_role_id key fails the test |
| Against a real GitLab Ultimate instance | Not executed | The tests use mocked HTTP responses based on the API docs above |
| Pipeline of this merge request | Pending |
Risks and compatibility
- Additive change. Callers that do not set the field send the same request body as before.
- For servers that do not support
member_role_id(non-Ultimate or older versions), the behavior when a caller sets it is decided by the server. It was not tested here.
AI assistance
This change was prepared with the help of an AI coding agent (Anthropic Claude, run through OpenCode). The submitter is accountable for the contents of this merge request.