ci: OpenAPI parity reconciliation + codegen scaffolding (Phase 5 — ARCH-H1 / ARCH-M6)

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)
This commit is contained in:
shankar0123
2026-05-13 20:24:20 +00:00
parent 1383fe419b
commit 3c81531398
6 changed files with 314 additions and 7 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# scripts/ci-guards/openapi-codegen-drift.sh
#
# Phase 5 ARCH-M6 scaffolding (2026-05-13): block the build when
# api/openapi.yaml changes but web/src/api/generated/ wasn't
# regenerated alongside. The generated tree is git-tracked; running
# `cd web && npm run generate` regenerates from api/openapi.yaml.
#
# Guard logic:
#
# 1. If web/src/api/generated/ does NOT exist yet, do nothing.
# This phase ships the orval.config.ts scaffolding without
# running `npm install orval` from the sandbox (disk-full); the
# first operator-run of `npm run generate` creates the directory
# and the guard activates from that point forward.
#
# 2. If web/src/api/generated/ exists:
# - Regenerate into a tmp dir using `npm run generate`
# (requires orval to be installed locally).
# - Diff against the tracked tree.
# - Fail the build with a clear regenerate-command pointer.
#
# Note: this guard requires Node + npm to be available on the CI
# runner. The frontend job in ci.yml already provisions both
# (.github/workflows/ci.yml frontend-build), so wiring is mechanical.
# Run order matters: this guard must run AFTER `npm ci` in the
# frontend job so orval is in node_modules.
set -e
GENERATED_DIR="web/src/api/generated"
if [ ! -d "$GENERATED_DIR" ]; then
echo "openapi-codegen-drift: skipped — $GENERATED_DIR does not exist yet."
echo " This is expected during Phase 5 scaffolding. Once the operator"
echo " runs 'cd web && npm install && npm run generate' for the first"
echo " time, the directory lands and this guard activates."
exit 0
fi
# Tolerate the case where orval isn't installed in the local
# environment — in that case the guard is informational. The CI
# pipeline activates it once the frontend job runs `npm ci`.
if [ ! -f "web/node_modules/.bin/orval" ]; then
echo "openapi-codegen-drift: skipped — web/node_modules/.bin/orval not present."
echo " Run 'cd web && npm ci' to install. CI runs npm ci before this guard."
exit 0
fi
# Snapshot the tracked tree, regenerate into a tmpdir, diff.
TMPGEN="$(mktemp -d -t orval-drift.XXXXXX)"
trap 'rm -rf "$TMPGEN"' EXIT
# Copy the tracked tree so we can compare against a fresh regeneration.
cp -r "$GENERATED_DIR" "$TMPGEN/tracked"
# Regenerate in-place; orval honors orval.config.ts output paths.
(cd web && npm run generate --silent) >/dev/null
# Diff the tracked tree against the freshly-regenerated tree.
if diff -r --brief "$TMPGEN/tracked" "$GENERATED_DIR" >/dev/null 2>&1; then
echo "openapi-codegen-drift: clean — generated client matches openapi.yaml"
# Restore the tracked tree (regeneration overwrites it; restore so
# the working tree is back to the tracked state).
rm -rf "$GENERATED_DIR"
cp -r "$TMPGEN/tracked" "$GENERATED_DIR"
exit 0
fi
echo "::error::openapi-codegen-drift regression: $GENERATED_DIR is stale."
echo ""
echo "api/openapi.yaml changed but the generated client tree wasn't"
echo "regenerated alongside. Regenerate with:"
echo ""
echo " cd web && npm run generate"
echo ""
echo "Then commit the updated $GENERATED_DIR/ alongside the openapi.yaml"
echo "change in this PR."
echo ""
echo "Diff (- tracked, + regenerated):"
diff -r "$TMPGEN/tracked" "$GENERATED_DIR" | head -80
# Restore tracked tree so the working tree isn't surprising.
rm -rf "$GENERATED_DIR"
cp -r "$TMPGEN/tracked" "$GENERATED_DIR"
exit 1
+18 -6
View File
@@ -7,13 +7,25 @@
#
# Per ci-pipeline-cleanup bundle Phase 9 / frozen decision 0.11.
#
# Verified gap at HEAD 1de61e91 (after root-cause):
# 142 router routes vs 136 OpenAPI operations
# 6 router-only routes (all SCEP wire-protocol endpoints)
# 0 OpenAPI-only operations
# 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.
#
# All 6 router-only routes are documented as legitimate exceptions in
# api/openapi-handler-exceptions.yaml.
# 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.