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. Onmainit fails, the body carryingpackage_name_patternandpackage_typeasnullbeside 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.goupdate 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 withgo test ./gitlab_test/... -race -tags=integration -run '^Test_ProtectedPackages'against thegitlab-ee-no-licenseservice ofdocker-compose.yml, which ran GitLab 19.5.0-ee without a license: onmainboth 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 reviewablewith the versions.tool-versionspins (Go 1.26.6, gofumpt 0.8.0, buf 1.55.1, golangci-lint 2.15.0):go mod tidy,make generateandmake fmtchange no file, golangci-lint reports 0 issues,buf format --exit-codeandbuf lintpass,go vetis clean andgo test -race workpasses.go test -race workpasses on Go 1.27.3 as well. With theintegrationbuild tag,go vetis 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.523sgitlab-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)