Repository DELETE answers 500 for the virtual kind, empty or not
What happens
DELETE /api/v1/{slug}/repositories/{repository_name} answers 500 for every
virtual repository. Empty or not, and whichever value the destructive query
parameter carries.
datastore.RepositoryStore.Delete reads the row's kind and applies
repositoryDeleteKindGate before it issues the DELETE. That gate returns
ErrRepositoryDeleteVirtualKind for RepositoryKindVirtual, because the
virtual kind's removal contract, dissociating the repository's upstream
associations, is not implemented by that method.
managementapi.writeDeleteStoreError has no arm for that sentinel. It matches
ErrRepositoryNotEmpty and ErrNotFound; everything else falls to
logAndWriteInternalError, which logs at ERROR and writes the shared 500.
Two consequences follow from the gate running ahead of the DELETE:
- The
409arm is unreachable for the virtual kind.ErrRepositoryNotEmptycomes from the foreign keys theDELETEtrips, and theDELETEnever runs. - The
202arm is unreachable for the same reason, because it branches off that same sentinel.
A virtual repository's delete therefore never reaches the not-empty selector at
all, and the answer does not depend on whether the repository holds anything.
This predates the destructive query parameter and is unchanged by it.
Why the severity is low
The management API refuses kind: virtual and kind: remote on create with
422 kind must be hosted, so a virtual repository exists only where something
other than this API inserted it. That is what has kept the 500 unnoticed.
What a fix has to decide
The Repository entry point group of docs/specs/S20-a-lifecycle-closed-beta.md
names no arm for the virtual kind, so the answer is not written down anywhere.
Two candidates:
- Map
ErrRepositoryDeleteVirtualKindto a refusal of its own, naming the unimplemented removal contract as the reason. This is a mapping change, and it needs a status decided rather than borrowed: the two the route already uses both carry meanings that do not fit. - Implement the removal contract, which is an edit to
repositoryDeleteKindGate. Note thatdatastore.RepositoryReaper.finalizeis that gate's second caller: its ownDELETE FROM repositoriestakes no child-row lock and removes no association row, so dropping the virtual arm answers for that call site too.RepositoryReaper.finalize's doc comment states that obligation in full.
Either way the fix changes what the delete surface delivers rather than correcting a mapping mistake, which is why it is filed here instead of folded into the destructive-delete work that surfaced it.
Where it is written down in the tree
internal/managementapi/delete.go—writeDeleteStoreError's default arm.internal/datastore/repositories.go—repositoryDeleteKindGateandRepositoryStore.Delete.