S07 - Phase 3 Step 1: Expose the Artifact Registry repository id on the shared type

What this does

This exposes Artifact Registry's own repository identifier on the ArtifactRegistryRepository GraphQL type, by applying the existing ArtifactRegistry::ExposesElementId concern that every other Artifact Registry element type already uses. So it is a one-line include plus a one-line call, not new machinery. The field is Artifact Registry's UUID rather than a GitLab global ID, which is that concern's documented purpose.

The upcoming virtual repository upstream picker lists candidate repositories through the repositories connection, and AssociateUpstreamRepositoryRequest takes a required upstream_repository_id. Without an id on the connection row, the picker has nothing to send. ArtifactRegistryRepositoryDetails inherits the field, so the single repository read carries it too.

To be precise about the contract, because the first commit message overstates it: the committed OpenAPI spec does not declare upstream_repository_ids on CreateRepositoryRequest or UpdateRepositoryRequest, which require only name and format. Carrying the ordered upstream list on create and update is artifact-registry#1356 and is the gate on Steps 4 and 5 of this track. The id this MR exposes feeds the association route today.

The Apollo cache keys deliberately stay on name and are not moving to the new id. Artifact Registry's own spec calls the field "informational; URLs address a repository by name", ADR-009 makes that name unique within its namespace and immutable, and every repository eviction in cache_update.js addresses one by name.

That matters more than it reads, and this MR is the point at which it starts to. Apollo only keys on a field the query actually asked for, so while no document selected id the keyFields: ['name'] override was belt and braces: deleting it left the types unnormalized, which breaks visibly. Now that the documents select the field, deleting it silently rekeys both types on the Artifact Registry id while every name-based eviction quietly stops matching. Measured both ways against the real document:

with keyFields ['name']   key: ArtifactRegistryRepositoryDetails:{"name":"my-repository"}   eviction by name: hit
without it                key: ArtifactRegistryRepositoryDetails:01a0b226-...              eviction by name: miss

The cache_config.js comment is rewritten to say so, and cache_config_spec.js pins it.

Because the documents select the field, the multiversion compatibility warning is real rather than a false positive, so each of the 14 selections carries @gl_introduced(version: "19.5.0"). An older backend strips the tagged node before validation, since FutureFieldFilter#future_node? compares the tagged milestone against its own, then resolves the absent field to null through FutureFieldFallback; a backend that has the field resolves it normally, because that fallback only stands in when get_field finds nothing. The slice already uses this with the identical version string on get_artifact_registry, get_registry_permissions and activate_artifact_registry.

This is Step 1 of the monolith S07 Phase 3 virtual repository CRUD plan and is deliberately a small single-concern MR: it is the only schema root the Vue picker waits on. It ships dark behind the existing artifact_registry_ui feature flag, so there is no changelog. Worth being exact about what the flag does and does not cover: ArtifactRegistryGating 404s the organization controllers while the flag is off, which is the default, so no browser sends these documents at all. Where it is enabled it offers no protection against the rolling-deploy case, because the flag is evaluated well after field validation. The directive is the mitigation, not the flag.

Closes #629499 (closed)

Changes

  • ee/app/graphql/types/artifact_registry/repository_type.rb: includes ::ArtifactRegistry::ExposesElementId and calls exposes_element_id noun: 'repository', milestone: '19.5'.
  • ee/app/assets/javascripts/packages_and_registries/artifact_registry/graphql/cache_config.js: the comment above typePolicies is rewritten. No policy or key change.
  • ee/app/graphql/types/concerns/artifact_registry/exposes_element_id.rb: the comment claiming the browser normalizes the Apollo cache on this id is dropped. Repositories key on name, and ArtifactRegistryUpstreamRepositorySummary sets keyFields: false, so the sentence did not describe two of the eleven users, and completing it would leave a shared concern restating cache_config.js by hand for eleven callers. The global-ID half stays, since it justifies the GraphQL/FieldMethod disable.
  • The 14 Artifact Registry .graphql documents that select a repository now select id, each tagged @gl_introduced(version: "19.5.0"). @graphql-eslint/require-selections returns early on hasIdFieldInType, so it was quiet only while the type had no id; exposing one turns every repository selection set that omits it into an error, from create_repository through get_version_files. Selecting the field costs nothing at runtime, because cache_config.js keys both repository types on name and Apollo stores the id as an ordinary field. A scoped eslint.config.mjs relaxation was tried and dropped: it has no precedent in that file, and it contradicts doc/development/fe_guide/graphql.md, which states id-presence checking as a project-wide invariant.
  • The Artifact Registry frontend mocks carry an id, which is what the same guide prescribes for the resulting Missing field 'id' while writing result errors. mock_data.js gains REPOSITORY_ID, the inline repository mocks in cache_config_spec, repository_detail_spec, repository_actions_spec, artifacts_table_spec, versions_table_spec and mock_resolvers_spec gain one, and queries_spec pins id in the repository field lists it asserts.
  • ee/spec/frontend/fixtures/artifact_registry/{artifact,repositories,repository,version_detail}.rb: the fixture assertions gain the id their stubbed Artifact Registry bodies already returned, since id is required on the Repository schema in api/openapi/v1.yaml. The generated fixtures under tmp/tests/frontend/fixtures-ee/ are gitignored and regenerate in CI.
  • ee/spec/frontend/packages_and_registries/artifact_registry/graphql/cache_config_spec.js: pins that both repository types identify by name when handed an object carrying both an id and a name. Verified to fail when keyFields is removed.
  • Regenerated GraphQL schema artifacts (doc/api/graphql/reference/_index.md and public/-/graphql/introspection_result.json), both showing id: ID! on ArtifactRegistryRepository and ArtifactRegistryRepositoryDetails as "Introduced in GitLab 19.5. Status: Experiment".
  • ee/spec/graphql/types/artifact_registry/repository_type_spec.rb and repository_details_type_spec.rb: the exact field lists pin the new field, plus a non-null assertion on id.
  • ee/spec/requests/api/graphql/organizations/artifact_registry_repositories_spec.rb and artifact_registry_repository_spec.rb: a connection row and the single repository read each resolve id to Artifact Registry's identifier.
  • ee/spec/requests/api/graphql/mutations/artifact_registry/repositories/create_spec.rb and update_spec.rb: both already selected id through all_graphql_fields_for, which is why their fixtures needed one; they now assert the value the payload returns.

