monolith/S05 virtual Step 3: Upstream list field and types

What this does

Step 3 of the monolith/S05 virtual repository detail plan for Artifact Registry. Adds the upstreamRepositories field to Types::ArtifactRegistry::RepositoryDetailsType (ArtifactRegistryRepositoryDetails), returning the upstream repositories a virtual repository resolves through, in resolution order.

The field resolves only for a virtual repository, using the virtual? predicate Step 2 (!255247 (merged)) added to the repository value object. A hosted or remote repository resolves null without reaching Artifact Registry. The field is a plain list rather than a connection: Artifact Registry caps the upstream list at 20 and sends no Link header (its ADR-004), so the client read from Step 1 (!254953 (merged)) returns a bare array and there is nothing to page. It can be selected once per operation.

Each association carries its 1-based resolution position and a summary of the repository it points at (id, name, format, kind). Kind admits HOSTED and REMOTE only, because an upstream is never itself virtual. Any breach of the kind contract (an unknown value, or a blank, null, or absent one) nulls that row's kind alongside a top-level error rather than resolving an unknown badge value. That row-scoped null is a schema property rather than a rendered degradation: the monolith's Apollo client sets no errorPolicy, so the default none discards the whole data envelope whenever a top-level error is present, and the detail page renders its service-unavailable alert in place of the table. Rendering the other rows beside one blank badge would need errorPolicy: 'all' on the reading query, which no step has specified.

The field also resolves null for a repository that is gone, and when Artifact Registry rejects the read: silently for a 401, 403, or 404, and alongside a top-level error for a 429, a 5xx, or any other 4xx. Everything here is EE-only and dark behind the artifact_registry_ui feature flag, so there is no changelog entry.

Closes #628664 (closed)

Changes

  • Add upstreamRepositories to Types::ArtifactRegistry::RepositoryDetailsType (ArtifactRegistryRepositoryDetails): a plain list of ArtifactRegistryUpstreamRepositoryAssociation, null: true, experiment milestone 19.5.
  • Add Resolvers::ArtifactRegistry::UpstreamRepositoriesResolver: includes the shared ArtifactRegistry::ResolvesOnRepository concern, carries extension Gitlab::Graphql::Limit::FieldCallCount, limit: 1, returns nil unless presented_repository.virtual?, and otherwise calls client.upstream_repositories(slug:, repository_name:, format:).
  • Add Types::ArtifactRegistry::UpstreamRepositoryAssociationType (ArtifactRegistryUpstreamRepositoryAssociation): id through the shared ExposesElementId concern, position (Int, non-null), and upstream_repository (non-null summary type), under authorize :read_artifact_registry and authorize_granular_token skip_reason: :parent_authorizes.
  • Add Types::ArtifactRegistry::UpstreamRepositorySummaryType (ArtifactRegistryUpstreamRepositorySummary): id, name (String, non-null), format (the existing RepositoryFormatEnum, non-null), and kind (the new enum, nullable), guarded against a value outside the contract, with the guard set derived from the enum rather than restated.
  • Add Types::ArtifactRegistry::UpstreamRepositoryKindEnum (ArtifactRegistryUpstreamRepositoryKind) with exactly two values, HOSTED and REMOTE.
  • Regenerate doc/api/graphql/reference/_index.md and public/-/graphql/introspection_result.json.
  • Specs: the resolver, the association type, the summary type, and the kind enum, all covering static schema shape only (name, authorizations, field list, nullability, experiment status), the updated repository_details_type_spec.rb field list, and a webmock-driven request spec that covers all kind-contract behavior.

References

How to set up and validate locally

Step 1 - RuboCop is clean on the changed Ruby

bundle exec rubocop --no-server \
  ee/app/graphql/types/artifact_registry/repository_details_type.rb \
  ee/app/graphql/resolvers/artifact_registry/upstream_repositories_resolver.rb \
  ee/app/graphql/types/artifact_registry/upstream_repository_association_type.rb \
  ee/app/graphql/types/artifact_registry/upstream_repository_summary_type.rb \
  ee/app/graphql/types/artifact_registry/upstream_repository_kind_enum.rb \
  ee/spec/graphql/resolvers/artifact_registry/upstream_repositories_resolver_spec.rb \
  ee/spec/graphql/types/artifact_registry/upstream_repository_association_type_spec.rb \
  ee/spec/graphql/types/artifact_registry/upstream_repository_summary_type_spec.rb \
  ee/spec/graphql/types/artifact_registry/upstream_repository_kind_enum_spec.rb \
  ee/spec/requests/api/graphql/organizations/artifact_registry_repository_upstream_repositories_spec.rb
