# 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 the `SecurityHeaders` section - **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`** — implements `IDataProtectionKeyContext`; one new Core migration - **C-13 `AvailabilityMiddleware`** — `/health` bypass, plus the FR-24 token-validation fix - **C-14/C-15 frontend** — same-origin config, Sentry, Umami - **C-16 host composition** — both `Program.cs` files ### Host-specific and workflow - **C-10 Static mounts** — `Api` only: `wwwroot/web/` at `/`, `wwwroot/admin/` at `/admin` - **C-11 `deploy-scp.yaml`**, **C-12 `continuous_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: 1. `C-10` must 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` 2. 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 |