AR delete surface/Step 13: Manifest row delete on the manifests table

What does this MR do and why?

Renders the row menu's Delete manifest item on the manifests table for Docker and OCI images, addressing the row by digest through ArtifactRegistryManifestDelete (!253536 (merged), merged).

Step 13 of 19 in the monolith artifact delete surface plan. Closes #627403 (closed).

Behind the artifact_registry_ui feature flag, which is dark.

Three things this does differently from the version row delete it otherwise copies

The confirmation body comes from the row, not the modal. What a manifest delete costs depends on what the manifest is. Deleting an index leaves its children behind as untagged, standalone manifests (Figma), and deleting a signature leaves the image it signed verifying as unsigned (Figma). A modal deriving its own copy cannot say either, which is why #627401 (closed) made the copy caller-supplied. The kind-to-body mapping is manifestDeleteBody in utils.js, so it is unit-testable away from the table.

The refetch is conditional. Artifact Registry refuses to delete a manifest that another manifest indexes, and the mutation reports that refusal through its payload errors rather than by raising. So the mutation resolves, and a refetchQueries handed to apollo.mutate would re-read a list nothing was removed from. The refetch therefore runs only after the helper reports acceptance, via this.$apollo.getClient().refetchQueries({ include: [getArtifactManifestsQuery] }). A refusal shows no success toast and triggers no refetch.

The refusal is not pre-empted client-side. The design greys out Delete manifest on a manifest a parent index references, but S14 assigns that rule to the delete contract. So the item stays enabled and the 409 reaches the user as the error the shared helper already raises, naming the blocking digests, which the contract calls the caller's next move.

Props threaded through

The mutation takes name, imageId, and digest. manifests_table.vue declared none of the first two and versions_section.vue passed it neither, though it already passes both to versions_table.vue. Both now thread through. This is the one place Steps 12 and 13 differ in size.

Refetch, not cache eviction

The plan file's ## Approach still mandates cache eviction and forbids a refetch. That rule was dropped on 2026-09-09 for the reasons recorded on #627401 (closed). The plan has not been amended, so where the two disagree the issues are current.

Stacked on

Targets 627401-ar-shared-delete-modal-and-version-row-delete (!254357 (merged)), which adds both repositories/components/delete_confirmation_modal.vue and graphql/utils/delete_mutation.js. Neither exists on master, so this cannot target master yet. Retarget and rebase once that merges.

Screenshots or screen recordings

A single walk through against a live Artifact Registry: the affordance and per-kind confirmation copy (Index, Image, Signature), Cancel sending nothing, the 409 refusal naming the blocking parent digest, and the accepted index delete that the re-read reflects.

How to set up and validate locally

The manifest list now reads from the real Artifact Registry API (image.manifests, wired up in !254165 (merged)), so this validates as one real-backend flow rather than the mock-list-plus-real-mutation split it used to need.

Prerequisites

  1. Artifact Registry running on :8080, with a hosted DOCKER or OCI repository holding a real OCI index that has per-platform children and at least one tag. Push a signature manifest too if you want to see all three confirmation-copy variants.

  2. GDK up. Sign in on :3333, not :3000: /users/sign_in 404s on the http-router.

  3. Enable the dark flag in rails console:

    Feature.enable(:artifact_registry_ui)

Walk through

Navigate to a container image's manifest list (.../artifact_registry/repositories/<name>/artifacts/<image-id>). The rows are real, read from image.manifests.

  1. Every row offers the action. Each row's overflow menu contains Delete manifest, rendered destructive (red). The menu toggle's accessible name is More actions for <short digest>, not just "More actions".

  2. The confirmation wording follows the row's kind. Open Delete manifest on each of these rows and confirm the body:

    Row kind Body must say
    Index (Type reads Index) its child manifests are not deleted and will remain in Artifact Registry as untagged, standalone manifests
    Plain image (Type reads Image) only that the manifest and any tags pointing to it go
    Signature (Type reads Signature for ...) the image it signs will then verify as unsigned

    All must end with This action cannot be undone, and none may ask you to type anything to confirm (ADR-010 scopes typed confirmation to repository delete).

  3. Dismissing sends nothing. Open the confirmation, press Cancel, and confirm no GraphQL request left the page.

  4. The refusal (409). Confirm the delete on a child manifest the index references. Artifact Registry refuses it, and because that refusal comes back through the mutation's payload errors, the shared helper raises an error alert naming the blocking parent digest. No success toast fires and the list is unchanged.

  5. The acceptance. Confirm the delete on the index digest. The mutation is accepted, the success toast appears, and the conditional refetch re-reads the list: the index and the tags that pointed at it are gone, its children remain as untagged, standalone manifests. Artifact Registry accepts rather than completes the request, so the row clears once the re-read lands.

MR acceptance checklist

  • Tests added for this feature/bug (Jest specs for manifests_table.vue, versions_section.vue, and manifestDeleteBody)
  • Conforms to the code review guidelines
  • Conforms to the Vue style guide
  • New strings extracted to locale/gitlab.pot
  • No changelog entry: the feature flag is dark

🤖 Generated with Claude Code

Edited by Fiona McCawley

Merge request reports

Loading
Loading