fix(feature_flags): omit unset gates from SetFeatureFlagOptions

What does this MR do?

This is the first of the small merge requests that carry the content of !3063 (closed) again, one topic each, now that it is closed (#2300 (closed) was its plan). Like the rest of that work, it comes from maintaining gitlab-mcp-server, an MCP server built on this library, whose feature flag tool and end-to-end suite build this request by hand today because of the problem below.

SetFeatureFlagOptions tags its eight optional fields (Key, FeatureGroup, User, Group, Namespace, Project, Repository and Force) without omitempty (feature_flags.go lines 115-125), so SetFeatureFlag sends all eight on every call, the ones the caller left unset as "" and false. The create or update a feature flag route declares key mutually exclusive with each gate beside it, and accepts only percentage_of_actors and percentage_of_time as its value (features.rb lines 57-105 at v19.5.0-ee). Grape counts a parameter that is present as given, whatever its value, so GitLab refuses every call with 400, the plainest one included: enabling a flag for the whole instance with &gitlab.SetFeatureFlagOptions{Value: true} is answered, on GitLab 19.5.0, with

400 {error: key does not have a valid value, key, feature_group are mutually exclusive, key, user are mutually exclusive, key, group are mutually exclusive, key, namespace are mutually exclusive, key, project are mutually exclusive, key, repository are mutually exclusive}

This MR adds omitempty to both halves of the tag of those eight fields. Value keeps its tags: GitLab requires it, and false is a value.

Is this a breaking change?

No. No exported name, type or signature changes; pointer fields, the usual form of an optional option here, would have changed the type of eight exported fields. What changes is the request body: a field the caller leaves at its zero value is no longer sent, and every field the caller sets is sent as before, which a unit test pins for value, key and force and an integration test for user. A request carrying the empty gates was refused as above, so no call that GitLab accepted sends anything different now. For force, false and leaving it out mean the same to GitLab.

How was this tested?

  • Two unit tests in feature_flags_test.go. TestSetFeatureFlagOmitsUnsetGates checks that a boolean gate with no other option sends value alone, and TestSetFeatureFlagSendsTheOptionsItIsGiven that value, key and force, set by the caller, are sent and nothing else is. On main both fail, the body carrying every field the caller left unset, as "" or false, beside what the caller set; with the fix both pass. The existing TestSetFeatureFlag passes before and after.
  • Three integration tests in gitlab_test/feature_flags_integration_test.go set a flag on a live instance the way a caller would: a boolean gate for the whole instance, enabled and then disabled; a percentage of actors, which sets key and no gate; and a gate for one user, which sets a gate and no key. Each uses a flag of its own and deletes it when it finishes. I ran them with go test ./gitlab_test/... -race -tags=integration -run '^Test_Features' against the gitlab-ee-no-license service of docker-compose.yml, which ran GitLab 19.5.0-ee without a license: on main all three fail with the 400 above (the percentage one without its first clause, since its key is valid), and with the fix all three pass. I ran only these three because on an instance without a license the rest of the suite stops at its license check, which !3148 (merged) fixes.
  • The steps of make reviewable with the versions .tool-versions pins (Go 1.26.6, gofumpt 0.8.0, buf 1.55.1, golangci-lint 2.14.0): go mod tidy, make generate and make fmt change no file, golangci-lint reports 0 issues, buf format --exit-code and buf lint pass, go vet is clean and go test -race work passes. go test -race work passes on Go 1.27.3 as well. With the integration build tag, go vet is clean, gofumpt lists nothing, and golangci-lint reports nothing in the new file.
Test output without and with the fix

Unit tests on main at f9fef569 with only the new tests added (shortened):

--- FAIL: TestSetFeatureFlagOmitsUnsetGates (0.00s)
        Error:          Not equal:
                        expected: map[string]interface {}{"value":false}
                        actual  : map[string]interface {}{"feature_group":"", "force":false, "group":"", "key":"", "namespace":"", "project":"", "repository":"", "user":"", "value":false}
--- FAIL: TestSetFeatureFlagSendsTheOptionsItIsGiven (0.00s)
        Error:          Not equal:
                        expected: map[string]interface {}{"force":true, "key":"percentage_of_actors", "value":30}
                        actual  : map[string]interface {}{"feature_group":"", "force":true, "group":"", "key":"percentage_of_actors", "namespace":"", "project":"", "repository":"", "user":"", "value":30}
--- PASS: TestSetFeatureFlag (0.00s)

Integration tests on the same tree against GitLab 19.5.0-ee (flag names shortened):

--- FAIL: Test_FeaturesSetFeatureFlag_Boolean_Integration (0.47s)
    POST .../api/v4/features/client_go_integration_...: 400 {error: key does not have a valid value, key, feature_group are mutually exclusive, ...}
--- FAIL: Test_FeaturesSetFeatureFlag_PercentageOfActors_Integration (0.72s)
    POST .../api/v4/features/client_go_integration_...: 400 {error: key, feature_group are mutually exclusive, key, user are mutually exclusive, ...}
--- FAIL: Test_FeaturesSetFeatureFlag_User_Integration (2.88s)
    POST .../api/v4/features/client_go_integration_...: 400 {error: key does not have a valid value, key, feature_group are mutually exclusive, ...}

With the fix:

--- PASS: TestSetFeatureFlagSendsTheOptionsItIsGiven (0.00s)
--- PASS: TestSetFeatureFlag (0.00s)
--- PASS: TestSetFeatureFlagOmitsUnsetGates (0.00s)
ok      gitlab.com/gitlab-org/api/client-go/v3  1.032s

--- PASS: Test_FeaturesSetFeatureFlag_PercentageOfActors_Integration (0.69s)
--- PASS: Test_FeaturesSetFeatureFlag_Boolean_Integration (1.54s)
--- PASS: Test_FeaturesSetFeatureFlag_User_Integration (1.66s)
ok      gitlab.com/gitlab-org/api/client-go/v3/gitlab_test      2.700s

gitlab-mcp-server keeps a record of what it finds and contributes in the projects it depends on, and in sibling projects, in upstream-bugs.md.

Related to #2300 (closed)

Merge request reports

Loading
Loading