AR artifact element presenter seam (monolith/S14 step 4)

What does this MR do and why?

Introduces the artifact element presenter seam for monolith/S14 Step 4. An AR artifact value object (a package or an image) holds no back-reference to the repository it was read through, so a child connection mounted on the element (versions on a package, manifests on an image) cannot reach the slug, the repository name and format, or the memoized client its own read needs. ArtifactRegistry::ArtifactPresenter pairs the value object with the loaded repository and organization to close that gap, and PackagesResolver now wraps each element in it. The change is behavior-preserving: the packages connection returns the same fields as before, now through the presenter, and the package union resolves each wrapped member type.

This revision addresses backend self-review feedback:

  • The presenter's required-keyword contract (repository:, organization:) is now pinned by specs, so the guard cannot be silently autocorrected away.
  • The presenter now overrides declarative_policy_subject to point authorization at the organization, mirroring RepositoryPresenter. Without it, dropping the type-level authorization skip would raise a RuntimeError (a 500) instead of denying, because the inherited delegate resolves to the bare value object, which has no policy class. Pinned by a mirrored spec pair.
  • The presenter declares presents for all three wrapped value-object classes, which activates the DelegatorOverride static-verification lint that catches Presenter::Base accidentally clobbering a value-object reader.
  • PaginatesLists#artifact_registry_connection grew a nodes: keyword (so the resolver can pass wrapped elements while keeping the page's own cursors); both the default and the explicit-override arm now have unit coverage.
  • The resolve_type comment now states the real reason the unwrap is load-bearing: case/when dispatches on Module#===, which is implemented in C and bypasses the presenter's Ruby is_a? override.

References

Known follow-up

The images half of the seam is not yet on master: there is no ImagesResolver or images connection. This step wires the packages path; the identical wrapping must land in ImagesResolver before Step 7 mounts the manifests connection on the image type, or Step 7's resolver receives a bare ArtifactRegistry::Image and object.repository raises. This ordering is tracked on the S14 work item rather than only in this description.

Screenshots or screen recordings

N/A - backend GraphQL seam, no UI and no schema surface change.

Database changes

None. No migrations, models, or queries.

How to set up and validate locally

The presenter and the union are pure Ruby, so this runs in rails runner with no HTTP or DB fixtures. It asserts delegation, the required-attribute guard, the authorization subject, and the union's unwrap of a wrapped element. Save as /tmp/ar_presenter_validate.rb and run bundle exec rails runner -e test /tmp/ar_presenter_validate.rb.

org  = FactoryBot.build_stubbed(:organization)
repo = ArtifactRegistry::RepositoryPresenter.new(
  ArtifactRegistry::Repository.new('name' => 'maven-releases', 'format' => 'maven'),
  organization: org
)
maven = ArtifactRegistry::MavenPackage.new('id' => 'a1', 'group_id' => 'com.example', 'artifact_id' => 'core')
presenter = ArtifactRegistry::ArtifactPresenter.new(maven, repository: repo, organization: org)

# 1. Delegates the value object's readers, and carries repository + organization.
raise '1a' unless presenter.id == 'a1' && presenter.group_id == 'com.example'
raise '1b' unless presenter.repository.equal?(repo) && presenter.organization.equal?(org)
puts '1 OK: delegates readers and carries repository + organization'

# 2. Required attributes: missing either keyword raises before a broken presenter escapes.
begin
  ArtifactRegistry::ArtifactPresenter.new(maven, organization: org)
  raise '2a: expected ArgumentError'
rescue ArgumentError; end
begin
  ArtifactRegistry::ArtifactPresenter.new(maven, repository: repo)
  raise '2b: expected ArgumentError'
rescue ArgumentError; end
puts '2 OK: repository and organization are required'

# 3. Authorization points at the organization (a policied object), not the bare value object.
raise '3a' unless presenter.declarative_policy_subject.equal?(org)
begin
  DeclarativePolicy.class_for(maven)
  raise '3b: expected the bare value object to have no policy'
rescue RuntimeError; end
DeclarativePolicy.class_for(presenter.declarative_policy_subject) # must not raise
puts '3 OK: declarative_policy_subject resolves to a policied object'

# 4. The package union resolves a wrapped element by unwrapping the delegate
#    (case/=== bypasses the presenter's is_a? override).
resolved = Types::ArtifactRegistry::PackageType.resolve_type(presenter, {})
raise '4a' unless resolved == Types::ArtifactRegistry::MavenPackageType
npm = ArtifactRegistry::NpmPackage.new('id' => 'b1', 'name' => '@acme/ui')
wrapped_npm = ArtifactRegistry::ArtifactPresenter.new(npm, repository: repo, organization: org)
raise '4b' unless Types::ArtifactRegistry::PackageType.resolve_type(wrapped_npm, {}) == Types::ArtifactRegistry::NpmPackageType
puts '4 OK: union resolves wrapped Maven and npm elements'

puts 'ALL OK'

Expected output:

1 OK: delegates readers and carries repository + organization
2 OK: repository and organization are required
3 OK: declarative_policy_subject resolves to a policied object
4 OK: union resolves wrapped Maven and npm elements
ALL OK

Or run the specs directly:

bundle exec rspec ee/spec/presenters/artifact_registry/ \
  ee/spec/graphql/types/artifact_registry/ \
  ee/spec/graphql/resolvers/artifact_registry/ \
  ee/spec/graphql/resolvers/concerns/artifact_registry/

Suggested labels

~"type::maintenance" ~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 step changes no flag-visible behavior. No changelog (dark), no i18n, no schema surface change.

Edited by Narendran

Merge request reports

Loading
Loading