fix(protected_packages): omit unset fields from a rule update

What does this MR do?

This is one 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). It makes the change !3150 (merged) made to SetFeatureFlagOptions, here for a package protection rule update. Like the rest of that work, it comes from maintaining gitlab-mcp-server, an MCP server built on this library, whose package protection tool, when GitLab refuses an update, tells its caller to name the pattern and the type on every one, and whose end-to-end suite sends the type with each update, because of the problem below.

UpdatePackageProtectionRulesOptions tags PackageNamePattern and PackageType without omitempty, while the two Nullable access level fields beside them carry it (protected_packages.go lines 85-90), so UpdatePackageProtectionRules sends both keys on every call, as null when the caller leaves them unset. The update a package protection rule route declares both optional (project_packages_protection_rules.rb lines 97-113 at v19.5.0-ee), and the example on that page updates the pattern alone. But the route hands the service every declared parameter the request carries, a null one included (line 118), the service assigns them to the rule (update_rule_service.rb lines 32-35), and the rule requires both to be present (rule.rb lines 35-40). So GitLab refuses every update that leaves either field out. On GitLab 19.5.0, changing only the minimum access level for push with &gitlab.UpdatePackageProtectionRulesOptions{MinimumAccessLevelForPush: gitlab.NewNullableWithValue(gitlab.ProtectionRuleAccessLevelOwner)} is answered with

422 {message: {error: [Package name pattern can't be blank, Package type can't be blank, Pattern can't be blank]}}

and changing only the pattern with 422 {message: {error: [Package type can't be blank]}}.

This MR adds omitempty to both halves of the tag of those two fields. CreatePackageProtectionRulesOptions keeps its tags: the create route requires both (lines 47-51), and GitLab refuses a create that leaves either out whether the key arrives as null or not at all.

Is this a breaking change?

No. No exported name, type or signature changes. What changes is the request body: a field the caller leaves nil is no longer sent, and a field the caller sets is sent as before, which the existing TestProtectedPackagesService_UpdatePackageProtectionRulesWithNullValues pins for all four fields. An update carrying a null pattern or type was refused as above, so no update that GitLab accepted sends anything different now. For the same reason this needs no opt-in toggle of the kind asked for on !3063 for the nil options body: no caller can rely on the body this removes, since GitLab never accepted it.

How was this tested?

  • A unit test in protected_packages_test.go, TestProtectedPackagesService_UpdatePackageProtectionRulesOmitsUnsetFields, checks that an update changing only the minimum access level for push sends that field alone. On main it fails, the body carrying package_name_pattern and package_type as null beside it; with the fix it passes. The five existing tests of the service pass before and after.
  • Two integration tests in gitlab_test/protected_packages_integration_test.go update a rule on a live instance the way a caller would, naming one field: one changes only the minimum access level for push, the other only the package name pattern. Each creates a project and an npm rule of its own, checks that the update keeps the fields it did not name, and deletes the rule when it finishes. I ran them with go test ./gitlab_test/... -race -tags=integration -run '^Test_ProtectedPackages' against the gitlab-ee-no-license service of docker-compose.yml, which ran GitLab 19.5.0-ee without a license: on main both fail with the 422 answers above, and with the fix both pass. With the fix the whole integration suite passes on that instance too: no test fails, and 16 tests or subtests skip.
  • 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.15.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 test on main at 85bf7810 with only the new tests added (shortened):

--- FAIL: TestProtectedPackagesService_UpdatePackageProtectionRulesOmitsUnsetFields (0.00s)
        Error:          Not equal:
                        expected: map[string]interface {}{"minimum_access_level_for_push":"owner"}
                        actual  : map[string]interface {}{"minimum_access_level_for_push":"owner", "package_name_pattern":interface {}(nil), "package_type":interface {}(nil)}
--- PASS: TestProtectedPackagesService_UpdatePackageProtectionRules (0.00s)
--- PASS: TestProtectedPackagesService_UpdatePackageProtectionRulesWithNullValues (0.00s)

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

--- FAIL: Test_ProtectedPackagesUpdatePackageProtectionRules_AccessLevelOnly_Integration (1.03s)
    PATCH .../api/v4/projects/16/packages/protection/rules/3: 422 {message: {error: [Package name pattern can't be blank, Package type can't be blank, Pattern can't be blank]}}
--- FAIL: Test_ProtectedPackagesUpdatePackageProtectionRules_NamePatternOnly_Integration (1.30s)
    PATCH .../api/v4/projects/17/packages/protection/rules/4: 422 {message: {error: [Package type can't be blank]}}

With the fix (shortened):

--- PASS: TestProtectedPackagesService_UpdatePackageProtectionRulesOmitsUnsetFields (0.00s)
--- PASS: TestProtectedPackagesService_UpdatePackageProtectionRulesWithNullValues (0.00s)
--- PASS: TestProtectedPackagesService_UpdatePackageProtectionRules (0.00s)
ok      gitlab.com/gitlab-org/api/client-go/v3  1.028s

--- PASS: Test_ProtectedPackagesUpdatePackageProtectionRules_NamePatternOnly_Integration (1.45s)
--- PASS: Test_ProtectedPackagesUpdatePackageProtectionRules_AccessLevelOnly_Integration (1.49s)
ok      gitlab.com/gitlab-org/api/client-go/v3/gitlab_test      2.523s

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