docs(plans): add the monolith/S04 repositories list plan

What does this MR do and why?

Adds the implementation plan for monolith/S04 (Repositories list), decomposing the slice into nine GitLab-monolith MRs along the GraphQL schema seam.

It also carries a small amendment to the S04 spec (the first two commits), which resolves two self-contradictions the plan would otherwise have had to decide on the spec's behalf. Happy to split those into their own MR ahead of this one if reviewers prefer.

Decomposition

# Step Depends on
1 GraphQL repository type and organization connection
2 GraphQL filter and sort arguments 1
3 Repositories list route and page shell
4 Repositories table 3
5 List toolbar and filters 3
6 Sortable columns and the sticky sort 4, 5
7 Connect the repositories list view 2, 6
8 Recent searches 5
9 Copy URL 4

The backend chain (1 → 2) and the frontend track (3 → 4, 5) start in parallel on the first MR, and only Step 7 spans both. Steps 8 and 9 are leaves that nothing depends on and that revert on their own. Every step sits under the review ceiling.

Spec amendment

  • Sort placement. The component tree listed "a sort affordance" in the toolbar while the toolbar section specified sortable column headers. The toolbar entry is dropped; the design already shows sortable headers, so the correction needs no resolution of its own.
  • Recent searches. The spec required persisting filter and sort selections, justified by the monolith filtered-search convention — which persists submitted filter tokens and not sort. Narrowed to filters, with the active sort persisted separately as a preference, the way the packages_and_registries lists do. Criterion 11 and the MVP-scope resolution follow.

Decisions the plan records

Scope and placement

  • No Ruby client step. monolith/S02 owns repositories, and it ships as the gitlab-artifact_registry-client gem, so nothing here touches ee/lib/.
  • No relocation or EE-seam step. monolith/S01 already re-homed the AR organization surface, and Types::Organizations::OrganizationType already carries prepend_mod with a live EE extension.

GraphQL surface

  • S04 introduces the shared ArtifactRegistryRepository type and its enums. Both this plan and monolith/S07 claimed Create on the type and the format, kind, and visibility enums, so whichever landed second would redefine a live schema type. The spec leaves the order open while its Type section expects S07, so this nomination invokes the spec's own "If S04 lands before S07" clause and needs no spec amendment. The reciprocal change has landed on !990 (merged). The type declares graphql_name 'ArtifactRegistryRepository' and authorize :read_artifact_registry, which Graphql/AuthorizeTypes requires and a field-level authorize: keyword does not satisfy, and each enum declares a prefixed graphql_name, which Graphql/EnumNames requires.
  • An additive-argument seam splits the surface: Step 1 lands the type, enums, and bare connection; Step 2 adds the filters and sort.
  • The membership gate is not a role pre-check. organization_policy.rb enables read_artifact_registry for any organization user, and AR owns role assignments — a principal can hold a direct role on one repository at any visibility without holding anything at the namespace level — so the resolver keeps the membership gate and adds no AR-role pre-check. A non-member resolves null; an organization user with no assignment anywhere gets an empty connection from AR, not null and not an error.
  • No id field. AR's id is a UUID its own contract calls informational, and a raw UUID would violate the monolith's GlobalID convention, so the Apollo type policy keys on name, which ADR-009 makes unique and immutable.
  • BigInt counters, since downloadsCount and sizeBytes are int64 at AR and Int overflows above two gigabytes.
  • The visibility enum mirrors the contract, carrying PUBLIC and INTERNAL beside PRIVATE even though the closed-beta scope narrows which values repositories take: the enum decodes what AR can return rather than what the phase creates, and an enum missing a returnable value would fail the whole connection on coercion. monolith/S07 narrows to Private on its write path (the create input and the form control) rather than on the shared enum.

Frontend

  • The plan extends the local GraphQL layer rather than creating it. The AR tree already carries the connection query document, typedefs for the @client entities, runtime resolvers registered through createDefaultClient, and a cache config with keyFields: ['name']. Step 4 adds its node fields and the matching typedefs and resolver seeds; Step 7 adds the connection policy and deletes the repositories entries from the local schema and resolvers once the field resolves server-side. Each strips the comment naming a future owner.
  • Those runtime resolvers mean the routed view renders in a browser before the schema exists, which is why Step 9 hangs off the table rather than the connect step: its :js example asserts a rendered row with no schema and no AR call.
  • Jest specs mock resolvers, not handlers, until Step 7 removes the @client directive, because an entirely client-side query never fires a mocked request handler. The runtime resolvers are a separate layer that no Jest spec reads.
  • Sorting is its own step. Sort is the only selection dimension with two homes, the route query and localStorage, so Step 6 holds the sortable headers, the emit, the route-query dimension, the sticky preference, the precedence rule, and the sort announcement. It follows both the table and the toolbar because the page's combined selection state and cursor reset arrive with Step 5, the first step to write a route-query dimension that is not the cursor.
  • The route query wins over the stored sort. The query is the single source of truth and the stored value is a fallback the page reads only when the query omits sort, restoring it with router.replace. A shared URL therefore renders the sort it carries, whatever the recipient has stored.
  • The sticky-sort key is deliberately unscoped, matching every live sort key in the monolith. The asymmetry with the scoped recent-searches key is the convention rather than an oversight: that key holds accumulated content and takes a <username>/ prefix, this one holds a preference and does not.
  • Selection state lives in the route query through the SPA's vue-router, so a filtered view is shareable and survives a reload. That would fire the S01 shell's whole-route focus watcher on every filter, sort, and page change, so Step 4 narrows the watcher to the path alongside the first updates that can trigger it.
  • The toolbar composes FilteredSearchBarRoot, which already carries the recent-searches machinery. Its storage key is caller-composed, so a namespace of <gon username>/<organization global ID> gives the per-user and per-organization scoping the spec requires, and sortOptions defaults to empty so no sort dropdown renders. namespace is a required prop, so the missing-username case omits only the storage key, which is what gates both the history list and the save path.
  • The live region announces the result state, not a count, because the list endpoint returns no total and the aggregate count is Phase 2. It ships with the first state change it can announce, since a region that enters the DOM at the same moment its content does is not reliably announced.
  • Mount bootstraps stay out of Jest. No mount bootstrap under packages_and_registries has a Jest spec, so the mount-data-to-provide seam is covered in the :js feature spec instead.
  • Copy URL is one step end to end (Step 9), holding the mount-data base URL, the builder, and the row action. It is the plan's only mixed-language step, under the exemption for a split whose halves carry no standalone value.

Step 1 is gated on monolith/S02 and monolith/S03 merging, neither of which has an MR open yet. Step 9 reads api_url from the Gitlab.config.artifact_registry stanza the S02 plan assigns to monolith/S03; the S03 plan does not schedule it yet.

One item stays open on the S07 side and does not block this plan: the S07 spec states the visibility narrowing over the enum rather than the write path, so it needs a one-sentence amendment on its own spec branch.

🤖 Generated with Claude Code

Edited by Rahul Chanila

Merge request reports

Loading
Loading