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: missThe 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::ExposesElementIdand callsexposes_element_id noun: 'repository', milestone: '19.5'.ee/app/assets/javascripts/packages_and_registries/artifact_registry/graphql/cache_config.js: the comment abovetypePoliciesis 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 onname, andArtifactRegistryUpstreamRepositorySummarysetskeyFields: false, so the sentence did not describe two of the eleven users, and completing it would leave a shared concern restatingcache_config.jsby hand for eleven callers. The global-ID half stays, since it justifies theGraphQL/FieldMethoddisable.- The 14 Artifact Registry
.graphqldocuments that select a repository now selectid, each tagged@gl_introduced(version: "19.5.0").@graphql-eslint/require-selectionsreturns early onhasIdFieldInType, so it was quiet only while the type had noid; exposing one turns every repository selection set that omits it into an error, fromcreate_repositorythroughget_version_files. Selecting the field costs nothing at runtime, becausecache_config.jskeys both repository types onnameand Apollo stores the id as an ordinary field. A scopedeslint.config.mjsrelaxation was tried and dropped: it has no precedent in that file, and it contradictsdoc/development/fe_guide/graphql.md, which statesid-presence checking as a project-wide invariant. - The Artifact Registry frontend mocks carry an
id, which is what the same guide prescribes for the resultingMissing field 'id' while writing resulterrors.mock_data.jsgainsREPOSITORY_ID, the inline repository mocks incache_config_spec,repository_detail_spec,repository_actions_spec,artifacts_table_spec,versions_table_specandmock_resolvers_specgain one, andqueries_specpinsidin 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, sinceidis required on theRepositoryschema inapi/openapi/v1.yaml. The generated fixtures undertmp/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 whenkeyFieldsis removed.- Regenerated GraphQL schema artifacts (
doc/api/graphql/reference/_index.mdandpublic/-/graphql/introspection_result.json), both showingid: ID!onArtifactRegistryRepositoryandArtifactRegistryRepositoryDetailsas "Introduced in GitLab 19.5. Status: Experiment". ee/spec/graphql/types/artifact_registry/repository_type_spec.rbandrepository_details_type_spec.rb: the exact field lists pin the new field, plus a non-null assertion onid.ee/spec/requests/api/graphql/organizations/artifact_registry_repositories_spec.rbandartifact_registry_repository_spec.rb: a connection row and the single repository read each resolveidto Artifact Registry's identifier.ee/spec/requests/api/graphql/mutations/artifact_registry/repositories/create_spec.rbandupdate_spec.rb: both already selectedidthroughall_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 --checkOutput (✅ )
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 PASSStep 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.rbOutput (✅ )
49 examples, 0 failuresStep 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.rbOutput (✅ )
67 examples, 0 failuresStep 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.jsOutput (✅ )
5 files inspected, no offenses detected
eslint: exit 0Step 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_registryOutput (✅ )
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 totalAfter 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 totalThe 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)