mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 11:01:31 +00:00
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:
@@ -7,6 +7,24 @@
|
||||
# (health, metrics, pprof) routes only.
|
||||
#
|
||||
# Per ci-pipeline-cleanup bundle Phase 9 / frozen decision 0.11.
|
||||
#
|
||||
# Phase 5 reconciliation (2026-05-13, architecture diligence audit
|
||||
# ARCH-H1): of the 64 entries below, 35 are legitimate wire-protocol
|
||||
# carve-outs (SCEP RFC 8894 = 8 entries, ACME RFC 8555 default + per-
|
||||
# profile = 27 entries) that MUST stay. 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. Burn-down plan:
|
||||
#
|
||||
# Sprint A (per-cluster, ~7-8 ops each):
|
||||
# Cluster 1: auth/sessions + auth/oidc (12 ops)
|
||||
# Cluster 2: auth/breakglass + auth/users + auth/runtime-config (8 ops)
|
||||
# Cluster 3: audit/export + demo-residual/cleanup + auth/logout +
|
||||
# auth/breakglass/login + auth/oidc/{login,callback,bcl} (9 ops)
|
||||
#
|
||||
# Each authored OpenAPI op needs request/response schemas (not
|
||||
# placeholders) so the generated client at web/orval.config.ts emits
|
||||
# typed signatures. When an op lands, delete the corresponding entry
|
||||
# below + bump the openapi-handler-parity.sh expected counts.
|
||||
|
||||
documented_exceptions:
|
||||
- route: "GET /scep"
|
||||
|
||||
Executable
+86
@@ -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
|
||||
@@ -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.
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
# Generated API Client
|
||||
|
||||
> Last reviewed: 2026-05-13
|
||||
|
||||
Phase 5 of the certctl architecture diligence remediation introduced
|
||||
orval-based code generation for the frontend API client. The
|
||||
hand-rolled `web/src/api/client.ts` (1,396 lines, 161 exported
|
||||
functions) is staged to be retired in favor of the generated
|
||||
TanStack-Query-shaped surface emitted by orval from
|
||||
`api/openapi.yaml`.
|
||||
|
||||
## Where things live
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `api/openapi.yaml` | Source of truth — 158 operations |
|
||||
| `api/openapi-handler-exceptions.yaml` | 64 routes intentionally NOT in OpenAPI (35 wire-protocol carve-outs + 29 REST-deferred) |
|
||||
| `web/orval.config.ts` | Codegen config — emits `react-query` hooks |
|
||||
| `web/src/api/generated/` | Output tree (regenerated, git-tracked) |
|
||||
| `web/src/api/client.ts` | Legacy hand-rolled client (TO BE DELETED in follow-up PR) |
|
||||
| `web/src/api/mutator.ts` | Fetch wrapper used by the generated client (CSRF, auth) |
|
||||
| `scripts/ci-guards/openapi-handler-parity.sh` | Verifies every router route is in OpenAPI OR exceptions |
|
||||
| `scripts/ci-guards/openapi-codegen-drift.sh` | Blocks the build when openapi.yaml changes but generated/ wasn't regenerated |
|
||||
|
||||
## First-time setup
|
||||
|
||||
Run from the repo root:
|
||||
|
||||
```bash
|
||||
cd web
|
||||
npm install # installs orval as a devDep
|
||||
npm run generate # regenerates web/src/api/generated/
|
||||
git add web/src/api/generated/ web/src/api/mutator.ts
|
||||
git commit -m "feat(web): initial generated API client"
|
||||
```
|
||||
|
||||
The mutator at `web/src/api/mutator.ts` is operator-authored (orval
|
||||
references it from `orval.config.ts`); it must export a
|
||||
`certctlFetch<T>(config: AxiosRequestConfig): Promise<T>` function
|
||||
that the generated code calls for every HTTP request. The mutator is
|
||||
where CSRF + bearer-token + retry policy + 401-redirect logic lives
|
||||
in one place.
|
||||
|
||||
## Migration pattern (per consumer)
|
||||
|
||||
The generated client emits one hook per OpenAPI operation. Migrate
|
||||
consumers one page at a time; the hand-rolled client and generated
|
||||
client coexist until the last consumer migrates.
|
||||
|
||||
```tsx
|
||||
// Legacy (web/src/api/client.ts → web/src/pages/CertificatesPage.tsx):
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getCertificates } from '../api/client';
|
||||
|
||||
const certs = useQuery({
|
||||
queryKey: ['certificates'],
|
||||
queryFn: getCertificates,
|
||||
});
|
||||
|
||||
// Generated (web/src/api/generated/certificates/certificates.ts):
|
||||
import { useGetCertificates } from '../api/generated/certificates/certificates';
|
||||
|
||||
const certs = useGetCertificates(); // wires queryKey + queryFn automatically
|
||||
```
|
||||
|
||||
The generated `useGetCertificates()` honors the QueryClient defaults
|
||||
set in `main.tsx` (frontend-design-audit Phase 2 TQ-H2 / TQ-M1 tier
|
||||
model). Per-call overrides (`staleTime`, `refetchInterval`, etc.) pass
|
||||
through as the second argument:
|
||||
|
||||
```tsx
|
||||
const certs = useGetCertificates({
|
||||
query: { staleTime: STALE_TIME.REAL_TIME },
|
||||
});
|
||||
```
|
||||
|
||||
## When the OpenAPI burn-down completes
|
||||
|
||||
Today 29 router routes are deferred from `openapi.yaml` (see the
|
||||
"REST-shaped" group in `api/openapi-handler-exceptions.yaml`). The
|
||||
generated client only covers what's in `openapi.yaml` — those 29
|
||||
routes still go through `web/src/api/client.ts` until their OpenAPI
|
||||
ops land. The burn-down plan:
|
||||
|
||||
```
|
||||
Sprint A — Cluster 1 (auth/sessions + auth/oidc): 12 ops
|
||||
Sprint B — Cluster 2 (auth/breakglass + auth/users + runtime-config): 8 ops
|
||||
Sprint C — Cluster 3 (auth/logout + audit/export + misc): 9 ops
|
||||
```
|
||||
|
||||
After Sprint C, every router route is either an OpenAPI operation or
|
||||
a wire-protocol carve-out. The last consumer migrates off
|
||||
`web/src/api/client.ts` and the file gets deleted in a follow-up PR.
|
||||
|
||||
## CI guards
|
||||
|
||||
- `openapi-handler-parity.sh` blocks any new router route that isn't
|
||||
in `openapi.yaml` AND isn't in the exceptions YAML.
|
||||
- `openapi-codegen-drift.sh` blocks any `openapi.yaml` change that
|
||||
doesn't regenerate `web/src/api/generated/` alongside.
|
||||
|
||||
Both run automatically as part of the per-PR CI guard sweep at
|
||||
`.github/workflows/ci.yml`.
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Phase 5 ARCH-M6 scaffolding (2026-05-13).
|
||||
*
|
||||
* Orval config for the certctl frontend. Reads ../api/openapi.yaml as
|
||||
* the source of truth and emits a TanStack-Query-shaped client into
|
||||
* web/src/api/generated/. Output is git-tracked; the
|
||||
* scripts/ci-guards/openapi-codegen-drift.sh guard fails the build
|
||||
* when api/openapi.yaml changes but the generated/ tree wasn't
|
||||
* regenerated alongside.
|
||||
*
|
||||
* Pairs with frontend-design-audit Phase 2 (TanStack tier model). The
|
||||
* generated client honors the staleTime / gcTime / refetchOnWindowFocus
|
||||
* shape that the Phase 2 work establishes at the QueryClient level.
|
||||
*
|
||||
* Sub-phase 5b status (2026-05-13):
|
||||
* - This config exists.
|
||||
* - 'npm install' for @hey-api/openapi-ts + orval was NOT run from
|
||||
* the audit sandbox (disk-full); operator runs:
|
||||
* cd web && npm install -D orval@^7.0.0
|
||||
* cd web && npm run generate
|
||||
* in the first follow-up sprint. The generated tree lands as
|
||||
* web/src/api/generated/{certctl.ts,certctl.msw.ts,model/*.ts}.
|
||||
* - client.ts deletion is a SEPARATE follow-on PR after consumers
|
||||
* migrate (per phase prompt's "do not delete in same PR" rule).
|
||||
*
|
||||
* Migration pattern (per-consumer):
|
||||
*
|
||||
* - // legacy:
|
||||
* - import { getCertificates } from '../api/client';
|
||||
* + import { useGetCertificates } from '../api/generated/certctl';
|
||||
* - const certs = useQuery({ queryKey: ['certificates'], queryFn: getCertificates });
|
||||
* + const certs = useGetCertificates();
|
||||
*
|
||||
* The generated hook uses the QueryClient defaults — no manual
|
||||
* queryKey / queryFn plumbing. Phase 2 of the frontend audit
|
||||
* (TanStack tier model) consumes this surface cleanly.
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'orval';
|
||||
|
||||
export default defineConfig({
|
||||
certctl: {
|
||||
input: {
|
||||
// Source-of-truth OpenAPI doc. Co-located one directory up.
|
||||
target: '../api/openapi.yaml',
|
||||
},
|
||||
output: {
|
||||
// All generated code lives under web/src/api/generated/.
|
||||
// The directory is git-tracked; the codegen-drift CI guard fails
|
||||
// if api/openapi.yaml changes and the generated tree wasn't
|
||||
// regenerated alongside.
|
||||
target: './src/api/generated/certctl.ts',
|
||||
schemas: './src/api/generated/model',
|
||||
// 'react-query' client emits per-operation useGet / useMutation
|
||||
// hooks wired to TanStack Query.
|
||||
client: 'react-query',
|
||||
mode: 'tags-split',
|
||||
// Prettier the output so diffs are reviewable.
|
||||
prettier: true,
|
||||
// Mock file (MSW) NOT generated by default — the Vitest tests
|
||||
// mock the api/client surface directly today; we don't want
|
||||
// the codegen to introduce a parallel mock convention until
|
||||
// the migration completes.
|
||||
mock: false,
|
||||
override: {
|
||||
// Use fetch via a shared mutator so we can wire CSRF + auth
|
||||
// headers in one place. The mutator file lives at
|
||||
// web/src/api/mutator.ts (lands in the same PR that runs
|
||||
// 'npm run generate' the first time).
|
||||
mutator: {
|
||||
path: './src/api/mutator.ts',
|
||||
name: 'certctlFetch',
|
||||
},
|
||||
// Use AbortController across the codegen surface so
|
||||
// page-unmount cancels in-flight requests cleanly.
|
||||
query: {
|
||||
useQuery: true,
|
||||
useMutation: true,
|
||||
// Per-query options resolved from the QueryClient defaults
|
||||
// set in main.tsx (frontend audit Phase 2 TQ-H2 / TQ-M1
|
||||
// tier model).
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
+3
-1
@@ -10,7 +10,8 @@
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"e2e": "playwright test",
|
||||
"e2e:install": "playwright install --with-deps chromium"
|
||||
"e2e:install": "playwright install --with-deps chromium",
|
||||
"generate": "orval --config ./orval.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "^5.90.21",
|
||||
@@ -23,6 +24,7 @@
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"orval": "^7.0.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
|
||||
Reference in New Issue
Block a user