feat(gitlabapi): scaffold the GitLab API surface
What
Step 4 of the S33 Phase 1 plan: the mounted-but-minimal /api/gitlab/v1 surface (ADR-009) that the namespace endpoints of the later steps register on.
internal/gitlabapi:NewHandler(Deps)with a privateServeMuxand an envelope-404 fallback — an unmatched path under the subtree answers the S01 envelope with the correlation ID echoed, never the stdlib text/plain miss (the consumers are generated platform clients that decode the envelope on every error path). TheNamespaceresource DTO carries the seven wire keys;statuscomes from the shared ADR-007 derivation (never re-implemented),created_atis RFC 3339 UTC, and the billing anchor stays server-side under the additive-only policy.- Auth: a seventh dispatch-table row routes
/api/gitlab/v1/...through the S08 bootstrap-token interim (Bearer,realm="artifact-registry", empty 401 body — byte-identical posture to the management API row)./api/gitlab/v2/...and other near-misses stay on the unknown-prefix 404 fall-through, preserving the additive versioning story. The row's second literal segment generalized the matcher's anchor to a positionalanchorsslice. transport.CodeServiceUnavailable(503) extends the S01 code set for the fail-closed paths that land with the provisioning endpoint.api/openapi/gitlab-v1.yaml: OpenAPI 3.1 skeleton (info,Error,Namespace), registered asgitlab@v1inredocly.yaml. Response schemas are deliberately open (noadditionalProperties: false) and enum growth is documented as tolerated — the flip side of the surface's additive-only compatibility promise.lint:openapinow lints every registered document; thepagesjob publishes the rendered reference under/api/gitlab/.internal/gitlabapi/contract_test.go: the contract rig — document loads and validates (kin-openapi's 3.0-era semantics; the redocly CI lint is the authoritative 3.1 gate), schema required-set and emitted keys pinned in both directions against the serializer, the fallback 404 body validated against theErrorschema, and the code enum pinned to the transport constants.docs/dev/gitlab-api.md: the surface's dev page — conventions, the backward-compatibility policy, and the endpoint registration checklist.
Stacking
Opened stacked on Step 3's branch; Steps 1–3 (!1010 (merged), !1012 (merged) via !1010 (merged), !1022 (merged)) have since merged, so this MR now targets main directly and the branch was rebased onto main (squash merges rewrite history, so the step's commits were replayed with rebase --onto). Plan-wise, Step 4 depends only on Step 1.
Review notes
- Size: ~1,350 LOC total: 919 tests, 138 OpenAPI, 88 docs, 19 CI/tooling, and only ~185 of production Go (a handler with one fallback route, a DTO, a dispatch row, and a constant). The bulk is the contract/test rig the sibling endpoint MRs build on.
- Why one MR, not two: the only clean seam is the auth/transport groundwork (the dispatch row plus
service_unavailable, ~355 changed lines) versus the rest. That split still leaves a ~990-line second MR, because the OpenAPI document, the contract tests that pin it, the handler they exercise, and the dev page that describes them have to travel together; cutting between them would separate the contract from its tests. With only ~185 lines of production Go in the whole diff, peeling the groundwork off barely reduces review effort while deepening the step-4 → step-5 stack, so the step ships as one MR. The seam stands if reviewers prefer the split. - Files beyond the plan's Step 4 list:
internal/auth/dispatch.go(+tests) — required by the step's "stub auth enforced" acceptance, which no listed file could satisfy;cmd/artifact-registry/wire.go— the one-line boot call site for the listedwire_gitlab.go; thelint:openapiscript change — the job lintedv1.yamlby explicit path, so registeringgitlab@v1alone would not have put the new document under CI validation. - Spec reading (401 shape): the spec's Error Cases table lists envelope code
unauthorizedfor a missing/invalid service credential, but the surface ships the S08 interim — bodiless 401 with theBearerchallenge, the same shape the management API serves — per the spec's own "the same interim used by the management API" sentence. The OpenAPIErrordescription records the exception. Follow-up: amend the spec's Error Cases row to the interim shape. - Note for the endpoint MRs (Steps 5–8): the datastore status derivation reads five lifecycle columns, and a projection that omits one silently reads as
active. Each endpoint MR should carry an integration test reading a non-active row through its real query. - Conformance: no Maven/npm/OCI protocol behavior changes; no conformance-suite impact.
- E2E catalog: no
docs/testing/scenario rows; the surface exposes no endpoints yet. - Pre-push passes:
/validate-step(findings fixed in-branch),/review-branch(REQUEST CHANGES; every finding addressed in-branch or recorded above), the local PR-review toolkit, and/simplify(applied: theanchorsgeneralization; declined: loopingpagesover registered APIs — revisit when a third document registers). - A second full PR-review-toolkit round ran post-open: it produced the dev-page fallback correction, the stale-comment refreshes, and four test-hardening changes (assembled-app mount probe, emitted-keys⊆schema pin, dispatch-table row-shape invariants,
TestMatchrow-identity assertion), followed by a comment-conciseness pass.
Spec coverage
Spec: docs/specs/S33-gitlab-api.md
(surface conventions, OpenAPI contract, error envelope). This MR owns
the scaffolding slice of AC-13 plus the surface-wide 401/404 shapes
and the service_unavailable transport code; endpoint rows belong to
the sibling endpoint MRs of the same plan and are listed for
completeness.
Acceptance criteria
| # | Criterion | Tests |
|---|---|---|
| AC-1 | Provisioning 201 creates the row with status: active |
Out of scope: provisioning endpoint MR |
| AC-2 | Idempotent anchor replay returns 200, unaltered |
Out of scope: provisioning endpoint MR |
| AC-3 | Replay with a disagreeing body returns 409 |
Out of scope: provisioning endpoint MR |
| AC-4 | Brand-List Validator unreachable returns 503, no row |
Out of scope: provisioning endpoint MR; the service_unavailable code it renders with is pinned here by TestWriteError_ServiceUnavailable |
| AC-5 | Invalid/reserved/brand-rejected slug returns 422 |
Out of scope: provisioning endpoint MR |
| AC-6 | Slug taken by another namespace returns 409 |
Out of scope: provisioning endpoint MR |
| AC-7 | GET /namespaces/:uuid returns anchor, slug, derived status; unknown UUID 404 |
Endpoint behavior out of scope: resolution endpoint MR. The shared resource it serializes is pinned here: TestNamespaceFromModel_MapsRowFields, TestNamespace_WireShape, TestNamespaceFromModel_CreatedAtNormalizedToUTC |
| AC-8 | Condition endpoints set/clear exactly their own column | Out of scope: service-condition endpoints MR |
| AC-9 | Suspended: client download succeeds, push rejected; blocked/disabled/deleted: both rejected | Out of scope: serviceability-gating MR (!1022 (merged), merged) |
| AC-10 | Management-surface requests rejected by the same predicate table | Out of scope: serviceability-gating MR (!1022 (merged), merged) |
| AC-11 | Verifications 204/opaque 400 |
Out of scope: batch verifications endpoint MR |
| AC-12 | Resolution and conditions work in every state | Endpoint behavior out of scope: resolution/conditions MRs. The resource's per-state status derivation is pinned here: TestNamespaceFromModel_DerivedStatus |
| AC-13 | OpenAPI document defines every endpoint/schema/response and validates in CI | TestContract_DocumentValidates, TestContract_NamespaceSchema_MatchesResource, TestContract_Unknown404_MatchesErrorSchema (document skeleton: info, Error, Namespace; operation entries extend the rig as each endpoint MR lands) |
Error cases
| # | Condition | Tests |
|---|---|---|
| E-1 | All: missing or invalid service credential (stub token) 401 unauthorized |
TestMiddleware_GitlabAPIRow_Positive, TestMiddleware_GitlabAPIRow_Negative |
| E-2 | All: malformed JSON body 400 bad_request |
Out of scope: endpoint MRs (no scaffolding route parses a body) |
| E-3 | Provision: slug fails validation 422 |
Out of scope: provisioning endpoint MR |
| E-4 | Provision: slug taken 409 |
Out of scope: provisioning endpoint MR |
| E-5 | Provision: replay body disagrees 409 |
Out of scope: provisioning endpoint MR |
| E-6 | Provision: Brand-List Validator unreachable 503 service_unavailable |
Out of scope: provisioning endpoint MR; the code constant and 503 envelope round-trip: TestWriteError_ServiceUnavailable |
| E-7 | Resolution/conditions/verifications: unknown UUID 404 not_found |
Out of scope: endpoint MRs |
| E-8 | Conditions: unknown <action> segment 404 not_found |
Endpoint route table out of scope: conditions MR. The surface-wide envelope-404 fallback it rides is pinned here: TestNewHandler_UnknownPath_Returns404WithEnvelope, TestContract_Unknown404_MatchesErrorSchema, TestWireGitlabAPI_MountsEnvelopeHandler, TestWireGitlabAPI_UnmountedPathIsBareMux404 |
| E-9 | Verifications: unknown/foreign id, empty or oversized batch 400 |
Out of scope: batch verifications endpoint MR |
| E-10 | service_unavailable (503) added to the shared internal/transport code set |
TestWriteError_ServiceUnavailable |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S-1 | Internet-facing surface guarded only by the bootstrap-token stub until the service credential lands | TestMiddleware_GitlabAPIRow_Negative (fail-closed 401 on missing/empty/wrong-scheme/mismatched credentials), TestMiddleware_GitlabAPIRow_Positive, TestMiddleware_GitlabAPIRow_UnknownVersionFallsThrough |
| S-2 | Verifications response deliberately opaque | Out of scope: batch verifications endpoint MR |
| S-3 | No organization data leaves the registry; responses return what the caller already persisted | TestNamespace_WireShape (exactly the seven caller-known keys; billing anchor excluded); endpoint MRs extend per response |
| S-4 | Slug input crosses a trust boundary into SQL and the Brand-List Validator | Out of scope: slug-validation and provisioning MRs |
Resolved spec ambiguities
- Error Cases 401 row -> the S08/management-API interim: the row lists
envelope code
unauthorized, but surface conventions defer auth to "the same interim used by the management API", whose non-OCI 401s emitBearer realm="artifact-registry"and an empty body; the tests pin that shape. - Namespace resource field set -> seven wire keys: the spec/plan name
"anchor tuple, slug, derived status"; AC-1 adds the namespace UUID
and AC-2 adds
created_at, so the resource isid,slug,platform,entity_type,entity_id,status,created_at. The billing anchor is excluded: under the additive-only v1 policy an omitted field can be added later, an emitted one is permanent. - AC-13 at this step -> document skeleton only (info,
Error,Namespaceschemas); operation entries validate as each endpoint MR registers them, per the plan's testing strategy.
Related to #181 (closed)