Add artifact registry hosted repository create form
What does this MR do and why?
MR 3 of 3 in the monolith/S07 Step 6 chain. Targets
!247968 (merged) (MR 1), not master.
This change is behind the feature flag :artifact_registry_ui (default off).
Makes the create flow reachable and complete: the form's common fields, the contract validation, the create mutation, and the route.
Changes
repositories/create/repositories_create_form.vue— the whole create flow in one component: the four common fields, validation, the mutation, payload-error mapping, cache eviction, the success toast, and navigation back to the list.router/index.js— registers/new/hosted, and a kind-less/newthat redirects to it rather than falling through to not-found./new/remoteand/new/virtualstill 404 deliberately — they are Phase 2 and do not exist yet.constants.js— format options, the Private visibility value, the route name and title.repositories/index.js— installsGlToast, which is not global and which this app did not previously have. It lands here because this is where the first toast appears.
Why one component rather than a form plus a wiring wrapper
An earlier revision of this MR split it that way — a presentational repositories_form.vue
with heading/submitButtonText/initialValues/hiddenFields/readonlyFields props, a
submit emit and an advanced-settings slot, behind a thin create wrapper — so a future edit
flow could reuse the fields.
That edit flow does not exist, so the seam had zero consumers, and it cost roughly 270
lines: 42 of props, the field-variation machinery, the slot, a second .stories.js, and ~160
lines of tests asserting the prop contract rather than any behaviour. The shared part gets
extracted when the edit form is built and both callers are visible, rather than guessed at now.
Validation splits by origin
Client-side rules block submission, so a value the contract rejects never reaches the
mutation. The bounds come from api/openapi/v1.yaml, as implemented in
internal/managementapi/create.go:
RepositoryName: minLength 1, maxLength 255, pattern ^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?$
Description: maxLength 1024minLength: 1 needs no separate rule — the pattern already excludes the empty string.
Anything still coming back in the mutation payload is an API error by definition, so it
surfaces through createAlert rather than being matched back onto a field. A duplicate name
always arrives that way, because the client cannot know what already exists — so
"Name has already been taken." is a dismissible page alert, not a field error. That is a
deliberate change from the previous revision, which string-matched payload messages onto
fields; that matcher, its TODO, and the bookkeeping that retracted stale field errors on
retype are all gone.
Description length uses GlFormTextarea's built-in characterCountLimit, which announces the
remaining count to assistive technology on a debounce. Visibility offers Private alone, per
the closed-beta narrowing.
The cache eviction lives outside the component
graphql/utils/cache_update.js exports the mutation update factory, matching the convention
the container registry settings already use
(packages_and_registries/settings/project/graphql/utils/cache_update.js), so the component
holds no cache surgery. It drops the connection field without arguments and then collects —
evicting one argument-keyed entry would leave the other filter and sort variants stale.
Not in scope
No entry affordance — no toolbar create button, no per-row actions menu. Those are Step 5,
deliberately left open so monolith/S04 can extend the list view without conflict. The route
is reached directly by URL for now.
Screenshots or screen recordings
| The form | A name the contract rejects |
|---|---|
![]() |
![]() |
Verified end to end against the GDK with the chain assembled locally: /new redirects to
/new/hosted; Bad Name? is blocked client-side; a valid submit lands on the list with a
toast and the repository appears in MR 2's readback with the chosen format; re-submitting the
same name renders "Name has already been taken." against the name field, not as a
page-level alert, and stays on the create view.
⚠️ On the size of this MR
734 added lines It is one component (~245) and its spec (~325). Collapsing the two-MR version actually reduced the total under review from 1146 to 734, but it concentrates it in one place. Splitting again would mean inventing a boundary rather than finding one: the only remaining cut is "fields now, wiring later", which would ship a form whose submit does nothing and then rewrite it. Happy to do that if you would rather.
How to set up and validate locally
echo "Feature.enable(:artifact_registry_ui)" | bundle exec rails c/o/<your-org>/-/artifact_registry/acme/repositories/new— should redirect to.../new/hosted.acmeis required; it isOrganizations::ArtifactRegistry::STUB_SLUG.- Submit blank — "Name is required.", no mutation fired.
- Try
-,?,UPPERCASE,trailing-— each blocked against the contract pattern. - Fill a valid name, pick a format, submit — success toast, back on the list, and with MR 2 present the repository appears in the readback with the chosen format.
- Browser Back (in-SPA, so the mock's store survives), submit the same name again — "Name has already been taken." on the field, still on the create view.
- A full reload resets the store; it is in-memory by design.
The chain
master ─→ MR 1 ─┬─→ MR 2 repositories list reads from local state
└─→ MR 3 hosted repository create form (this)Merge order: MR 1, then MR 2 and MR 3 in either order.
Note
MR 2 and this MR both modify repositories/app_spec.js — MR 2 adds the Apollo provider the
list query needs, this one adds the create-route render and focus tests. Whichever merges
second needs a small union resolution there. Verified locally: the union is the two
providers plus both provide values plus all five tests, and it passes.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist.
- No changelog: dark behind a default-off feature flag.
- No backend code, no database surface, no documentation change.
Written by GitLab Duo