Output (✅)
10 files inspected, no offenses detected

Step 2 - The resolver and type specs pass

bundle exec rspec \
  ee/spec/graphql/resolvers/artifact_registry/upstream_repositories_resolver_spec.rb \
  ee/spec/graphql/types/artifact_registry/upstream_repository_association_type_spec.rb \
  ee/spec/graphql/types/artifact_registry/upstream_repository_summary_type_spec.rb \
  ee/spec/graphql/types/artifact_registry/upstream_repository_kind_enum_spec.rb \
  ee/spec/graphql/types/artifact_registry/repository_details_type_spec.rb
Output (✅)
37 examples, 0 failures

The resolver cases cover the one-call-per-operation limit, one upstream-list read per format (maven, npm, docker, oci) carrying the slug, repository name, and format, the order Artifact Registry sent, hosted and remote resolving null without reaching Artifact Registry, and a 404 resolving null rather than erroring.

Step 3 - The request spec passes

bundle exec rspec ee/spec/requests/api/graphql/organizations/artifact_registry_repository_upstream_repositories_spec.rb
Output (✅)
21 examples, 0 failures

Driven by webmock against a stubbed Artifact Registry: associations render in position order with each summary discriminated by kind, an empty list renders empty rather than null, the whole list is read in exactly one Artifact Registry request, hosted and remote resolve null with no list call, a 404 between the two reads resolves null, a server error, a transport failure, and a timeout each render the service-unavailable error beside the still-loaded repository, an unknown, null, blank, or absent kind each nulls just that row's kind while the rest of the list and the repository still resolve, aliasing the field twice in one operation is rejected on the second selection and still costs a single Artifact Registry read rather than two, the credential and scheme stay out of the response body, and the repository resolves null with no client built when the artifact_registry_ui flag is off, when the user is not an organization member, and when the caller is anonymous.

Step 4 - The graphql-verify steps pass, one rake invocation each

bundle exec rake gitlab:graphql:validate
bundle exec rake gitlab:graphql:check_docs
bundle exec rake gitlab:graphql:check_introspection_sync
bundle exec rake gitlab:graphql:schema:dump
node scripts/frontend/graphql_possible_types_extraction.js --check
Output (✅)
### validate
OK    ee/app/graphql/queries/mcp/security/save_vulnerability_update_severity.mutation.graphql
OK    ee/app/graphql/queries/mcp/wikis/list_group_wiki_pages.query.graphql
OK    ee/app/graphql/queries/mcp/work_items/get_work_item.query.graphql
(one OK line per operation file, last three shown)

### check_docs
GraphQL documentation is up to date

### check_introspection_sync
✅ All GraphQL introspection schemas are up to date

### schema:dump
Schema IDL dumped into tmp/tests/graphql/gitlab_schema.graphql
Schema JSON dumped into tmp/tests/graphql/gitlab_schema.json

### possible_types
(no output, exit 0)

Evidence

Change type: GraphQL (one field, one resolver, three types). No client class and no frontend file changed, so the console and screen-recording sections are N/A.

Rails console

N/A. The client read this field calls shipped in Step 1 (!254953 (merged)); nothing in ArtifactRegistry::Client changed here.

GraphiQL

Not recorded. The virtual path cannot be driven end to end against a live Artifact Registry yet: virtual create is refused and the listUpstreamRepositories route is contract-only with no handler (gitlab-org/ops/artifact-registry#314 (closed)), so no repository(name:) read returns kind: virtual for GraphiQL to exercise. The behavior is proven deterministically by the request spec in Step 3 instead, which stubs the Artifact Registry responses and asserts the rendered payload and the outbound request count.

Screen recordings

N/A. No frontend files changed; this step is backend-only and dark behind artifact_registry_ui.

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

Related to #628475 (closed)

Edited by Fiona McCawley

Merge request reports

Loading
Loading