How to set up and validate locally

Run from the Rails root. Steps 4 and 5 need a live Artifact Registry and an organization activated for it; they were run against a local Artifact Registry built from origin/main on :8080 with the GDK on http://gdk.test:3333.

Step 1 - The six graphql-verify steps

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:generate_all_introspection_schemas
bundle exec rake gitlab:graphql:schema:dump
node scripts/frontend/graphql_possible_types_extraction.js --check
Output (✅)
gitlab:graphql:validate                        PASS (exit 0; every non-OK line is "SKIP ... client query")
gitlab:graphql:check_docs                      PASS "GraphQL documentation is up to date"
gitlab:graphql:check_introspection_sync        PASS (failed before regeneration, as expected, then passed)
gitlab:graphql:generate_all_introspection_schemas  PASS (+32 lines, 0 deletions: the two `id` blocks, no unrelated drift)
gitlab:graphql:schema:dump                     PASS (writes to gitignored tmp/tests/)
graphql_possible_types_extraction.js --check   PASS

Step 2 - The two GraphQL type specs

bundle exec rspec ee/spec/graphql/types/artifact_registry/repository_type_spec.rb \
                  ee/spec/graphql/types/artifact_registry/repository_details_type_spec.rb
Output (✅)
49 examples, 0 failures

Step 3 - The two request specs

bundle exec rspec ee/spec/requests/api/graphql/organizations/artifact_registry_repositories_spec.rb \
                  ee/spec/requests/api/graphql/organizations/artifact_registry_repository_spec.rb
Output (✅)
67 examples, 0 failures

Step 4 - A connection row resolves the Artifact Registry id, live

query {
  organization(id: "gid://gitlab/Organizations::Organization/1") {
    artifactRegistryRepositories(first: 3) {
      nodes { id name kind }
    }
  }
}
Output (✅)
{
  "data": {
    "organization": {
      "artifactRegistryRepositories": {
        "nodes": [
          { "id": "01a0b226-be19-7bd3-b65a-f842cc8194b7", "name": "spike-virtual-4b194207", "kind": "VIRTUAL" },
          { "id": "01a0b226-be00-72ee-aba7-472c264d3c5f", "name": "spike-upstream-4b194207-4", "kind": "HOSTED" },
          { "id": "01a0b226-bdfb-799c-bf62-202bf19768c4", "name": "spike-upstream-4b194207-3", "kind": "HOSTED" }
        ]
      }
    }
  }
}

The ids match what the client returns directly for the same namespace, read in a rails runner:

id="01a0b216-1a9d-7455-af94-dfcd12f5a95b" name="npm-virtual-demo" format="npm" kind="virtual"
id="01a0b226-be19-7bd3-b65a-f842cc8194b7" name="spike-virtual-4b194207" format="maven" kind="virtual"

Step 5 - The single repository read inherits the field, live

query {
  organization(id: "gid://gitlab/Organizations::Organization/1") {
    artifactRegistryRepository(name: "npm-virtual-demo") {
      id
      name
      kind
    }
  }
}
Output (✅)
{
  "data": {
    "organization": {
      "artifactRegistryRepository": {
        "id": "01a0b216-1a9d-7455-af94-dfcd12f5a95b",
        "name": "npm-virtual-demo",
        "kind": "VIRTUAL"
      }
    }
  }
}

Organization.artifactRegistryRepository is declared as RepositoryDetailsType, so this covers the "details type inherits it" criterion behaviorally. ArtifactRegistry::RepositoryPresenter delegates id to the value object, confirmed in a rails runner:

