feat(gitlabapi): resolve a namespace by UUID

What

GET /api/gitlab/v1/namespaces/{id} (S33, Namespace resolution): the handler, the OpenAPI operation, and the wiring.

  • No availability filter. A blocked, disabled, deleted, or purged namespace resolves 200 with its derived status. This is the surface the platform inspects and repairs through, so applying the client-side gating predicate here would lock it out of the rows it has to fix.
  • Canonical ids only. uuid.Parse accepts the unhyphenated, brace-wrapped, urn:uuid:-prefixed, and uppercase spellings, so one namespace answered on several working URLs. The parse now round-trips through String() and anything else is 400, and the parameter declares that narrower pattern so a generated client enforces it too. The rule constrains spelling, not encoding: the segment arrives percent-decoded, so an encoded form of the canonical id still resolves. That gap is recorded where the rule is stated; closing it would mean parsing EscapedPath for no gain against any client this surface serves.
  • 400 over 404 on a bad id. Reporting a typo as 404 would tell the platform a namespace is gone when it is not, and a platform flow may act on that destructively. The raw segment never reaches the store.
  • A store outage is never a 404. Only datastore.ErrNotFound takes that path; a nil row without an error is a controlled 500, not a nil dereference. Both 500s carry one operation-scoped log message, with the cause in the error field.
  • One store seam. gitlabAPIDeps takes a single gitlabapi.NamespaceStore, the consumer-side union of the two role interfaces, instead of the same value under one parameter per role.

Spec amendment

The spec described 404 for an unknown UUID and said nothing about a segment that is not a UUID, so this endpoint's 400 would have shipped undocumented. It also left every UUID-keyed route free to accept the alias spellings. AC 7, the resolution section, and the Error Cases table now state the canonical-form rule for the surface; conditions and verifications reuse the same guard when their steps land.

Stacking

Developed stacked on !1052 (merged) (Step 5's handler): Step 6 depends only on Step 4 in the plan, but it touches the Deps struct, the wire seam, and the provisioning test files that !1052 (merged) owned, so it branched from there to keep the diff clean. !1052 (merged) merged on 2026-07-28; this MR now targets main and is rebased onto it, so the diff is Step 6 alone.

Size

1,249 insertions and 53 deletions, of which 137 are production Go (detail.go 98, handler.go 29, wire_gitlab.go net 10). The remainder is 1,020 test lines and 92 of OpenAPI prose, spec, and CI wiring. Reviewable production surface is well inside the plan's 200-400 target. The test volume is the unit tables (lifecycle states, refusals, log guards), the canonical-spelling property, the contract sweep, and a deliberately thin integration composition: one hit and one miss through the real store. The per-column projection risk lives in internal/datastore, where the three readers share namespaceColumns() and TestNamespaceStore_FindByID_ReturnsEveryLifecycleState guards it.

Spec coverage

AC Behavior Test
7 Anchor, slug, and derived status returned; unknown UUID → 404 TestResolveNamespace_EveryLifecycleState_Returns200, TestResolveNamespace_UnknownUUID_Returns404
7 Non-canonical id → 400, over the whole alias family TestResolveNamespace_MalformedID_Returns400, TestResolveNamespace_NonCanonicalID_Returns400, TestParseCanonicalUUID_AcceptsExactlyTheCanonicalSpelling, TestContract_NamespaceIDParameter_MatchesHandlerGuard
7 The rule constrains spelling, not encoding: an encoded canonical id resolves TestResolveNamespace_PercentEncodedCanonical_Resolves
12 Resolves in every state, including soft-deleted and purged TestResolveNamespace_EveryLifecycleState_Returns200 (derived status per state); internal/datastore's TestNamespaceStore_FindByID_ReturnsEveryLifecycleState (projection per column, pre-existing)
13 Operation and every response class declared and validated TestResolution_ResponsesMatchOpenAPIContract
Both 500s log the operation with the cause; a canceled request writes and logs nothing unless a real fault raced the hang-up TestResolveNamespace_StoreFailure_Returns500, TestResolveNamespace_CanceledRequest_LogAndWriteGuards

internal/gitlabapi joins the test:integration job; its first integration test drives resolution through the real NamespaceStore, one seeded hit and one miss.

Known deviation

A method this path does not serve answers 404 from the subtree catch-all rather than 405. That is Step 4's fallback design, not this endpoint's; the 404 prose says so rather than claiming a guarantee the deployment does not make.

Related to #194 (closed)

Edited by João Pereira

Merge request reports

Loading
Loading