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.
9.7 KiB
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_URLagainst that environment'sSecurityHeaders: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.mdOrdering Constraint 4) - Depended on by: U6 (invoked by this workflow's
deploy-test/deploy-productionjobs) - 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
-
Step 1: Fix the 5 lint errors + 1 warning (FR-21)
AddCmsInstanceDialog.tsx:55andInviteUserDialog.tsx:50(react-hooks/set-state-in-effect): replaced the close-triggereduseEffectwith ahandleOpenChangewrapper around theDialog'sonOpenChange, since resetting state on close is an event response, not a render synchronizationInviteUserDialog.tsx:54(react-hooks/immutability,resetused before its declaration): resolved as part of the same change —useForm()now runs before the effect it used to feedSettingsPage.tsx:40(react-hooks/set-state-in-effect): replaced with the "adjust state during render" pattern (compare fetchedavailabilityagainst a tracked previous value, callsetStatedirectly in the render body) — the React-documented alternative when state is derived from changing data rather than a user actionSetStatusDialog.tsx:72(warning,react-hooks/incompatible-libraryonwatch()): replacedwatch('status')withuseWatch({ 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 lintno longer matched FR-21's original 6-item list exactly —SetStatusDialog.tsx:32from 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 inbutton.tsx— an inline eslint-disable, not a file split, to match established convention) and a second, previously-hidden effect inSetStatusDialog.tsx(lines 74–86) that the React Compiler's lint plugin had not been able to analyze until thewatch()incompatibility above was resolved. Fixed with the same render-time-sync pattern asSettingsPage.tsx, split so the close-triggered reset lives inhandleOpenChangeand the instance-driven reset lives in the render-time sync - Result:
pnpm run lint— 0 problems
-
Step 2: Pin vulnerable packages (FR-22, closing OPEN-03)
Microsoft.OpenApi2.0.0 (transitive viaMicrosoft.AspNetCore.OpenApi10.0.9, high severity, GHSA-v5pm-xwqc-g5wc) → pinned to2.11.0inSlpModularCms.Core.csproj(latest 2.x; patched at 2.7.5+, deliberately not jumping to the 3.x major thatMicrosoft.AspNetCore.OpenApi10.0.9 does not target)System.Security.Cryptography.Xml10.0.9 (transitive viaMicrosoft.AspNetCore.DataProtection, five high-severity advisories, all patched at 10.0.10) → pinned to10.0.10inSlpModularCms.Core.csproj, plus independently inSlpModularCms.Modules.Master.Tests.csprojandSlpModularCms.Modules.Availability.Tests.csproj, which each carry their own directMicrosoft.AspNetCore.DataProtectionreference 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
-
Step 3: Generate
.gitea/workflows/continuous_integration.yaml- Triggers:
pull_request(opened/synchronize/reopened, validation only),pushtomaster(validation + automatic test deploy),workflow_dispatchwithdeploy_productionboolean (defaultfalse) configjob: turns environment-specificenv:values into job outputs (same reason as the reference project — theenvcontext is unavailable inside a called reusable workflow'swith: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_URLvs. backend'sSecurityHeaders:AllowedScriptOrigins), but per D-16 the live backend value is a host environment variable the Gitea runner cannot read — noappsettings.{Environment}.jsonexists; there's only the single, environment-agnosticappsettings.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 againstVITE_UMAMI_SCRIPT_URL_TEST/_PRODUCTIONat 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 actualSecurityHeaders__AllowedScriptOrigins__0env var — carried to Operations host-setup documentation as an explicit "these two must match" note, not silently assumed - Refinement during generation:
VITE_UMAMI_SCRIPT_URLis one shared Gitea variable for both environments (D-24 — one Umami instance), not a_TEST/_PRODUCTIONpair; onlyVITE_UMAMI_WEBSITE_IDdiffers per environment, matching the reference project's exact pattern. The REF-U5-01 gate therefore compares the same origin against each environment's ownSECURITY_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 fulldotnet publishofSlpModularCms.Api(publish-test/publish-productionjobs), which triggers the admin SPA's build a second time internally via the existingBuildAndCopyAdminFrontendMSBuild target — the environment'sVITE_*variables are set on that step's process environment, not passed as build arguments, since that target invokespnpm builddirectly - Two environment-specific publish artifacts (FR-05):
publish-test(VITE_APP_ENV=test) andpublish-production(gated onworkflow_dispatchwithdeploy_production: true), each with its ownVITE_UMAMI_WEBSITE_ID, sharedVITE_SENTRY_DSNandVITE_UMAMI_SCRIPT_URL - Toolchain pinned explicitly (
actions/setup-dotnet,pnpm/action-setup), nolatesttags (D-04) deploy-test/deploy-productionjobs call./.gitea/workflows/deploy-scp.yaml(U6) withsecrets: inherit, supplyingartifact_name,environment,deploy_path,service_name,health_check_urlfrom Gitea variables perinfrastructure-design.md§ 6, plusrun_db_backup: trueonly fordeploy-production- Production reachable only via
workflow_dispatchwith the flag set (FR-04) —deploy-testnever triggers a production deploy under any push condition
- Triggers:
-
Step 4: Validate YAML
- Parsed successfully: 12 jobs,
deploy-production's condition requiresworkflow_dispatchANDdeploy_production == 'true'— confirmed unreachable from a barepushevent (FR-04). Job graph:config/backend-build/vulnerability-scan/frontend-preparerun independently →backend-testneedsbackend-build→frontend-build/frontend-test/frontend-lintneedfrontend-prepare→publish-test/publish-productionneed all six gates →deploy-test/deploy-productionneed their publish job plusconfig
- Parsed successfully: 12 jobs,
-
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) |