AR client version list read (monolith/S14 plan: 2/25)

What does this MR do and why?

Adds #versions to ArtifactRegistry::Client for monolith/S14 Step 2 (version-list slice). It reads the AR versions sub-collection (.../:format/packages/:package_id/versions) and returns a keyset Page of Version value objects plus both Link-header cursors, nil on a 404 (the package was removed between reads), and maps every other outcome through the S02 Client::Error hierarchy.

Rather than a bespoke read, #versions flows through the existing artifact_page reader, which is generalized here to serve any keyset list: it now takes an optional id/sub_collection (to build the nested path) and extra_query (the sort/order keys). Reusing it keeps error handling, cursor parsing, and page construction in one place shared with #packages/#images, and returns the same type-agnostic Page those methods return (no separate page class).

The Version value object exposes id, version, created_at (coerced via the shared TimeCoercion), and passes created_by/project_id/git_commit_sha through as raw opaque strings; resolving those into Rails records is a later GraphQL step, not the client's job.

This revision addresses backend review feedback:

  • The shared reader now guards a sub-collection's parent id (guard_segments!(id:) if sub_collection), so a future caller cannot silently build a .../packages//versions path; #versions relies on that single guard.
  • A versions 404 now logs the failing package_id (threaded as log_context into nil_on_missing), so per-package drift is diagnosable and not just attributable to the slug. :id was added to the ErrorReporter context allowlist so the key survives redaction.
  • A continuation-page read (a cursor-supplied 404) is pinned to resolve nil, the same not-found outcome as a first-page read; the GraphQL connection consumer guards that nil in Step 6.

References

Screenshots or screen recordings

N/A - backend client method, no UI.

Database changes

None. No migrations, models, or queries; this is an HTTP client read against the Artifact Registry service.

How to set up and validate locally

The read talks to Artifact Registry over HTTP, so the script below stubs the endpoint with WebMock and asserts the three behaviours the specs pin: the happy-path Page, the 404 -> nil outcome, and the parent-id guard. Run it with bundle exec rails runner -e test /tmp/ar_versions_validate.rb (WebMock is loaded in the test environment).

require 'webmock'
include WebMock::API
WebMock.enable!

base_url   = 'https://artifact-registry.example.test'
slug       = 'my-group'
repo       = 'maven-releases'
package_id = 'a1b2c3d4-0000-0000-0000-000000000000'
versions_url = "#{base_url}/api/v1/#{slug}/repositories/#{repo}/maven/packages/#{package_id}/versions"

body = [
  { 'id' => 'v1', 'version' => '1.10.0', 'created_at' => '2026-07-03T09:15:00Z',
    'created_by' => '101', 'project_id' => '202', 'git_commit_sha' => 'deadbeef' }
]

token_exchange = Object.new
def token_exchange.token_for(*) = 'tok'
client = ArtifactRegistry::Client.new(base_url: base_url, current_user: User.new, token_exchange: token_exchange)

# 1. Happy path: a Page of Version rows plus the Link cursor.
stub_request(:get, versions_url).with(query: hash_including({}))
  .to_return(status: 200, body: body.to_json,
             headers: { 'Content-Type' => 'application/json',
                        'Link' => %(<#{versions_url}?cursor=NEXT>; rel="next") })
page = client.versions(slug: slug, repository_name: repo, format: 'maven', package_id: package_id)
raise 'expected Page'          unless page.is_a?(ArtifactRegistry::Page)
raise 'expected 1 Version row' unless page.nodes.size == 1 && page.nodes.first.is_a?(ArtifactRegistry::Version)
raise 'attribution not raw'    unless page.nodes.first.created_by == '101'
raise 'cursor missing'         unless page.next_cursor == 'NEXT'
puts "1 OK: Page with #{page.nodes.size} Version row, next_cursor=#{page.next_cursor}"

# 2. 404 (package removed between reads) resolves nil, not an error.
stub_request(:get, versions_url).with(query: hash_including({}))
  .to_return(status: 404, body: { error: { code: 'not_found' } }.to_json,
             headers: { 'Content-Type' => 'application/json' })
raise 'expected nil on 404' unless client.versions(slug: slug, repository_name: repo, format: 'maven', package_id: package_id).nil?
puts "2 OK: 404 -> nil"

# 3. Blank package_id raises before any request (guarded in artifact_page).
begin
  client.versions(slug: slug, repository_name: repo, format: 'maven', package_id: '')
  raise 'expected ArgumentError'
rescue ArgumentError
  puts "3 OK: blank package_id -> ArgumentError"
end

WebMock.disable!
puts 'ALL OK'

Expected output:

1 OK: Page with 1 Version row, next_cursor=NEXT
2 OK: 404 -> nil
3 OK: blank package_id -> ArgumentError
ALL OK

Or run the specs directly:

bundle exec rspec ee/spec/lib/artifact_registry/client_spec.rb ee/spec/lib/artifact_registry/version_spec.rb

Suggested labels

~"type::feature" ~backend ~"group::container registry" ~"Category:Artifact Registry"

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist.

Feature flag artifact_registry_ui is dark; the client is not flag-gated. No changelog (dark), no i18n (library code).

Edited by Narendran

Merge request reports

Loading
Loading