# Code Generation Plan — U5 CI Workflow & Quality Gates ## Unit Context - **Unit**: U5 CI Workflow & Quality Gates (Pipeline-type — YAML + prerequisite frontend/package fixes, no new C# business logic) - **Component**: C-12 CI workflow (`.gitea/workflows/continuous_integration.yaml`) - **Requirements**: FR-01, FR-04, FR-05, FR-21, FR-22 - **Carried-in item**: REF-U3-01 — a blocking gate comparing `VITE_UMAMI_SCRIPT_URL` against that environment's `SecurityHeaders:AllowedScriptOrigins`, since the backend cannot see the frontend's build-time Umami variable itself - **Depends on**: U3, U4 (both committed — gates must run against finished application code, per `unit-of-work-dependency.md` Ordering Constraint 4) - **Depended on by**: U6 (invoked by this workflow's `deploy-test` / `deploy-production` jobs) - **No test project applies**: same category as U6 — `.gitea/workflows/` is pipeline configuration. The lint fixes and package pins ARE ordinary application/frontend code changes and are covered by the existing backend test suite and frontend lint/type-check, not a new test project. ## Note on sequencing (transparency) Two of this plan's steps — the lint fixes (FR-21) and the package pins (FR-22) — were investigated and completed during initial analysis, before this plan document existed, because they required running the actual toolchain (`pnpm run lint`, `dotnet list package --vulnerable`, live NuGet advisory lookups) to discover the *current* facts (exact files/lines, exact patched versions) rather than anything design-level. They are recorded here as completed steps for full traceability, and are already build/test-verified (see Step 13.5 record below). **Step 3 — the CI workflow YAML itself — is the step awaiting your review before being written.** ## Steps - [x] Step 1: Fix the 5 lint errors + 1 warning (FR-21) - `AddCmsInstanceDialog.tsx:55` and `InviteUserDialog.tsx:50` (`react-hooks/set-state-in-effect`): replaced the close-triggered `useEffect` with a `handleOpenChange` wrapper around the `Dialog`'s `onOpenChange`, since resetting state on close is an event response, not a render synchronization - `InviteUserDialog.tsx:54` (`react-hooks/immutability`, `reset` used before its declaration): resolved as part of the same change — `useForm()` now runs before the effect it used to feed - `SettingsPage.tsx:40` (`react-hooks/set-state-in-effect`): replaced with the "adjust state during render" pattern (compare fetched `availability` against a tracked previous value, call `setState` directly in the render body) — the React-documented alternative when state is derived from changing data rather than a user action - `SetStatusDialog.tsx:72` (warning, `react-hooks/incompatible-library` on `watch()`): replaced `watch('status')` with `useWatch({ control, name: 'status' })`, react-hook-form's own compiler-compatible alternative - **Drift found during analysis, fixed as part of the same requirement, not filed**: current `pnpm run lint` no longer matched FR-21's original 6-item list exactly — `SetStatusDialog.tsx:32` from the requirement was already clean, but two problems FR-21 did not list were present: `badge.tsx:32` (`react-refresh/only-export-components`, fixed identically to the existing precedent in `button.tsx` — an inline eslint-disable, not a file split, to match established convention) and a second, previously-hidden effect in `SetStatusDialog.tsx` (lines 74–86) that the React Compiler's lint plugin had not been able to analyze until the `watch()` incompatibility above was resolved. Fixed with the same render-time-sync pattern as `SettingsPage.tsx`, split so the close-triggered reset lives in `handleOpenChange` and the instance-driven reset lives in the render-time sync - Result: `pnpm run lint` — 0 problems - [x] Step 2: Pin vulnerable packages (FR-22, closing OPEN-03) - `Microsoft.OpenApi` 2.0.0 (transitive via `Microsoft.AspNetCore.OpenApi` 10.0.9, high severity, GHSA-v5pm-xwqc-g5wc) → pinned to `2.11.0` in `SlpModularCms.Core.csproj` (latest 2.x; patched at 2.7.5+, deliberately not jumping to the 3.x major that `Microsoft.AspNetCore.OpenApi` 10.0.9 does not target) - `System.Security.Cryptography.Xml` 10.0.9 (transitive via `Microsoft.AspNetCore.DataProtection`, five high-severity advisories, all patched at 10.0.10) → pinned to `10.0.10` in `SlpModularCms.Core.csproj`, plus independently in `SlpModularCms.Modules.Master.Tests.csproj` and `SlpModularCms.Modules.Availability.Tests.csproj`, which each carry their own direct `Microsoft.AspNetCore.DataProtection` reference for testing and therefore have their own unpinned path to the vulnerable transitive version that Core's pin alone does not reach - Result: `dotnet list SlpModularCms.sln package --vulnerable --include-transitive` — clean across all 10 projects - [x] Step 3: Generate `.gitea/workflows/continuous_integration.yaml` - Triggers: `pull_request` (opened/synchronize/reopened, validation only), `push` to `master` (validation + automatic test deploy), `workflow_dispatch` with `deploy_production` boolean (default `false`) - `config` job: turns environment-specific `env:` values into job outputs (same reason as the reference project — the `env` context is unavailable inside a called reusable workflow's `with:` block) - Six blocking gates (FR-01, D-10): backend build (`dotnet build -c Release`), backend tests (`dotnet test -c Release`), vulnerability scan (`dotnet list package --vulnerable`, now clean per Step 2), frontend build, frontend tests, frontend lint/format-check (now clean per Step 1) - **REF-U3-01 gate — a mechanism decision this unit must make, not just implement**: the pattern doc says the CI job compares "both values" (frontend's `VITE_UMAMI_SCRIPT_URL` vs. backend's `SecurityHeaders:AllowedScriptOrigins`), but per D-16 the live backend value is a **host** environment variable the Gitea runner cannot read — no `appsettings.{Environment}.json` exists; there's only the single, environment-agnostic `appsettings.json`. Reading the real runtime value is therefore not possible from CI, the exact problem this gate exists to solve, one level up. **Resolution (new, flagged as REF-U5-01)**: add a second pair of Gitea Actions variables, `SECURITY_ALLOWED_SCRIPT_ORIGINS_TEST` / `_PRODUCTION`, that mirror the intended host env var value and are compared against `VITE_UMAMI_SCRIPT_URL_TEST` / `_PRODUCTION` at CI time. This makes the gate implementable today, at the cost of a second place that must be kept in sync with the host's actual `SecurityHeaders__AllowedScriptOrigins__0` env var — carried to Operations host-setup documentation as an explicit "these two must match" note, not silently assumed - **Refinement during generation**: `VITE_UMAMI_SCRIPT_URL` is one shared Gitea variable for both environments (D-24 — one Umami instance), not a `_TEST`/`_PRODUCTION` pair; only `VITE_UMAMI_WEBSITE_ID` differs per environment, matching the reference project's exact pattern. The REF-U5-01 gate therefore compares the same origin against each environment's own `SECURITY_ALLOWED_SCRIPT_ORIGINS_*` variable — two gate runs, one shared input - **Artifact clarification during generation**: the "frontend build" gate (`pnpm run build`) is a standalone PR-time validation, independent of environment. The actual per-environment deployable artifact is a full `dotnet publish` of `SlpModularCms.Api` (`publish-test` / `publish-production` jobs), which triggers the admin SPA's build a second time internally via the existing `BuildAndCopyAdminFrontend` MSBuild target — the environment's `VITE_*` variables are set on that step's process environment, not passed as build arguments, since that target invokes `pnpm build` directly - Two environment-specific publish artifacts (FR-05): `publish-test` (`VITE_APP_ENV=test`) and `publish-production` (gated on `workflow_dispatch` with `deploy_production: true`), each with its own `VITE_UMAMI_WEBSITE_ID`, shared `VITE_SENTRY_DSN` and `VITE_UMAMI_SCRIPT_URL` - Toolchain pinned explicitly (`actions/setup-dotnet`, `pnpm/action-setup`), no `latest` tags (D-04) - `deploy-test` / `deploy-production` jobs call `./.gitea/workflows/deploy-scp.yaml` (U6) with `secrets: inherit`, supplying `artifact_name`, `environment`, `deploy_path`, `service_name`, `health_check_url` from Gitea variables per `infrastructure-design.md` § 6, plus `run_db_backup: true` only for `deploy-production` - Production reachable only via `workflow_dispatch` with the flag set (FR-04) — `deploy-test` never triggers a production deploy under any push condition - [x] Step 4: Validate YAML - Parsed successfully: 12 jobs, `deploy-production`'s condition requires `workflow_dispatch` AND `deploy_production == 'true'` — confirmed unreachable from a bare `push` event (FR-04). Job graph: `config`/`backend-build`/`vulnerability-scan`/`frontend-prepare` run independently → `backend-test` needs `backend-build` → `frontend-build`/`frontend-test`/ `frontend-lint` need `frontend-prepare` → `publish-test`/`publish-production` need all six gates → `deploy-test`/`deploy-production` need their publish job plus `config` - [x] Step 5: Documentation — `construction/u5-ci-workflow/code/generation-summary.md` ## Story / Requirement Traceability | Step | Covers | |---|---| | 1 | FR-21 | | 2 | FR-22, OPEN-03 | | 3 | FR-01, FR-04, FR-05, REF-U3-01 | | 4 | Definition of Done: "workflow YAML is syntactically valid" | | 5 | Definition of Done: "all six gates pass locally against the current tree" — recorded from Steps 1–2's verification plus this unit's own build/test check (Step 13.5) |