Files
SluijsensandClaude Opus 5 8568ca43c6 Plans the Gitea deployment feature and refreshes the codebase analysis
Adds the AI-DLC inception record for deploying the CMS as a single .NET
application on hosting where no server configuration is possible.

The reverse-engineering artifacts were regenerated: the previous set
predated the Master module, the Slave host, the solution reorganisation
and single-host serving, all of which matter for deployment. Findings
were verified by running the build, both test suites and the linter
rather than inferred, which surfaced two facts the plan depends on:
the frontend lint gate currently fails (5 errors), and two transitive
packages carry high-severity advisories.

Records 24 functional requirements, 32 traced decisions and a
seven-unit decomposition whose ordering is load-bearing: durability
work must land before the first automated deploy, or the very first
deploy is the one that silently breaks master/slave trust.

Two conflicts found while designing and carried into the units:
- Both modules call AddDataProtection(), which runs after the host and
  would override a persistent key store while still passing any
  registration test.
- The availability gate runs before authentication, so its admin
  bypass cannot read HttpContext.User.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HHoJpxYXzHACSQguHrC5fw
2026-07-27 23:59:30 +02:00

14 KiB
Raw Permalink Blame History

Units of Work — Gitea Deployment Workflow

Date: 2026-07-27 Decomposition basis: Q1 = C (split the oversized unit), Q2 = B (merge the quality-gate fixes into the CI unit), Q3 = B (split into Hosting & Serving plus Data Durability)


Decomposition Outcome

The proposed 7-unit split changed in two ways that cancel out numerically:

  • Unit 2 was split in two (Q3 = B) — it had bundled three different kinds of work whose only commonality was the deadline "before the first deploy"
  • The quality-gate prerequisites were merged into the CI unit (Q2 = B) — the lint fixes and package pins exist because the gates are being switched on, so they land in the same commit as the gates

Net result: still 7 units, but with boundaries drawn along the work rather than along the deadline.

# Unit Type
U1 Hosting & Serving Application
U2 Data Durability Application
U3 HTTP Security Headers & CSP Application
U4 Observability Integration Application + Frontend
U5 CI Workflow & Quality Gates Pipeline
U6 Deploy Workflow Pipeline
U7 Repository Documentation Documentation

Execution Rounds (Q4 = B)

Serial where dependent, grouped where independent. One commit per unit regardless of grouping (Q6 = A); a round is an approval boundary, not a commit boundary.

Round Units Why grouped
R1 U1 + U2 Mutually independent — one changes serving and middleware, the other changes persistence and startup. Neither reads the other's output
R2 U3 + U4 Tightly coupled — U3's CSP configuration is populated with the Umami and Sentry origins that U4 introduces. Splitting them means writing a CSP against origins that do not exist yet
R3 U5 + U6 U6 is invoked by U5; the two workflow files are designed against one shared input interface
R4 U7 Depends on U6's settled host layout

Everything in R1 and R2 must land before R3's deploy workflow can safely run — the reason the durability work is not left until later.


U1 — Hosting & Serving

Purpose: make one process serve two independent front-ends correctly, and expose infrastructure liveness that the CMS's own on/off state cannot mask.

Scope:

  • Remount static files: wwwroot/web/ at /, wwwroot/admin/ at /admin, each with its own PhysicalFileProvider (Q3 of Application Design = A)
  • Retarget both SPA fallbacks, preserving the nonfile constraint so missing assets still 404
  • Tolerate a missing wwwroot/web/ at startup — a fresh deployment has none until a website workspace deploys into it, and the CMS must still start and serve /admin and /api/v1
  • Add AddCmsHealthChecks() / MapCmsHealthChecks() exposing GET /health, liveness only, no database call
  • Add /health to AvailabilityMiddleware._bypassPrefixes
  • Fix IsAdminBypass to stop trusting an unvalidated token (FR-24)

Components: C-04, C-10, C-13, and the U1 portion of C-16 Requirements: FR-07, FR-10, FR-24 Projects touched: SlpModularCms.Core, SlpModularCms.Api, SlpModularCms.Api.Slave, SlpModularCms.Modules.Availability

Carried-in design item: § 5.2 of application-design.mdAvailabilityMiddleware runs before UseAuthentication(), so HttpContext.User is unpopulated when the admin bypass is evaluated. Functional Design for this unit decides between validating the token in the middleware (contained, duplicates validation parameters) and moving authentication earlier (smaller change, wider blast radius).

