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
upstreamRepositoriestoTypes::ArtifactRegistry::RepositoryDetailsType(ArtifactRegistryRepositoryDetails): a plain list ofArtifactRegistryUpstreamRepositoryAssociation,null: true, experiment milestone 19.5. - Add
Resolvers::ArtifactRegistry::UpstreamRepositoriesResolver: includes the sharedArtifactRegistry::ResolvesOnRepositoryconcern, carriesextension Gitlab::Graphql::Limit::FieldCallCount, limit: 1, returns nil unlesspresented_repository.virtual?, and otherwise callsclient.upstream_repositories(slug:, repository_name:, format:). - Add
Types::ArtifactRegistry::UpstreamRepositoryAssociationType(ArtifactRegistryUpstreamRepositoryAssociation):idthrough the sharedExposesElementIdconcern,position(Int, non-null), andupstream_repository(non-null summary type), underauthorize :read_artifact_registryandauthorize_granular_token skip_reason: :parent_authorizes. - Add
Types::ArtifactRegistry::UpstreamRepositorySummaryType(ArtifactRegistryUpstreamRepositorySummary):id,name(String, non-null),format(the existingRepositoryFormatEnum, non-null), andkind(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,HOSTEDandREMOTE. - Regenerate
doc/api/graphql/reference/_index.mdandpublic/-/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.rbfield list, and a webmock-driven request spec that covers all kind-contract behavior.
References
- Work item: #628664 (closed)
- Parent work item: #628475 (closed)
- Plan MR (merged): gitlab-org/ops/artifact-registry!2534 (merged), file
docs/plans/monolith/2026-09-10-repository-detail-virtual.md, step "Step 3: Upstream list field and types" - Step 1, the client read (merged): !254953 (merged)
- Step 2, the kind gate (merged): !255247 (merged)
- Artifact Registry work item for the missing
listUpstreamRepositorieshandler: gitlab-org/ops/artifact-registry#314 (closed)
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.rbOutput (✅ )
10 files inspected, no offenses detectedStep 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.rbOutput (✅ )
37 examples, 0 failuresThe 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.rbOutput (✅ )
21 examples, 0 failuresDriven 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 --checkOutput (✅ )
### 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.
Related to #628475 (closed)