mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 23:39:00 +00:00
ci-pipeline-cleanup Phases 7-9: image-and-supply-chain job
Bundle: ci-pipeline-cleanup, Phases 7-9 / frozen decisions 0.8 + 0.10 + 0.11.
NEW image-and-supply-chain job (Ubuntu, ~3 min). Three steps:
PHASE 7 — Digest validity
scripts/ci-guards/digest-validity.sh resolves every @sha256:<digest>
ref in deploy/**/*.{yml,Dockerfile*} against its registry. Closes the
H-001 lying-field gap that Bundle II hit (11 fabricated digests passed
H-001's regex-only check and failed docker pull in CI).
Sandbox verification: 16/16 digests in deploy/* + Dockerfiles all
return HTTP 200 from registry-1.docker.io / ghcr.io / mcr.microsoft.com.
PHASE 8 — Docker build smoke (all 4 Dockerfiles)
Per frozen decision 0.10: build Dockerfile, Dockerfile.agent,
deploy/test/f5-mock-icontrol/Dockerfile, deploy/test/libest/Dockerfile.
Catches syntax errors + COPY path drift before tag-time release.yml.
The test-sidecar Dockerfiles are load-bearing for vendor-e2e — a
syntax error there silently breaks the e2e suite.
PHASE 9 — OpenAPI ↔ handler operationId parity
scripts/ci-guards/openapi-handler-parity.sh extracts router routes
(r.mux.Handle / r.Register "METHOD /path" syntax — Go 1.22+ ServeMux),
extracts OpenAPI operations (paths × HTTP methods), and fails if any
router route has no operationId AND is not documented in the new
api/openapi-handler-exceptions.yaml.
Verified gap at HEAD 1de61e91 (root-caused):
142 router routes, 136 OpenAPI operations
6 router-only routes — all SCEP wire-protocol endpoints (RFC-shaped,
not REST). Documented in api/openapi-handler-exceptions.yaml with
one-line why: justifications.
0 OpenAPI-only operations.
Going forward: any new gap fails the build unless documented.
Status checks per push: now 7 (was 8 after Phase 5+6 dropped windows;
this Phase adds 1 = +1 net). Final acceptance gate target.
ci.yml: 383 → 432 lines (+49 for the new job + steps).
This commit is contained in:
Executable
+103
@@ -0,0 +1,103 @@
|
||||
#!/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.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# All 6 router-only routes are documented as legitimate exceptions in
|
||||
# api/openapi-handler-exceptions.yaml.
|
||||
#
|
||||
# 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
|
||||
Reference in New Issue
Block a user