Definition of done (Q5 = B): builds; all existing tests pass; new tests for /health reachability including while the instance is disabled, for the two static mounts' fallback precedence, for graceful startup without wwwroot/web/, and for the admin bypass rejecting a forged token while still accepting a valid one.


U2 — Data Durability

Purpose: make a redeploy safe. Nothing this unit delivers is visible in normal operation; its entire value is that the atomic release switch in U6 does not silently destroy trust or schema state.

Scope:

  • ApplicationDbContext implements IDataProtectionKeyContext with a DataProtectionKeys set; one new Core migration
  • AddCmsDataProtection() configuring PersistKeysToDbContext<ApplicationDbContext>
  • Set an explicit application discriminator — the default derives from the content root path, which changes on every atomic release switch, defeating the purpose by a different route
  • Remove services.AddDataProtection() from AvailabilityModule and MasterModule — module registration runs after the host's, so those calls would override the persistent key store
  • MigrateCoreDatabase() applying ApplicationDbContext migrations at startup, fail fast on failure (Q8 of Application Design = A)

Components: C-05, C-06, C-07, and the U2 portion of C-16 Requirements: FR-11, FR-12 Projects touched: SlpModularCms.Core, SlpModularCms.Modules.Availability, SlpModularCms.Modules.Master, both hosts

Carried-in design item: § 5.1 of application-design.md — the duplicate AddDataProtection() conflict. This is the unit's highest-value test: without it, FR-12 passes registration tests while remaining ephemeral, and the failure only surfaces later as an apparent network fault between master and slave.

Definition of done (Q5 = B): builds; all existing tests pass; new tests asserting that the persistent key store survives module registration, that the application discriminator is explicit and stable, and that a protected value round-trips across a simulated content-root change. Both hosts start successfully.


U3 — HTTP Security Headers & CSP

Purpose: supply, from inside the application, the headers that would normally come from nginx or IIS configuration — which NFR-01 forbids relying on.

Scope:

  • SecurityHeadersMiddleware applying headers at response start via OnStarting
  • Per-header scoping (FU1 = A): X-Content-Type-Options and Strict-Transport-Security on all responses; Content-Security-Policy, X-Frame-Options and Referrer-Policy on HTML responses only
  • SecurityHeadersOptions binding a new SecurityHeaders section
  • CspPolicyBuilder with two code-defined policies — Strict and Relaxed — composed once at startup
  • Path-to-policy assignment and allowed origins in configuration; policy definitions in code (FU2 = A)
  • Registration before static files, since static files short-circuit the pipeline
  • An unknown policy name fails at startup, not per request

Components: C-01, C-02, C-03, and the U3 portion of C-16 Requirements: FR-18 Projects touched: SlpModularCms.Core, both hosts

Definition of done (Q5 = B): builds; all existing tests pass; new tests for policy composition per name, path-to-policy resolution including the default fallback, per-header applicability across HTML and non-HTML responses, headers reaching static-file responses, not overwriting pre-set headers, and startup failure on an unknown policy name.


U4 — Observability Integration

Purpose: make it possible to tell, without host access, whether the application is erroring and whether it is being used.

Scope:

  • AddCmsLogging() — structured logging with a correlation identifier, independent of Sentry (Q10 of Application Design = B)
  • AddCmsSentry() — initialises only when a DSN is configured; absent DSN is a supported state, not an error
  • Environment and release tagging
  • Emit security-relevant events for alerting (FR-19)
  • Frontend: @sentry/react initialisation, Umami tracking script with a per-environment website ID, absent in local development
  • Frontend: config.ts treats an absent or empty VITE_API_BASE_URL as same-origin while still accepting an explicit absolute URL for local development

Components: C-08, C-09, C-14, C-15, and the U4 portion of C-16 Requirements: FR-13, FR-14, FR-15, FR-16, FR-19 Projects touched: SlpModularCms.Core, both hosts, frontend/

Carried-in open item: OPEN-01 — the correlation-ID mechanism (TraceIdentifier versus W3C traceparent) is decided in this unit's NFR Design, along with the definition of an alertable security event.

Note on lint: because the quality-gate fixes moved to U5 (Q2 = B), pnpm run lint is still failing for pre-existing reasons while this unit changes frontend files. Lint should be run on the changed files during this unit so no new violations accumulate, even though the blocking gate is not switched on until U5.

Definition of done (Q5 = B): builds; all existing tests pass; new tests for logging configuration with correlation ID present, Sentry registration being a no-op without a DSN, same-origin resolution when VITE_API_BASE_URL is empty, explicit-URL behaviour preserved, and the Umami component rendering nothing without a website ID.


