Gate every Artifact Registry surface on a configured base URL
What does this MR do and why?
An instance whose artifact_registry.api_url is unset renders the Artifact Registry pages, hides the URL-copy affordances without saying why, and answers every GraphQL query with the literal message base_url is required. The setting has no production default (config/initializers/1_settings.rb defaults it only in development and test), so this is the state a self-managed instance is in until an operator acts.
This introduces ArtifactRegistry::Configuration with one predicate, configured?, and applies it to every surface that has to agree on it:
ArtifactRegistryGating#ensure_artifact_registry_available!, covering all three Artifact Registry controllersSidebars::Organizations::Menus::ArtifactRegistryMenu#render?EE::Sidebars::Organizations::Menus::SettingsMenu#show_artifact_registry_menu_item?- both mount-data helpers, through
Configuration.client_base_url - the organization overview page, through a new
artifact_registry_configuredmount-data key
One predicate decides the route gate and the browser-facing value alike, so they cannot disagree and a rendered page always carries a non-nil client_base_url. HTTP_ORIGIN_PATTERN is deleted, since the degenerate origins it rejected can no longer be produced.
One rule set for the gate and the client
configured? admits an http(s) scheme with a host and no user information, query, fragment, or path deeper than /. Those four rules are not restated here: Configuration.base_url_violation(uri) returns the reason a parsed URL cannot serve as a base URL, or nil when it can. configured? checks for nil; ArtifactRegistry::Client#validate_base_url! raises the returned string as its ConfigurationError message, keeping its own parse, rescue and error reporting. So the gate and construction cannot drift.
That covers construction only. guard_service_transport! refuses plaintext HTTP in production per request, and the gate does not mirror it, so a production instance set to http://ar.internal:8080 passes configured?, renders, and then refuses every service-authenticated call. Nothing reaches that path until a service credential has a caller, so it is latent rather than live.
A distinct exception class, not a message swap
Base-URL faults now raise ArtifactRegistry::Client::ConfigurationError rather than ArgumentError. ArtifactRegistry::RendersErrors rescues ::ArgumentError broadly and forwards e.message into the response, where the setting's name does not belong, so the operator fault needs a class outside that hierarchy. The client's caller-input ArgumentError sites are unchanged and still surface their own messages, which the spec asserts so a later widening of the new class fails a test.
ConfigurationError maps to service-unavailable and logs nothing. An unconfigured instance is silent on every path, and the absent setting is the only signal: only an operator can change the condition, so a line per refused navigation or per query adds nothing, and a polling GraphQL client would otherwise write one unaggregated line per request. Gitlab::ErrorTracking.log_exception writes through trackers: [Logger], with no Sentry-side grouping to collapse the repeats.
The no-credential AuthorizationError path still logs; that condition is not operator-fixable by configuration. With the log gone, ConfigurationError and UnavailableError render identically and share one rescue clause.
Deliberate non-changes
- No gate on the resolver or the mutation. A resolver gate would return a null field, which is already documented as the signal for
artifact_registry_uibeing off. A second cause would make that description wrong. The client constructor stays the single enforcement point for callers that bypass the routes. artifact_registry_configuredis its own mount-data key, not folded intocan_read_artifact_registry. The two answers have different remedies, so keeping them separate lets an operator tell a permission problem from a configuration one from the page's data attribute alone.Organizations::Settings::ArtifactRegistryHelperis unchanged. It composesOrganizations::ArtifactRegistryHelperand inherits the delegation. Its spec is updated, since the behaviour it asserts changes.
Feature flag
artifact_registry_ui, unchanged. The new condition applies whether the flag is on or off.
References
- Plan:
docs/plans/monolith/2026-08-14-unconfigured-ar-base-url.md, Step 1 - Spec: monolith/S02 Artifact Registry Ruby client, which owns the configuration stanza and the
configured?row - Builds on !249808 (merged), which landed
validate_base_url!. This MR aligns the gate with those refusals and moves the base-URL raises toConfigurationError, including the one inguard_service_transport!.
Screenshots or screen recordings
No new UI. The change only removes affordances, and only on an instance whose base URL is unset or malformed:
| State | Organization overview | Navigation | Artifact Registry routes |
|---|---|---|---|
| Configured (well-formed origin) | unchanged from today | unchanged | unchanged |
| Unset or malformed | no "Go to Artifact Registry" link, no Artifact Registry wording in the empty state | both entries absent | 404 |
A correctly configured instance is pixel-identical before and after, which is why no before/after pair is attached.
How to set up and validate locally
-
Enable the flag and confirm the feature works as it does today:
Feature.enable(:artifact_registry_ui)With
artifact_registry.api_urlset to a well-formed origin, the sidebar entries, the organization overview link, and the Artifact Registry pages all behave as before. -
Comment the key out of
config/gitlab.yml, restart, and reload the organization overview. The "Go to Artifact Registry" link and the Artifact Registry wording are gone, both navigation entries are absent, and/-/organizations/<path>/-/artifact_registryreturns404. -
Set it to
https://ar.example.com/api/v1,http://,ftp://host, orhttps://user:pass@ar.example.com. Each behaves as the unset case. -
With the setting unset, query the API directly and confirm the response says the service is unavailable and never names the setting:
query { organization(fullPath: "<path>") { artifactRegistryRepositories { nodes { name } } } }
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.
Tests
430 examples, 0 failures across the eleven RSpec files, and 10/10 in app_spec.js. Both renders_errors operations are asserted, because the concern's other arms diverge by operation and this one does not. All three Artifact Registry routes assert the 404, rather than one standing in for the others, through a shared example the five gated surfaces consume.
Configuration's accept and reject tables assert configured?, client_base_url and construction of a real client per row, so the gate and the client are checked against one another over 24 values rather than in separate places.
Related to #617489 (closed)