REPO=ArtifactRegistry::Repository id="01a0b216-1a9d-7455-af94-dfcd12f5a95b"
PRESENTER id="01a0b216-1a9d-7455-af94-dfcd12f5a95b" (delegates? true)

Step 6 - Lint the changed files

bundle exec rubocop ee/app/graphql/types/artifact_registry/repository_type.rb \
  ee/spec/graphql/types/artifact_registry/repository_type_spec.rb \
  ee/spec/graphql/types/artifact_registry/repository_details_type_spec.rb \
  ee/spec/requests/api/graphql/organizations/artifact_registry_repositories_spec.rb \
  ee/spec/requests/api/graphql/organizations/artifact_registry_repository_spec.rb
node_modules/.bin/eslint ee/app/assets/javascripts/packages_and_registries/artifact_registry/graphql/cache_config.js
Output (✅)
5 files inspected, no offenses detected
eslint: exit 0

Step 7 - The Artifact Registry GraphQL documents lint, and the frontend suite is green

Needs the apollo schema the eslint job consumes, built from the branch's own dump.

bundle exec rake gitlab:graphql:schema:dump
node -e "const fs=require('fs'),{buildSchema,printSchema}=require('graphql');fs.writeFileSync('tmp/tests/graphql/gitlab_schema_apollo.graphql',printSchema(buildSchema(fs.readFileSync('tmp/tests/graphql/gitlab_schema.graphql','utf8'),{assumeValid:true})))"
node_modules/.bin/eslint --no-warn-ignored 'ee/app/assets/javascripts/packages_and_registries/artifact_registry/**/*.graphql'
bundle exec rspec --pattern "ee/spec/frontend/fixtures/artifact_registry/**/*.rb"
node_modules/.bin/jest --config jest.config.js ee/spec/frontend/packages_and_registries/artifact_registry
Output (✅)

Before the documents selected id, against the real dump:

✖ 14 problems (14 errors, 0 warnings)

And with the selections added but the mocks untouched, which is the fallout the guide anticipates:

Test Suites: 15 failed, 53 passed, 68 total
Tests:       512 failed, 2020 passed, 2532 total

After both, on a fresh rebase onto master:

eslint over the artifact_registry .graphql documents   exit 0 (stock rule, no config change)
gitlab:graphql:validate                                exit 0, no non-OK Artifact Registry document
rspec ee/spec/frontend/fixtures/artifact_registry      13 examples, 0 failures
Test Suites: 71 passed, 71 total
Tests:       2692 passed, 2692 total

The fixture generators need gitaly, so run them without CI=true in a worktree, otherwise the four version_detail.rb examples fail on a refused praefect.socket.

Step 8 - The version directive is inert on a backend that has the field

future_node? strips the tagged node on any backend at the introduction milestone or older, this one included, so the risk is that it also suppresses the field where it exists. The fixture generators execute the real documents against the real schema, which settles it.

bundle exec rspec --pattern "ee/spec/frontend/fixtures/artifact_registry/**/*.rb"
python3 -c "import json; print(json.load(open('tmp/tests/frontend/fixtures-ee/ee/graphql/packages_and_registries/artifact_registry/graphql/queries/get_repositories.query.graphql.json'))['data']['organization']['artifactRegistryRepositories']['nodes'][0]['id'])"
Output (✅)
13 examples, 0 failures

get_repositories       -> id = 'a1b2c3d4-0000-0000-0000-000000000000'
get_repository_detail  -> id = 'a1b2c3d4-0000-0000-0000-000000000000'
get_repository_images  -> id = 'a1b2c3d4-0000-0000-0000-000000000000'

A real id rather than a null, so FutureFieldFallback correctly stands aside when get_field finds the field. The null arm is covered by the existing spec/requests/api/graphql/future_fields_spec.rb.

Evidence

Environment: local GDK on http://gdk.test:3333 with a local Artifact Registry built from origin/main on :8080. The caproni rig was not reachable during this run, so the fallback environment was used. Recorded signed in as a plain non-admin organization member.

Rails console

N/A. This MR changes no client method. The two rails runner reads quoted under Steps 4 and 5 are supporting checks on the value object and its presenter, not a client change.

GraphiQL

A repositories-connection row carries the Artifact Registry id

The connection query runs in GraphiQL and each of the three nodes resolves id to Artifact Registry's own UUID alongside name and kind.

The single repository read carries it too, through the details type

artifactRegistryRepository(name: "npm-virtual-demo") resolves id to the same UUID the connection returned for that repository, which is the details type inheriting the field.

Screen recordings

N/A. No user-visible change. The frontend edits are a comment in cache_config.js and one extra selected field in 14 documents; no cache policy, no key change, no component behavior change, and the schema addition ships dark behind artifact_registry_ui. The field the documents now request is not read by any component yet, and its first consumer is the upstream picker in a later step.

Related to #629072 (closed)

🤖 Generated with Claude Code

Edited by Fiona McCawley

Merge request reports

Loading
Loading