U5 — CI Workflow & Quality Gates

Purpose: validate every change, and be the only route to production.

Scope:

  • Fix the 5 frontend lint errors and 1 warning (FR-21) — merged here per Q2 = B, so the gates and the fixes land together and the pipeline is never red on arrival
  • Pin Microsoft.OpenApi and System.Security.Cryptography.Xml to patched versions (FR-22, OPEN-03)
  • .gitea/workflows/continuous_integration.yaml with triggers on pull_request, push to master, and workflow_dispatch with a deploy_production boolean defaulting to false
  • Six blocking gates: backend build, backend tests, vulnerability scan, frontend build, frontend tests, frontend lint and format-check
  • Toolchain installed explicitly and pinned (actions/setup-dotnet, pnpm/action-setup); no latest tags
  • Two environment-specific builds with their own Vite variables (FR-05)
  • Production reachable only via workflow_dispatch with the flag set (FR-04)

Components: C-12 Requirements: FR-01, FR-04, FR-05, FR-21, FR-22 Projects touched: .gitea/workflows/, frontend/src/ (lint fixes), *.csproj (package pins)

Definition of done (Q5 = B): all six gates pass locally against the current tree — pnpm run lint clean, dotnet list package --vulnerable clean, all tests green. Workflow YAML is syntactically valid. Production cannot be triggered by a push.


U6 — Deploy Workflow

Purpose: turn a validated build into a running release without endangering the customer's website, the database, or master↔slave trust.

Scope:

  • .gitea/workflows/deploy-scp.yaml as a reusable workflow_call workflow (Q11 of Application Design = B — one workflow per transport, identical input interface)
  • Plain shell steps, no container actions (they fail on the Podman-backed runner)
  • Deployment sequence: download artifact → production only: database backup before any change → upload to a new release directory → link the persistent wwwroot/web/ into it → switch the active release atomically → restart the process → verify /health → prune old releases keeping at least the previous one
  • Automatic test deployment on master; production deployment only when invoked with the flag
  • Environment-specific paths from Gitea variables, credentials from secrets

Components: C-11 Requirements: FR-02, FR-03, FR-06, FR-08, FR-20 Projects touched: .gitea/workflows/

Carried-in assumption: ASM-01 — wwwroot/web/ must live outside the swapped release directory and be linked into each new release. Confirmed in this unit's Infrastructure Design. Getting this wrong destroys the customer's website, the highest-severity risk in the feature.

Definition of done (Q5 = B): workflow YAML valid; the deployment sequence documented step by step including the rollback path; the wwwroot/web/ linking step explicit and justified. Note that end-to-end verification requires the actual Pi, SSH credentials and a database, so it cannot be fully proven in CI — real-run verification belongs to the Operations phase.


U7 — Repository Documentation

Purpose: let a website workspace deliver a site that works, without its author needing to read this repository's code.

Scope (Q8 = A — repository documentation here; operational documents in the Operations phase):

  • Website workspace contract (FR-09): target path wwwroot/web/, required structure, forbidden paths (admin/, the application root), reserved paths (/admin, /api/v1, /health), SPA-fallback behaviour, how to call /api/v1 same-origin without CORS, which CSP applies, and how to include the Umami script
  • README updates: the new wwwroot layout, the health endpoint and what it does not mean, the changed production setup section
  • frontend/.env.example updates for the same-origin default and the new observability variables

Components: none — documentation only Requirements: FR-09 Projects touched: repository root, frontend/

Definition of done: documentation is accurate against the code as built in U1U6, and the website contract is complete enough to follow without reading source.


Out of Unit Scope — Delivered by the Operations Phase

Requirement Delivered at
FR-17 — UptimeRobot monitors for /health, / and /admin Monitoring Setup
FR-19 — Sentry alert rules (the application-side event emission is in U4) Monitoring Setup
FR-23 — deployment instructions, host setup, rollback plan, FTPS switch path Deployment Setup
DEV-01…04 re-confirmation, appsettings compliance gate Production Readiness Validation

Code Organization

Brownfield — the existing structure is retained. New code follows the solution layout mandated by CLAUDE.md / AGENTS.md:

  • Cross-cutting concerns go in src/SlpModularCms.Core/Hosting/, in new subfolders Security/, Health/ and Observability/
  • No new project is added to the solution
  • Workflow files go in .gitea/workflows/ at the repository root
  • Tests mirror their production project, per the existing Tests solution folder convention