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_packages flag. Experiments are outside the breaking-change policy.
  • Changelog: changed.
  • gem push still 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, default false) and rubygems_duplicate_exception_regex (text, default '', limit 255) on namespace_package_settings.
  • API: Both settings are in the PackageSettings GraphQL type, the updateNamespacePackageSettings mutation, and the update service allow-list.
  • Check: Packages::Rubygems::ProcessGemService finds an existing package with the same name and version in the project. If duplicates are not allowed, it raises DuplicatePackageError before the lease. Packages::ErrorHandling marks 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

Screenshots or screen recordings

No UI changes.

Before After

Database

Small table (table_size: small). The migration is safe to run online.

  • Constant defaults: false and '' are constants, so the add is fast.
  • Lock retries: The migration uses with_lock_retries.
  • Text limit: Added with NOT VALID, then VALIDATE.
  • 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

  1. In GDK, enable rubygem_packages for a project in a group.
  2. Push my_gem 0.0.1.
  3. Push the same file again. The second package shows the "already exists" error.
  4. Run this mutation:
    mutation {
      updateNamespacePackageSettings(input: {namespacePath: "<group>", rubygemsDuplicatesAllowed: true}) {
        packageSettings { rubygemsDuplicatesAllowed }
        errors
      }
    }
  5. 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.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading