Add a setting to allow or prevent duplicate RubyGems
What does this MR do and why?
RubyGems duplicates are now blocked by default. This matches rubygems.org. A group can allow them with a new setting.
Behavior change
- What changes: Groups that push the same gem name and version twice will now get an errored package on the second push. Before, the new file joined the existing package.
- Why this is allowed: The RubyGems registry is an Experiment behind the
rubygem_packagesflag. Experiments are outside the breaking-change policy. - Changelog:
changed. gem pushstill returns 201: Gems are processed asynchronously, so the error shows up afterward. NuGet works the same way.
What this adds
- Two settings:
rubygems_duplicates_allowed(boolean, defaultfalse) andrubygems_duplicate_exception_regex(text, default'', limit 255) onnamespace_package_settings. - API: Both settings are in the
PackageSettingsGraphQL type, theupdateNamespacePackageSettingsmutation, and the update service allow-list. - Check:
Packages::Rubygems::ProcessGemServicefinds an existing package with the same name and version in the project. If duplicates are not allowed, it raisesDuplicatePackageErrorbefore the lease.Packages::ErrorHandlingmarks the new package as errored with "Gem '' with version '' already exists". - Exception regex:
Namespace::PackageSetting.duplicates_allowed?applies the regex to the name or the version. - Pending destruction: Packages pending destruction do not count as duplicates.
- Settings source: Settings come from the project's own namespace, same as other formats.
- Docs: New "Duplicate gems" section on the RubyGems page. It replaces "Pushing gems with the same name or version". The supported functionality table now says "No (configurable)". The development settings table and the GraphQL reference are updated.
The setting has the same shape as the Terraform module setting (c09fadea).
Not in this MR
The group settings UI toggle comes in a follow-up MR. Until then, use GraphQL.
Why these defaults
A customer reported that a released version could be silently replaced. Lockfile checksums then stopped matching. Blocking duplicates by default protects those lockfiles. See the customer feedback.
References
- Closes #323796
- Customer feedback
- Related: protection rules MR !264053 (merged)
Screenshots or screen recordings
No UI changes.
| Before | After |
|---|---|
Database
Small table (table_size: small). The migration is safe to run online.
- Constant defaults:
falseand''are constants, so the add is fast. - Lock retries: The migration uses
with_lock_retries. - Text limit: Added with
NOT VALID, thenVALIDATE. - Up and down: Both run clean.
- ClickHouse: Siphon does not replicate this table.
- Queries: None changed.
duplicates_allowed?reads the already-loaded settings row.
Migration up
main: == 20261021213417 AddRubygemsDuplicatesAllowedToNamespacePackageSettings: migrating
main: -- transaction_open?(nil)
main: -- add_column(:namespace_package_settings, :rubygems_duplicates_allowed, :boolean, {:null=>false, :default=>false, :if_not_exists=>true})
main: -- add_column(:namespace_package_settings, :rubygems_duplicate_exception_regex, :text, {:null=>false, :default=>"", :if_not_exists=>true})
main: -- transaction_open?(nil)
main: -- transaction_open?(nil)
main: -- execute("ALTER TABLE namespace_package_settings\nADD CONSTRAINT check_44831f930f\nCHECK ( char_length(rubygems_duplicate_exception_regex) <= 255 )\nNOT VALID;\n")
main: -- execute("SET statement_timeout TO 0")
main: -- execute("ALTER TABLE namespace_package_settings VALIDATE CONSTRAINT check_44831f930f;")
main: -- execute("RESET statement_timeout")
main: == 20261021213417 AddRubygemsDuplicatesAllowedToNamespacePackageSettings: migrated (0.0995s) Migration down
main: == 20261021213417 AddRubygemsDuplicatesAllowedToNamespacePackageSettings: reverting
main: -- transaction_open?(nil)
main: -- remove_column(:namespace_package_settings, :rubygems_duplicates_allowed, {:if_exists=>true})
main: -- remove_column(:namespace_package_settings, :rubygems_duplicate_exception_regex, {:if_exists=>true})
main: == 20261021213417 AddRubygemsDuplicatesAllowedToNamespacePackageSettings: reverted (0.0977s) How to set up and validate locally
- In GDK, enable
rubygem_packagesfor a project in a group. - Push
my_gem0.0.1. - Push the same file again. The second package shows the "already exists" error.
- Run this mutation:
mutation { updateNamespacePackageSettings(input: {namespacePath: "<group>", rubygemsDuplicatesAllowed: true}) { packageSettings { rubygemsDuplicatesAllowed } errors } } - Push again. The file is added to the existing package.
Tests: 920 examples pass across the service, worker, model, settings service, GraphQL mutation and type, and RubyGems API specs. Rubocop is clean. Code review found no Critical or Important issues.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.