mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:41:30 +00:00
3c81531398
Phase 5 reconciliation: the audit's headline framing 'ARCH-H1 = 62-route
OpenAPI gap' was a measurement scoping error. Every one of the 209
unique router routes is already accounted for — 154 in api/openapi.yaml,
55 in api/openapi-handler-exceptions.yaml. The existing
openapi-handler-parity.sh CI guard already enforces this and passes
clean today. The audit subtracted operation-count from route-count
without accounting for the documented exceptions YAML.
Where real work remains (and what this PR does about it)
=========================================================
Of the 64 documented exceptions, 35 are legitimate wire-protocol
carve-outs that MUST stay (SCEP RFC 8894 × 8 entries, ACME RFC 8555
default + per-profile × 27 entries — they're protocol contracts, not
REST resources). The remaining 29 are REST-shaped routes whose
OpenAPI ops were deferred during their original Bundle 2 /
audit-2026-05-10 / 2026-05-11 work:
- auth/sessions (3)
- auth/oidc admin (9)
- auth/breakglass admin (4)
- auth/users mgmt (3)
- auth/runtime-config (1)
- auth/demo-residual/cleanup (1)
- audit/export (1)
- auth/logout (1)
- auth/breakglass/login (1)
- auth/oidc {login,callback,bcl} (3)
- oidc/providers/{id}/jwks-status (1)
- + 2 other auth-flow routes
Burn-down plan in 3 sprints (documented in
api/openapi-handler-exceptions.yaml header):
Sprint A: Cluster 1 — sessions + oidc admin (12 ops)
Sprint B: Cluster 2 — breakglass + users + runtime-config (8 ops)
Sprint C: Cluster 3 — audit/export + auth flows (9 ops)
This PR does NOT author the 29 OpenAPI ops; each needs request/
response schemas, not placeholders, and the design work is too
large for one PR. The reconciliation here is documentation + a CI
guard that will fail any future schema-drift, plus the scaffolding
needed for sub-phase 5b.
Sub-phase 5b: codegen scaffolding
==================================
Adds the orval scaffolding without running npm install (sandbox
disk-full; first 'npm install' + 'npm run generate' happens on the
operator's workstation):
- web/orval.config.ts — codegen config emits react-query hooks
from api/openapi.yaml into web/src/api/generated/
- web/package.json — adds orval@^7.0.0 devDep + 'generate' npm script
- web/CODEGEN.md — operator-facing migration doc:
first-time setup, per-consumer migration pattern, burn-down plan,
CI-guard rules
- scripts/ci-guards/openapi-codegen-drift.sh — blocks the build
when api/openapi.yaml changes but web/src/api/generated/ wasn't
regenerated alongside. Currently no-op (the directory doesn't
exist yet); activates from the first 'npm run generate' run.
The legacy web/src/api/client.ts stays in tree per the phase prompt's
'do not delete in same PR as codegen' rule. Consumers migrate one
page at a time as their OpenAPI ops land; client.ts deletion is a
SEPARATE follow-up PR after the last consumer migrates.
Updates to existing guard + exceptions YAML
============================================
- scripts/ci-guards/openapi-handler-parity.sh header rewritten
with the Phase 5 reconciliation numbers (220/158/64/0) and the
wire-protocol vs REST-deferred classification.
- api/openapi-handler-exceptions.yaml header rewritten with the
35/29 split + the 3-sprint burn-down plan. Each exception entry
is unchanged; the header now documents which entries are
permanent (wire-protocol) vs temporary (REST-deferred).
Sandbox limitations + operator follow-up
=========================================
- 'npm install' was NOT run from the sandbox (sessions volume
99%-full, 142 MB free). The operator runs 'cd web && npm install'
on their workstation; this lands orval@^7.0.0 in node_modules,
then 'cd web && npm run generate' produces the initial
web/src/api/generated/ tree.
- First per-consumer migration (suggested: web/src/pages/AuthSettings
or one of the operator-decision pages) lands in a follow-up PR
after npm install completes.
- The 29-op OpenAPI burn-down is a 2-sprint effort tracked under
ARCH-H1 in cowork/certctl-architecture-diligence-audit.html.
All CI guards (openapi-handler-parity, openapi-codegen-drift, plus
every existing guard) verified clean by running each individually.
Closes:
- cowork/certctl-architecture-diligence-audit.html#fix-ARCH-H1
(reconciliation: gap is 0 with exceptions accounted for; burn-down
plan documented for follow-up sprints)
- cowork/certctl-architecture-diligence-audit.html#fix-ARCH-M6
(codegen scaffolding shipped; client.ts deletion follows in a
subsequent PR after consumers migrate)
116 lines
4.6 KiB
Bash
Executable File
116 lines
4.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# scripts/ci-guards/openapi-handler-parity.sh
|
||
#
|
||
# Verify every router.Register / r.mux.Handle call has a matching
|
||
# operationId in api/openapi.yaml, modulo documented exceptions in
|
||
# api/openapi-handler-exceptions.yaml.
|
||
#
|
||
# Per ci-pipeline-cleanup bundle Phase 9 / frozen decision 0.11.
|
||
#
|
||
# Phase 5 reconciliation (2026-05-13):
|
||
# 220 r.Register call sites in internal/api/router/router.go
|
||
# 209 unique (METHOD /path) router routes after de-duplication
|
||
# 158 operationIds in api/openapi.yaml
|
||
# 64 documented exceptions in api/openapi-handler-exceptions.yaml
|
||
# 0 unaccounted router routes — every route is in OpenAPI OR
|
||
# in the exceptions YAML. Guard passes clean today.
|
||
#
|
||
# Of the 64 exceptions:
|
||
# 35 wire-protocol carve-outs (SCEP RFC 8894 = 8, ACME RFC 8555
|
||
# default + per-profile = 27). These MUST stay as exceptions —
|
||
# they're protocol contracts, not REST resources.
|
||
# 29 REST-shaped routes deferred from openapi.yaml authoring
|
||
# (auth sessions, OIDC providers admin, breakglass admin,
|
||
# users mgmt, runtime-config, demo-residual-cleanup, audit
|
||
# export). Burn-down target: author the 29 OpenAPI ops over
|
||
# the next ~2 sprints so the generated client (web/orval.config.ts)
|
||
# covers them. Tracked under ARCH-H1 in
|
||
# cowork/certctl-architecture-diligence-audit.html.
|
||
#
|
||
# Going forward: any new gap (in either direction) fails the build
|
||
# unless documented in the exceptions YAML.
|
||
|
||
set -e
|
||
|
||
python3 - <<'PY'
|
||
import re, sys, yaml
|
||
|
||
# Extract router routes: r.mux.Handle("METHOD /path", ...) and
|
||
# r.Register("METHOD /path", ...) — Go 1.22+ ServeMux pattern syntax.
|
||
with open('internal/api/router/router.go') as f:
|
||
src = f.read()
|
||
routes = []
|
||
for m in re.finditer(r'r\.(?:mux\.Handle|Register)\("([A-Z]+)\s+(/[^"]*)"', src):
|
||
routes.append((m.group(1), m.group(2)))
|
||
router_set = set(routes)
|
||
|
||
# Extract OpenAPI operations: paths × HTTP methods
|
||
with open('api/openapi.yaml') as f:
|
||
spec = yaml.safe_load(f)
|
||
oapi_set = set()
|
||
for path, methods in (spec.get('paths') or {}).items():
|
||
for method, op in methods.items():
|
||
if method.upper() in ('GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS'):
|
||
oapi_set.add((method.upper(), path))
|
||
|
||
# Extract documented exceptions
|
||
try:
|
||
with open('api/openapi-handler-exceptions.yaml') as f:
|
||
exc_doc = yaml.safe_load(f)
|
||
except FileNotFoundError:
|
||
exc_doc = {'documented_exceptions': []}
|
||
exception_set = set()
|
||
for entry in (exc_doc.get('documented_exceptions') or []):
|
||
route_str = entry['route']
|
||
parts = route_str.split(maxsplit=1)
|
||
if len(parts) == 2:
|
||
exception_set.add((parts[0], parts[1]))
|
||
|
||
# Report counts
|
||
print(f"Router routes: {len(router_set)}")
|
||
print(f"OpenAPI operations: {len(oapi_set)}")
|
||
print(f"Documented exceptions: {len(exception_set)}")
|
||
print()
|
||
|
||
fail = False
|
||
|
||
# Routes in router but NOT in openapi AND NOT in exceptions = drift
|
||
router_only_undocumented = router_set - oapi_set - exception_set
|
||
if router_only_undocumented:
|
||
print(f"::error::OpenAPI ↔ handler drift: {len(router_only_undocumented)} router routes have no OpenAPI operationId AND are not in api/openapi-handler-exceptions.yaml:")
|
||
for m, p in sorted(router_only_undocumented):
|
||
print(f" {m:6} {p}")
|
||
print()
|
||
print("Either:")
|
||
print(" (a) Add the operationId to api/openapi.yaml (preferred for REST endpoints), OR")
|
||
print(" (b) Add the route to api/openapi-handler-exceptions.yaml with a one-line `why:` justification")
|
||
print(" (only for protocol-shaped or operational routes — health probes,")
|
||
print(" Prometheus scrape, SCEP/EST/OCSP wire-protocol endpoints, etc.).")
|
||
fail = True
|
||
|
||
# Routes in openapi but NOT in router = orphan operationId
|
||
oapi_only = oapi_set - router_set
|
||
if oapi_only:
|
||
print(f"::error::OpenAPI ↔ handler drift: {len(oapi_only)} OpenAPI operations have no router registration:")
|
||
for m, p in sorted(oapi_only):
|
||
print(f" {m:6} {p}")
|
||
print()
|
||
print("Either delete the operationId from api/openapi.yaml, OR add the missing")
|
||
print("router registration in internal/api/router/router.go.")
|
||
fail = True
|
||
|
||
# Exceptions that don't match any router route = stale exception
|
||
stale_exceptions = exception_set - router_set
|
||
if stale_exceptions:
|
||
print(f"::error::Stale exceptions in api/openapi-handler-exceptions.yaml — these routes are not in the router:")
|
||
for m, p in sorted(stale_exceptions):
|
||
print(f" {m:6} {p}")
|
||
print()
|
||
print("Remove the stale entry from api/openapi-handler-exceptions.yaml.")
|
||
fail = True
|
||
|
||
if fail:
|
||
sys.exit(1)
|
||
print("openapi-handler-parity: clean.")
|
||
PY
|