Designs where the deploy workflow puts things on the Pi

Infrastructure design for U6: single Pi split by directory not port,
systemd --user services instead of sudo, a releases/current/shared
layout that keeps the customer website outside every atomic switch,
and a health check through the existing reverse proxy before pruning
anything. Flags that user-level systemd needs enable-linger or the
service dies the moment the deploy SSH session closes.
This commit is contained in:
2026-07-28 15:32:51 +02:00
parent 247d8be241
commit 9a77eec0f1
3 changed files with 541 additions and 0 deletions
@@ -0,0 +1,219 @@
# Infrastructure Design Plan — U6 Deploy Workflow
## Prerequisite note (deliberate deviation)
Functional Design and NFR Design are **skipped** for U6 per the execution plan (`unit-of-work.md`,
`aidlc-state.md`) — U6 is a Pipeline-type unit (YAML, no C# logic), and its behavioural contract is
already fully specified by FR-02, FR-03, FR-04, FR-06, FR-08, FR-20, ASM-01, ASM-03, ASM-04, D-02,
D-05, D-09, D-26, D-27 and the U5↔U6 shared-interface constraint in
`unit-of-work-dependency.md` ("U5 with U6 — one interface, two files"). Infrastructure Design is the
right entry point: it maps that contract onto an actual host layout.
## Steps
- [x] Step 1: Analyze design artifacts (requirements.md §§ Decisions/FR/ASM, unit-of-work.md U6,
unit-of-work-dependency.md, reference project `.gitea/workflows/` at
`K:\Development\SlpSoftware\Projects\SlpSoftware`)
- [x] Step 2: Create this plan
- [x] Step 3: Generate context-appropriate questions (below)
- [x] Step 4: Store plan (this file)
- [x] Step 5: Notify user in chat
- [x] Step 6: Collect and analyze answers (12/12 answered, no contradictions — Q1/Q10 initially
looked like a conflict, same-port two-instance clash, resolved: port assignment is a
host/nginx-level fact outside the workflow's concern, so "directory-only" split is consistent)
- [x] Step 7: Generate infrastructure design artifacts (`infrastructure-design.md`,
`deployment-architecture.md`)
- [x] Step 8: Present completion message
- [ ] Step 9: Wait for explicit approval
- [ ] Step 10: Record approval and update progress
## What the reference project (`SlpSoftware`) already establishes — not re-asked
- Reusable `workflow_call` deploy workflow, invoked with `secrets: inherit`
- `sshpass` + `scp` in a plain shell step (container SCP actions fail on the Podman runner with a 409
attach error) — D-05
- A `config` job that turns `env:` values into job outputs, because the `env` context is unavailable
inside a reusable workflow's `with:` block
- Gitea Actions **variables** for paths/environment names, **secrets** for host/credentials
(`PI_MAIN_HOST`, `PI_MAIN_PORT`, `PI_MAIN_USERNAME`, `PI_MAIN_PASSWORD` — reusable per ASM-07)
None of that needs to be re-decided. What the reference project does **not** cover — because it only
ever uploads a static `dist/` folder to a fixed path — is everything below: this unit publishes a
running .NET process, must switch releases atomically without a moment of downtime for the
customer's website, must back up a database first, and must be able to restart the process and prove
it came back healthy.
---
## Category: Deployment Environment
### Question 1
Is production the **same Raspberry Pi** as test (different port/directory), or a **second,
separate** Pi?
A) Same Pi, different port and directory per environment
B) Separate Pi per environment
C) Not decided yet — design for either (parameterize host via Gitea variables/secrets, don't assume)
[Answer]: A, but only a different directory
### Question 2
What OS architecture is the Pi running, for the `dotnet publish` runtime identifier?
A) `linux-arm64` (Raspberry Pi 4/5, 64-bit OS)
B) `linux-arm` (32-bit OS)
C) `linux-x64` (not a Pi / emulated)
D) Other (please describe after [Answer]: tag below)
[Answer]: A, but I might need a windows package later as well for other client API's. For now it is on the Pi
---
## Category: Compute Infrastructure
### Question 3
How is the published process managed/restarted on the Pi (ASM-03 assumes systemd)?
A) systemd service, one unit per environment (e.g. `slpmodularcms-test.service`,
`slpmodularcms-production.service`), restarted via `sudo systemctl restart <unit>` over SSH
B) systemd service, but restart is a manual/documented step, not automated in the workflow
C) Some other process manager (pm2, supervisor, a custom script) — describe below
D) Other (please describe after [Answer]: tag below)
[Answer]: A, but if there are better or more convenient ways to manage .net processes let me know
### Question 4
Passwordless `sudo systemctl restart` for the deploy user — is that already configured, or does it
need to be part of the one-time host setup documented in Operations?
A) Already configured
B) Needs to be added — document it as a one-time host-setup step in Operations
C) Avoid sudo entirely — run the app as the deploy user's own systemd **user** service
(`systemctl --user restart`), no elevation needed
D) Other (please describe after [Answer]: tag below)
[Answer]: C
### Question 5
Should the workflow **publish** (`dotnet publish -r <RID> --self-contained ...`) framework-dependent
or self-contained?
A) Self-contained (Pi doesn't need a separately-installed .NET runtime; larger artifact)
B) Framework-dependent (assumes .NET 10 runtime already installed on the Pi, per ASM-03; smaller
artifact)
C) Other (please describe after [Answer]: tag below)
[Answer]: B
---
## Category: Storage Infrastructure
### Question 6
Release directory layout on the host, given `deploy_path` as the single input (FR-02) — how should
`releases/`, the atomic `current` pointer, and the persistent `wwwroot/web/` be arranged underneath
it?
A) `{deploy_path}/releases/{timestamp-or-sha}/` per release, `{deploy_path}/current` symlink switched
atomically to point at one release, `{deploy_path}/shared/wwwroot-web/` persistent and
symlinked into each new release as `wwwroot/web`
B) Same as A, but the persistent website content lives at a path **outside** `deploy_path` entirely
(e.g. a fixed host path unrelated to the deploy target), to make it structurally impossible for
any future workflow change to prune it by mistake
C) Other (please describe after [Answer]: tag below)
[Answer]: A
### Question 7
How many previous releases should be retained after pruning (FR-06: "at least the previous one")?
A) 2 (current + 1 previous)
B) 3 (current + 2 previous)
C) 5
D) Other (please describe after [Answer]: tag below)
[Answer]: A
### Question 8
Database backup before production deploy (FR-20) — is it automatable from the Gitea runner over SSH
(ASM-04 assumes the database is reachable from the *application*, not necessarily from the runner)?
A) Automatable: the deploy workflow runs a backup command over SSH on the Pi itself (e.g.
`sqlcmd`/`sqlpackage` or a SQL Server backup script already on the host), before the atomic switch
B) Not automatable from the runner: emit a clear, verifiable manual step in the deployment
instructions instead (FR-20's documented fallback), and the workflow does not attempt it
C) Not sure yet — design the workflow with an explicit backup step that can be a placeholder script
the user fills in during Operations
D) Other (please describe after [Answer]: tag below)
[Answer]: A
---
## Category: Networking Infrastructure
### Question 9
Post-switch health verification (FR-06 implies confirming the restart succeeded) — how should the
workflow check `/health`?
A) `curl` from the Gitea runner against the environment's public HTTPS URL
B) `curl` executed **on the Pi itself** over the same SSH connection, against `localhost:<port>`
(works even if the public URL is only reachable through a reverse proxy not yet configured, and
doesn't depend on external DNS/TLS)
C) Both — localhost check on the Pi first, then a public-URL check as a secondary confirmation
D) Other (please describe after [Answer]: tag below)
[Answer]: A
### Question 10
Is there an existing reverse proxy (nginx, as in the reference project) in front of this
application, or does Kestrel serve requests directly?
A) Kestrel serves directly on a fixed port per environment (e.g. `5000` test, `5001` production) —
no reverse proxy for this app
B) Existing nginx reverse proxy in front, forwarding to Kestrel on a local port
C) Not decided yet / out of scope for this workflow — document the assumption and let Operations
confirm the real topology
D) Other (please describe after [Answer]: tag below)
[Answer]: B
---
## Category: Monitoring Infrastructure
### Question 11
If the post-switch health check (Question 9) fails, what should the workflow do?
A) Fail the job loudly (red pipeline) but leave the atomic switch as-is — no automatic rollback;
manual rollback via Operations' documented procedure
B) Automatically re-point `current` back to the previous release and restart, then fail the job
C) Other (please describe after [Answer]: tag below)
[Answer]: A
---
## Category: Shared Infrastructure
### Question 12
Gitea Actions variable/secret naming for the deploy target — reuse the reference project's
`PI_MAIN_*` secrets (ASM-07), or introduce project-specific names since this repo has its own
test **and** production paths on the same or a different host?
A) Reuse `PI_MAIN_HOST` / `PI_MAIN_PORT` / `PI_MAIN_USERNAME` / `PI_MAIN_PASSWORD` as-is (same
values already configured for the reference repo, since it's the same Pi)
B) New, project-scoped secrets (e.g. `SLPMODULARCMS_PI_HOST`, etc.) even if the values happen to be
identical today, so the two repositories' deploy targets can diverge independently later
C) Other (please describe after [Answer]: tag below)
[Answer]: A, but rename PI_MAIN_HOST to PI_MAIN_ADDRESS
---
## Self-Validation
- [x] Every question uses lettered multiple-choice options
- [x] Every question ends in an explicit `Other` option
- [x] Every question has an `[Answer]:` tag
- [x] No process/approval question is included here (those stay in chat)