U5 — the gate everything else has to pass

continuous_integration.yaml: six blocking checks, then a separate
publish per environment so a Vite build never gets tagged for the
wrong one, then a call into last commit's deploy workflow. Along the
way: the lint list had drifted (two problems not in the requirement,
one already fixed), and the Umami-origin gate needed a variable pair
of its own since the backend's side of that comparison lives on the
host, not in CI. Pinned the two vulnerable packages while at it.
This commit is contained in:
2026-07-28 16:07:03 +02:00
parent bd2a963498
commit 9f4ae475e7
12 changed files with 691 additions and 36 deletions
@@ -0,0 +1,135 @@
# 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 7486) 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 12's verification plus this unit's own build/test check (Step 13.5) |