Add disable and enable Artifact Registry mutations
What does this MR do and why?
Adds two GraphQL mutations, Mutations::ArtifactRegistry::Disable and Mutations::ArtifactRegistry::Enable, that toggle an organization's Artifact Registry between active and disabled.
Each mutation takes no arguments beyond clientMutationId. The organization is read from request context, and Artifact Registry derives the namespace from it.
The two mutations are near-identical, differing only by their condition endpoint (disable_namespace / enable_namespace) and target status. The shared flow (the availability gate, the endpoint call, the cache invalidation, and the payload build) lives in a small intermediate base, Mutations::ArtifactRegistry::ConditionMutation, parameterized by a condition :disable_namespace / condition :enable_namespace class-level declaration. disable.rb and enable.rb are thin subclasses that declare only their name, description, and endpoint, so the two cannot drift apart. ConditionMutation itself extends the shared Mutations::ArtifactRegistry::Base (the same base the repository mutations use), which supplies the feature-flag gate, the organization from context, the RendersErrors error mapping, and the standard errors payload array.
ConditionMutation overrides the base availability gate to require update_organization instead of the base's read_artifact_registry floor, because a condition change is an owner-only write. That ability implies the read the base would otherwise check, and it is re-checked server-side on every request (the Artifact Registry service credential carries no user identity, so the Rails check is the only user gate). When the artifact_registry_ui flag is off, the mutation raises a top-level resource-not-available error before any client call.
It calls the declared condition endpoint through the organization's service-authenticated client, builds the registry payload from that response, and invalidates the resolution cache so the render path re-reads the new status rather than a stale one. A subclass that forgets to declare its endpoint fails fast with a clear error rather than a confusing nil dispatch.
Repeating a condition against a registry already in the target state succeeds as a no-op, since Artifact Registry treats the condition endpoints as idempotent.
The request specs are de-duplicated the same way: the shared scenarios live in a shared example group, an Artifact Registry condition mutation, parameterized by the mutation name, endpoint, and target status, and each spec is a thin wrapper that sets those and calls it_behaves_like.
This change sits 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.
References
- Plan: https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/plans/monolith/2026-08-04-activation-and-deactivation.md
- Spec: https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/specs/monolith/S10-activation-and-deactivation.md
- Related to #608391 (closed)
Screenshots or screen recordings
N/A: this is a dark, off-by-default backend change with no user-visible behavior yet.
How to set up and validate locally
This is a dark, off-by-default change, validated by the automated checks and the console script below.
bundle exec rubocop \
ee/app/graphql/mutations/artifact_registry/condition_mutation.rb \
ee/app/graphql/mutations/artifact_registry/disable.rb \
ee/app/graphql/mutations/artifact_registry/enable.rb \
ee/spec/support/shared_examples/requests/api/graphql/mutations/artifact_registry/condition_mutation_shared_examples.rb \
ee/spec/requests/api/graphql/mutations/artifact_registry/disable_spec.rb \
ee/spec/requests/api/graphql/mutations/artifact_registry/enable_spec.rbConfirmed output: 6 files inspected, no offenses detected.
bundle exec rspec \
ee/spec/requests/api/graphql/mutations/artifact_registry/disable_spec.rb \
ee/spec/requests/api/graphql/mutations/artifact_registry/enable_spec.rbConfirmed output: 28 examples, 0 failures.
bundle exec rake gitlab:graphql:check_docsConfirmed output: GraphQL documentation is up to date.
The script below can be run with bundle exec rails runner <file> or pasted into rails console. It stubs the Artifact Registry client so no live service is needed, and prints ALL CHECKS PASSED on success.
Feature.enable(:artifact_registry_ui)
organization = FactoryBot.create(:organization)
owner = FactoryBot.create(:organization_owner, organization: organization).user
member = FactoryBot.create(:organization_user, organization: organization).user
mapping = FactoryBot.create(:artifact_registry_namespace_mapping, organization: organization)
# A fake client recording the calls the mutations make and returning a namespace
# in the requested state, so we assert the endpoint hit and the resolved payload
# without a real Artifact Registry.
fake_client = Class.new do
attr_reader :calls
def initialize
@calls = []
end
def disable_namespace(uuid:)
@calls << [:disable_namespace, uuid]
namespace('disabled', uuid)
end
def enable_namespace(uuid:)
@calls << [:enable_namespace, uuid]
namespace('active', uuid)
end
def namespace(status, uuid)
ArtifactRegistry::Namespace.new(
'id' => uuid, 'slug' => 'acme', 'status' => status,
'created_at' => '2026-01-01T00:00:00Z'
)
end
end.new
Organizations::Organization.define_method(:artifact_registry_service_client) { fake_client }
def run_mutation(name, user, organization)
query = "mutation { #{name}(input: {}) { registry { status } errors } }"
GitlabSchema.execute(query, context: { current_user: user, current_organization: organization }).to_h
end
failures = []
check = ->(label, condition) { failures << label unless condition; puts "#{condition ? 'PASS' : 'FAIL'}: #{label}" }
result = run_mutation('artifactRegistryDisable', owner, organization)
data = result.dig('data', 'artifactRegistryDisable')
check.call('disable: no top-level errors', result['errors'].nil?)
check.call('disable: payload errors empty', data && data['errors'] == [])
check.call('disable: status is disabled', data && data.dig('registry', 'status') == 'disabled')
check.call('disable: called disable_namespace with the mapping uuid',
fake_client.calls.include?([:disable_namespace, mapping.ar_namespace_id]))
result = run_mutation('artifactRegistryEnable', owner, organization)
data = result.dig('data', 'artifactRegistryEnable')
check.call('enable: status is active', data && data.dig('registry', 'status') == 'active')
before = fake_client.calls.size
result = run_mutation('artifactRegistryDisable', member, organization)
check.call('member: top-level access error', result['errors']&.any? { |e| e['message'].to_s.match?(/permission/) })
check.call('member: no client call', fake_client.calls.size == before)
Feature.disable(:artifact_registry_ui)
before = fake_client.calls.size
result = run_mutation('artifactRegistryDisable', owner, organization)
check.call('flag off: top-level access error', result['errors']&.any? { |e| e['message'].to_s.match?(/permission/) })
check.call('flag off: no client call', fake_client.calls.size == before)
Feature.enable(:artifact_registry_ui)
org_no_mapping = FactoryBot.create(:organization)
owner_no_mapping = FactoryBot.create(:organization_owner, organization: org_no_mapping).user
result = run_mutation('artifactRegistryDisable', owner_no_mapping, org_no_mapping)
check.call('no mapping row: top-level access error',
result['errors']&.any? { |e| e['message'].to_s.match?(/permission/) })
puts(failures.empty? ? "\nALL CHECKS PASSED" : "\nFAILED: #{failures.join(', ')}")Confirmed output: all ten checks print PASS, followed by ALL CHECKS PASSED.
Once the flag is enabled for an activated organization, the two mutations can also be run directly:
mutation { artifactRegistryDisable(input: {}) { registry { status } errors } }
mutation { artifactRegistryEnable(input: {}) { registry { status } errors } }MR acceptance checklist
This checklist encourages us to confirm any changes have been analyzed to reduce risks in quality, performance, reliability, security, and maintainability.