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
11 KiB
Application Design — Gitea Deployment Workflow
Feature: gitea-deployment-workflow
Date: 2026-07-27
Consolidates: components.md, component-methods.md, services.md, component-dependency.md
1. Design Summary
This feature adds cross-cutting infrastructure to an existing modular monolith, plus the CI/CD pipeline that deploys it. It introduces no new business capability and no new domain entity beyond a Data Protection keys table.
The design is shaped by four decisions taken during this stage:
| Decision | Choice | Consequence |
|---|---|---|
| Q1 = A — placement | All new cross-cutting concerns live in SlpModularCms.Core as extension methods |
Both hosts get identical behaviour; nothing is implemented twice |
| Q2 = A — Slave scope | The Slave host receives everything except static mounts | The Slave is a reference instance showing what a customer-facing API looks like, not a stripped-down dev tool. Its behaviour must match production |
| Q9 = A — composition | Each host keeps its own Program.cs |
What remains duplicated is a readable list of calls, not logic |
| Q8 = A — failure mode | Fail fast on migration failure | Makes the liveness-only health check meaningful: a process that cannot migrate never starts, so /health goes silent and monitoring goes red |
14 code components (9 new, 5 modified) and 2 workflow components, across Core, Api, Modules.Availability, frontend and .gitea/workflows/.
2. Components
See components.md for full definitions. Summary:
New in SlpModularCms.Core
- C-01
SecurityHeadersMiddleware— applies headers that would normally come from nginx or IIS, which NFR-01 forbids relying on - C-02
SecurityHeadersOptions— binds theSecurityHeaderssection - C-03
CspPolicyBuilder— composes CSP strings from code-defined policies plus configured origins - C-04 Health-check registration —
GET /health, liveness only - C-05
CmsDataProtection— database-backed key ring - C-07 Startup migration runner — Core migrations, fail fast
- C-08
CmsLogging— structured logging, independent of Sentry - C-09
CmsSentry— optional; absent DSN is a supported state
Modified
- C-06
ApplicationDbContext— implementsIDataProtectionKeyContext; one new Core migration - C-13
AvailabilityMiddleware—/healthbypass, plus the FR-24 token-validation fix - C-14/C-15 frontend — same-origin config, Sentry, Umami
- C-16 host composition — both
Program.csfiles
Host-specific and workflow
- C-10 Static mounts —
Apionly:wwwroot/web/at/,wwwroot/admin/at/admin - C-11
deploy-scp.yaml, C-12continuous_integration.yaml
3. Key Interfaces
See component-methods.md for full signatures. The contracts that carry design weight:
AddCmsHealthChecks() takes no options. Deliberate — "just add a database check" then becomes a visible code change rather than configuration drift, keeping D-21 (liveness only) enforced by shape rather than by discipline.
MigrateCoreDatabase() has no try/catch. Exceptions propagate by design (Q8 = A).
AddCmsSentry() treats an absent DSN as normal. Local development and any Sentry-less deployment run unchanged.
SecurityHeadersOptions configures assignment, not definition. Policy content is in code; path assignment and environment-specific origins are configuration (FU2 = A). A misconfiguration can misroute a path but cannot invent a broken policy.
Both deploy workflows share one input interface (Q11 = B), so switching transport changes only a uses: line.
4. Orchestration
See services.md for the full pipeline. The two orderings that matter most:
Security headers precede static files. Static files short-circuit the pipeline; anything registered after them never reaches the public website — the very surface the CSP is for. Headers are applied at response start via OnStarting, because the content type is unknown earlier and per-header scoping (FU1 = A) depends on it.
/health is an endpoint, so it runs after the availability gate. That is exactly why /health must be on the bypass list (D-22) — otherwise a deliberately disabled instance would report itself as unhealthy, re-creating the conflation this feature exists to avoid.
Startup order: logging and Sentry first (so later failures are captured) → module discovery → services including Data Protection → build → Core migration (fail fast) → module migrations → serve.
5. Two Conflicts Found During Design
Both were discovered by tracing the composition order, and both would have produced code that looks correct while doing nothing useful.
5.1 Duplicate AddDataProtection() would silently defeat FR-12
AvailabilityModule.RegisterServices and MasterModule.RegisterServices each call services.AddDataProtection() today. Module registration runs after the host's registration, so the modules' bare calls would override the persistent key store configured by AddCmsDataProtection().
The result: FR-12 appears implemented, tests that check registration pass, and the key ring is still ephemeral — so the first atomic release switch silently breaks master↔slave trust in a way that presents as a network fault. Exactly the failure FR-12 exists to prevent.
Resolution: remove AddDataProtection() from both modules; the host configures Data Protection once. Assigned to Unit 2, with a test asserting the persistent store survives module registration.
Second-order requirement: the Data Protection application discriminator must be set explicitly. By default it derives from the content root path, which changes on every atomic release-directory switch (FR-06) — which would defeat FR-12 by a different route.
5.2 AvailabilityMiddleware runs before authentication
FR-24 requires the admin bypass to stop trusting an unvalidated token. But AvailabilityMiddleware is installed by orchestrator.UseModules(app) at step 8, while UseAuthentication() runs at step 9 — so HttpContext.User is not yet populated when the bypass is evaluated.
Two options, decided in Functional Design for Unit 2:
| Option | Trade-off |
|---|---|
(a) Validate the token inside the middleware with the same TokenValidationParameters as the bearer scheme |
Contained, but duplicates validation parameters — which must then be shared from one source rather than copied |
(b) Move UseAuthentication() before the module middleware |
Smaller code change, but alters the pipeline for every module including future ones — a wider blast radius than this feature warrants |
Neither is obviously correct, which is why it is recorded rather than decided here.
6. Dependencies
See component-dependency.md for the matrix and data flows. The coupling that deserves attention:
The wwwroot/web/ filesystem convention is the feature's weakest link. It is enforced by convention, not by the type system, and getting it wrong destroys a customer's website (NFR-02). Two design obligations follow:
C-10must tolerate a missingwwwroot/web/at startup — a fresh deployment has none until a website workspace deploys into it, and the CMS must still start and serve/adminand/api/v1- Deployment must link the persistent
wwwroot/web/into each new release directory (ASM-01)
Core reaches the Slave. Every change lands in a host with no test project, verified at Build and Test by starting it.
Build-time frontend configuration forces two artifacts (D-15), and the publish target makes Node and pnpm prerequisites of dotnet publish.
7. Requirements Coverage
| Requirement | Covered by |
|---|---|
| FR-01, FR-03, FR-04, FR-05 | C-12 |
| FR-02 | C-11, S-06 |
| FR-06, FR-20 | S-05 |
| FR-07, FR-08 | C-10, S-01 |
| FR-09 | Unit 7 documentation (Infrastructure Design input) |
| FR-10 | C-04, C-13, S-01 |
| FR-11 | C-07, S-03 |
| FR-12 | C-05, C-06 — and § 5.1 |
| FR-13 | C-14 |
| FR-14 | C-09 |
| FR-15, FR-16 | C-15 |
| FR-17 | Operations phase |
| FR-18 | C-01, C-02, C-03, S-02 |
| FR-19 | C-09 + NFR Design, Unit 4 |
| FR-21, FR-22 | Unit 1 — no design needed |
| FR-23 | Operations phase |
| FR-24 | C-13 — and § 5.2 |
All 24 functional requirements are accounted for: 18 by a designed component, 3 by the Operations phase, 2 needing no design, and 1 (FR-09) depending on Infrastructure Design output.
8. Security Compliance (Security Baseline extension — enabled, blocking)
Assessed against the design.
| Rule | Status | Notes |
|---|---|---|
| SECURITY-01 | Addressed | TLS enforced in connection strings; HSTS on all responses (FU1 = A) |
| SECURITY-02 | N/A | No load balancer, API gateway or CDN in this architecture |
| SECURITY-03 | Addressed | C-08 structured logging with correlation ID; no secrets or PII. Mechanism decided in NFR Design (OPEN-01) |
| SECURITY-04 | Addressed | C-01/C-02/C-03, with per-header scoping so nosniff covers assets — the specific gap caught in follow-up round 2 |
| SECURITY-05 | Unchanged | /health is the only new endpoint and accepts no input |
| SECURITY-06 | Addressed | Deploy credentials scoped to the target; Gitea secrets repository-scoped |
| SECURITY-07 | Partially N/A | No cloud networking; applicable parts are documented host setup |
| SECURITY-08 | Improved | C-13 closes the forged-token bypass (FR-24) — a pre-existing finding this feature now fixes rather than inherits |
| SECURITY-09 | Addressed | Directory browsing stays disabled; Scalar remains Development-only; production errors stay generic; no default credentials |
| SECURITY-10 | Addressed | Blocking vulnerability gate; pinned tool versions; --frozen-lockfile. DEV-02 records the missing packages.lock.json and SBOM |
| SECURITY-11 | Addressed | Rate limiting pre-exists; security logic stays in Core/Identity; the misuse cases explicitly designed against are website destruction (NFR-02) and silent key-ring loss (§ 5.1) |
| SECURITY-12 | Unchanged | Pre-existing; DEV-03 records absent MFA and breached-password checking |
| SECURITY-13 | Addressed | SRI for external scripts where supported; CSP constrains them; pipeline definitions are version-controlled and reviewable |
| SECURITY-14 | Addressed with DEV-01 | Alerting via C-09; retention deviation accepted |
| SECURITY-15 | Unchanged | Global exception handler pre-exists. New fail-closed decision: an unknown CSP policy name fails at startup rather than degrading silently. The master gate's deliberate fail-open remains an intentional, business-driven exception |
No blocking security findings. SECURITY-08 improves from "pre-existing, unchanged" to "improved" because Q12 = A folded the fix into this feature.
9. Carried Forward to Construction
| Item | Resolved at |
|---|---|
§ 5.1 — remove duplicate AddDataProtection(); set an explicit application discriminator |
Functional Design + Code Generation, Unit 2 |
| § 5.2 — FR-24 implementation approach (validate in middleware versus move authentication) | Functional Design, Unit 2 |
| OPEN-01 — correlation-ID mechanism | NFR Design, Unit 4 |
| OPEN-03 — patched versions for the two vulnerable packages | Code Generation, Unit 1 |
ASM-01 — wwwroot/web/ outside the swapped release directory |
Infrastructure Design, Unit 6 |
| Definition of an alertable security event | NFR Design, Unit 4 |
C-10 tolerating a missing wwwroot/web/ at startup |
Functional Design, Unit 2 |