AR versions sort argument (monolith/S14 plan: 8/25)

What does this MR do and why?

This MR adds a sort argument to the Artifact Registry versions GraphQL connection, letting clients order versions by publication date or by version. It is step 8 of the 25-step monolith/S14 plan. The artifact_registry_ui feature flag is dark (default disabled).

Specifics:

  • Adds a new ArtifactRegistryVersionSort GraphQL enum with four values over the two columns the Artifact Registry contract sorts on: CREATED_AT_ASC, CREATED_AT_DESC, VERSION_ASC, VERSION_DESC. Each value carries its { sort:, order: } pair.
  • The versions connection on both ArtifactRegistryMavenPackage and ArtifactRegistryNpmPackage takes an optional sort argument (milestone 19.4, experiment). It defaults to publication date descending (CREATED_AT_DESC), matching the endpoint's own default. An explicit null falls back to that default via replace_null_with_default.
  • VersionsResolver splits the enum's sort/order pair onto the client call and sends the pair unconditionally, so the order stays pinned even if the endpoint default ever moves.
  • The two versions field descriptions now say "ordered by publication date descending by default" rather than stating an unconditional order, since the sort argument would otherwise make that false. Schema artifacts (GraphQL reference doc and introspection JSON) are regenerated.

Caveat: on a remote repository created_at is the cache-fill time, so the created_at sort orders by cache recency rather than upstream publish order. This is noted on the enum.

Testing:

  • Enum spec asserts the exact value-to-pair map.
  • Resolver spec (static only, per the GraphQL testing standard) asserts the returned connection type, the FieldCallCount budget, and the sort argument's type, default_value, and replace_null_with_default.
  • Request spec runs a table over all four enum values asserting each reaches Artifact Registry as its sort/order pair over the wire; an explicit sort: null case asserting the default pair still goes out; and a no-argument default case asserting created_at/desc and that no other sort is sent. All existing versions request stubs were updated for the now-always-sent sort/order params (WebMock exact-match query stubs).
  • RuboCop clean; GraphQL docs regenerated and in sync; frontend graphql jest suite passes.

References

Screenshots or screen recordings

N/A. Backend GraphQL schema change behind the dark artifact_registry_ui flag; no UI in this MR.

How to set up and validate locally

  1. Save the following script and run it with bundle exec rails runner validate_step8.rb:
# Validates that the versions `sort` argument maps each enum value to the client's
# sort/order pair, and that an explicit null falls back to the default pair.
require 'webmock'
require 'rspec/mocks/standalone'
include WebMock::API
WebMock.enable!
WebMock.disable_net_connect!(allow: %w[gdk.test 127.0.0.1 localhost])

failures = []
check = ->(desc, cond) { puts("#{cond ? 'PASS' : 'FAIL'}: #{desc}"); failures << desc unless cond }

ActiveRecord::Base.transaction do
  s = SecureRandom.hex(4)
  org  = FactoryBot.create(:organization, path: "arv-#{s}", name: "AR #{s}")
  user = FactoryBot.create(:user, username: "arv-#{s}", email: "arv-#{s}@example.com")
  FactoryBot.create(:organization_user, organization: org, user: user)

  base = 'http://artifact-registry.test'
  slug = 'resolved-handle'
  repo = 'maven-releases'
  pkg  = 'a1b2c3d4'
  Gitlab.config.artifact_registry['api_url'] = base
  Feature.enable(:artifact_registry_ui, org)

  # Skip the namespace-mapping round trip: return a resolved registry with a fixed slug.
  registry = ArtifactRegistry::NamespaceMapping::Registry.new(slug: slug, status: 'active')
  mapping = FactoryBot.create(:artifact_registry_namespace_mapping, organization: org)
  allow_any_instance_of(ArtifactRegistry::NamespaceMapping).to receive(:registry).and_return(registry)
  allow_any_instance_of(ArtifactRegistry::TokenExchange).to receive(:token_for).and_return('tok')

  repo_url = "#{base}/api/v1/#{slug}/repositories/#{repo}"
  pkgs_url = "#{repo_url}/maven/packages"
  vers_url = "#{pkgs_url}/#{pkg}/versions"
  jh = { 'Content-Type' => 'application/json' }

  stub_request(:get, repo_url).to_return(status: 200, headers: jh,
    body: { 'id' => 'r1', 'name' => repo, 'format' => 'maven', 'kind' => 'hosted',
            'visibility' => 'private', 'downloads_count' => 0, 'size_bytes' => 0, 'settings' => {} }.to_json)
  stub_request(:get, pkgs_url).with(query: hash_including({})).to_return(status: 200, headers: jh,
    body: [{ 'id' => pkg, 'group_id' => 'com.example', 'artifact_id' => 'core' }].to_json)
  stub_request(:get, vers_url).with(query: hash_including({})).to_return(status: 200, headers: jh, body: [].to_json)

  query = <<~GQL
    query($id: OrganizationsOrganizationID!, $name: String!, $sort: ArtifactRegistryVersionSort) {
      organization(id: $id) {
        artifactRegistryRepository(name: $name) {
          packages(first: 20) {
            nodes { ... on ArtifactRegistryMavenPackage { versions(first: 20, sort: $sort) { nodes { id } } } }
          }
        }
      }
    }
  GQL

  outbound_for = ->(sort_value) do
    WebMock::RequestRegistry.instance.reset!
    res = GitlabSchema.execute(query, context: { current_user: user },
      variables: { 'id' => org.to_global_id.to_s, 'name' => repo, 'sort' => sort_value })
    raise "errors: #{res['errors']}" if res['errors']
    sig = WebMock::RequestRegistry.instance.requested_signatures.hash.keys.find { |k| k.uri.to_s.include?('/versions') }
    q = sig ? Rack::Utils.parse_query(sig.uri.query) : {}
    [q['sort'], q['order']]
  end

  expected = {
    'CREATED_AT_ASC'  => %w[created_at asc],
    'CREATED_AT_DESC' => %w[created_at desc],
    'VERSION_ASC'     => %w[version asc],
    'VERSION_DESC'    => %w[version desc]
  }
  expected.each do |enum_value, (sort, order)|
    check.call("#{enum_value} -> sort=#{sort} order=#{order}", outbound_for.call(enum_value) == [sort, order])
  end

  check.call('no sort argument -> created_at/desc', outbound_for.call(nil) == %w[created_at desc])

  raise ActiveRecord::Rollback
end
WebMock.disable!
puts(failures.empty? ? "\nALL PASS" : "\nFAILURES: #{failures.join('; ')}")
  1. Confirm the output ends with ALL PASS (all five checks). This script was run locally and prints ALL PASS.

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Feature flag artifact_registry_ui is dark; no changelog (dark), and the enum and argument text are schema text rather than i18n. Related to #618410 (closed)

Edited by Narendran

Merge request reports

Loading
Loading