feat(ci): catalogue-drift gate for @RequirePrivilege/@RequireRole literals (ADR-0025)
CI / commits (pull_request) Successful in 2m18s
CI / check (pull_request) Successful in 2m57s
CI / scan (pull_request) Successful in 3m5s
CI / a11y (pull_request) Successful in 3m11s
Docs site / build (pull_request) Successful in 3m24s
CI / perf (pull_request) Successful in 6m57s

Phase 3 of the ADR-0025 phasing per its §"More Information" §347:
a small CI gate that asserts every string literal passed to the
authorization decorators belongs to the closed catalogue declared
in libs/shared/auth/src/lib/authorization.types.ts.

The TypeScript signature on the decorators already enforces this
at compile time — the new gate is defence-in-depth against escape
hatches (`as Privilege` casts, hand-edits to the type union
without updating the runtime constant). It runs in <1s, so it
rides the existing `check` CI job rather than starting its own.

Implementation:
- scripts/check-catalogue-drift.mjs: TypeScript compiler API.
  Parses authorization.types.ts to extract the catalogue arrays,
  walks every .ts file under apps/ and libs/, finds each
  CallExpression whose callee identifier matches @RequirePrivilege
  or @RequireRole, validates every string-literal argument.
  Non-literal args (variable indirection) are skipped on purpose —
  the TypeScript signature catches them at compile time and the
  gate's value-add is on literal misspellings. Generated gRPC
  stubs under apps/portal-bff/src/grpc/gen are skipped (their
  `roles[]` field is a wire-format unrelated to the catalogue).
  Reports grouped by file with line:column for each violation,
  exits 1 on drift.
- scripts/check-catalogue-drift.spec.mjs: 13 tests via node:test
  (built-in, no Vitest dependency). Covers catalogue parsing,
  file-level violation detection, workspace-wide aggregation,
  skipped folders, gen-stub exclusion, line/column reporting.
- package.json: ci:catalogue-drift + ci:catalogue-drift:test
  scripts.
- .gitea/workflows/ci.yml: the `check` job now runs the gate's
  unit tests (first, to fail fast if the gate itself is broken)
  then the gate against the live workspace.

Self-tested by injecting `RequirePrivilege('Portal.RogueDrift')`
and `RequireRole('rogue-role-x')` into an existing spec — both
caught with file:line:column and exit code 1.

Test plan:
- pnpm ci:catalogue-drift:test: 13/13 green.
- pnpm ci:catalogue-drift: clean (4 privileges, 24 roles).
- pnpm nx affected -t format:check lint test build: no project affected (script lives outside Nx).
This commit is contained in:
Julien Gautier
2026-05-24 00:59:57 +02:00
parent 77af6951c9
commit f9a061e6f8
4 changed files with 517 additions and 0 deletions
+9
View File
@@ -44,6 +44,15 @@ jobs:
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- run: pnpm ci:check
# Catalogue-vs-code drift gate per ADR-0025 §"Confirmation".
# Cheap (parses ~workspace .ts files via the TypeScript
# compiler API, ~1s on a warm cache) so it rides the same
# `check` job rather than spinning up a new one. The
# script's own unit tests run first — if they fail the gate
# itself is broken and the actual catalogue check would be
# noise.
- run: pnpm ci:catalogue-drift:test
- run: pnpm ci:catalogue-drift
scan:
runs-on: [self-hosted, on-prem]