Add Artifact Registry slug-and-status read surface
What does this MR do and why
This adds the read half of the GitLab Artifact Registry activation surface: a cached resolver on the organization-to-namespace mapping, and a read-only GraphQL type and field that expose it.
It implements Steps 3 and 4 of the monolith S10 plan together, since each is small and Step 4 depends directly on Step 3.
Resolution and caching (Step 3)
ArtifactRegistry::NamespaceMapping#registryresolves the slug, derived status, and creation time from the mapping row's UUID (GET /api/gitlab/v1/namespaces/:uuid) and caches all three under a singleRails.cacheentry (5 minutes).#expire_registry_cacheinvalidates the entry for a Rails-initiated transition. It has no production caller in this MR; the activate, disable, and enable mutations that call it land in later steps, so it is registered inscripts/lint/keela_excluded.ymluntil then.- Failed resolutions are cached under a shorter negative expiry
(1 minute). The cached value is a plain hash carrying the client error's
class, status, code, and request ID, so
#registrycan hand back aResolutionFailurethat reconstructs the original typed exception. The resolver re-raises it, and the shared error-mapping concern maps a cached failure exactly as a live one would: an authorization failure resolves the field null, an API or unavailability failure surfaces the matching error with its status, code, and request ID. - A
404keeps the mapping row and resolves to an unknown status rather than treating the organization as not-activated. An organization with no mapping row makes no client call. - The cache stores plain hashes rather than value objects, so a rolling deploy that edits the value objects cannot leave undeserializable entries behind.
ArtifactRegistry::CachesClientgains a memoized, service-authenticatedartifact_registry_service_clientfor the render path, alongside the existing per-user client. The per-user client requires acurrent_userthis caller does not have; a separate memo keeps the nil-user client out of the per-user bucket.
Read GraphQL surface (Step 4)
Types::ArtifactRegistry::RegistryType(graphql_nameArtifactRegistry) exposesslug,status, andcreatedAt.statusis non-null and a plain String, not an enum, so a status Artifact Registry adds within its API version reaches the response instead of raising.slugandcreatedAtare nullable: the unknown 404 state resolves with a status but no slug or creation time. The type authorizesread_artifact_registry.Resolvers::ArtifactRegistry::RegistryResolverreturns null when the organization has no mapping row, and re-raises the reconstructed client exception on a cached failure so the shared error-mapping concern turns an authorization failure into a null field and an unavailability failure into a service-unavailable error.- An
artifactRegistryfield mounts on the organization type (null: true, experiment, authorizingread_artifact_registry) behind theartifact_registry_uifeature flag. With the flag off, the field stays in the schema, resolves null, and makes no client call. - Regenerates the GraphQL reference docs and the introspection JSON.
Scope
This supersedes and folds in !250719 (closed) (Step 4), which was stacked on this branch. Consolidated into one MR because the reviewable surface of each half is small.
Notes for reviewers
- Service credential is stubbed until the auth real path lands.
ArtifactRegistry::ServiceCredential#tokenreturnsnilby design until the credential is wired (with the S08/S09 auth work), the same team-wide state that keeps the S04/S07 clients stubbed. Until then this field resolves null on a real instance; a resolution attempt now logs the resulting authorization failure to error tracking rather than failing silently. - Fan-out. The field mounts on the organization type and costs one AR call per organization on a cold cache, bounded by the connection page size. The AR GitLab API is UUID-keyed with no bulk read, so this matches the standing per-node cost ADR-014 names; no batch loader is added here.
Feature flag
Behind the existing artifact_registry_ui feature flag, which is dark
and disabled by default. Because the flag is dark, there is no changelog
and the schema text needs no i18n.
Database review
Not required. The only database-adjacent term in the diff is
Rails.cache.delete, which is a cache operation, not a database query or
scope. No migration, no schema change, no new query.
How to set up and validate locally
This is a dark, off-by-default change with no user-visible behavior yet. It is validated by the model and GraphQL specs, the static-analysis lint, and the GraphQL artifact check.
-
RuboCop is clean on all changed files:
10 files inspected, no offenses detected -
The unused-methods static analysis passes (
bundle exec keela --quiet): exit0, no newly unused methods. -
The model and GraphQL unit specs pass:
bundle exec rspec \ ee/spec/models/artifact_registry/namespace_mapping_spec.rb \ ee/spec/models/ee/organizations/organization_spec.rb \ ee/spec/models/concerns/artifact_registry/caches_client_spec.rb \ ee/spec/graphql/types/artifact_registry/registry_type_spec.rb \ ee/spec/graphql/resolvers/artifact_registry/registry_resolver_spec.rbOutput:
75 examples, 0 failures. -
The GraphQL request spec passes, including the unknown-404 case:
VITE_ENABLED=true bundle exec rspec \ ee/spec/requests/api/graphql/organizations/artifact_registry_spec.rbOutput:
8 examples, 0 failures. -
The GraphQL introspection JSON and reference docs regenerate with no further diff (in sync).
Then enable the flag for an activated organization and run:
organization { artifactRegistry { slug status createdAt } }