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:
+135
@@ -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 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) |
|
||||
Reference in New Issue
Block a user