U6 — the deploy workflow itself

deploy-scp.yaml: download, back up the production database before
touching anything, upload into a timestamped release, symlink the
persistent website in, switch current and restart, verify /health
with retries, prune old releases only once that check passes. No
sudo, no container actions, one shared interface U5 will call next.
This commit is contained in:
2026-07-28 15:33:03 +02:00
parent 9a77eec0f1
commit bd2a963498
4 changed files with 355 additions and 0 deletions
@@ -1027,3 +1027,80 @@ Nothing pushed. Next: Round 3 — U5 CI Workflow & Gates and U6 Deploy Workflow,
by Infrastructure Design. U5 carries REF-U3-01's Umami origin gate plus FR-21 and FR-22.
---
## Session Resume
**Timestamp**: 2026-07-28T00:00:00Z
**User Input**: "Ik was bezig met de gitea-deployment-workflow feature met aidlc. Die wil ik graag oppakken. Als het goed is waren net u3 en u4 afgerond"
**AI Response**: "Confirmed Round 2 (U3 Security Headers & CSP, U4 Observability) is complete, committed, and verified (372 backend / 237 frontend tests passing). Nothing pushed. Proceeding to Round 3: U5 CI Workflow & Gates + U6 Deploy Workflow (Infrastructure Design first for U6)."
**Context**: Session resumed; loaded aidlc-state.md, unit-of-work.md, and Round 2 verification record
---
## Infrastructure Design — U6 Deploy Workflow (Planning)
**Timestamp**: 2026-07-28T00:05:00Z
**User Input**: "(implicit — continuing Round 3 per prior approval)"
**AI Response**: "Created infrastructure-design-plan question file with 12 questions across 6 categories (Deployment Environment, Compute, Storage, Networking, Monitoring, Shared Infrastructure), covering Pi topology, publish RID/self-contained mode, systemd restart mechanism and sudo, release/persistence directory layout, retention count, database backup automatability, health-check verification method, rollback-on-failure behaviour, and Gitea secret naming. Functional Design and NFR Design confirmed skipped for U6 per execution plan (Pipeline-type unit)."
**Context**: CONSTRUCTION — Round 3 — U6 Infrastructure Design, Step 3-5 (question generation, awaiting user answers)
---
## Infrastructure Design — U6 Deploy Workflow (Complete)
**Timestamp**: 2026-07-28T00:20:00Z
**User Input**: "Ik heb de vragen ingevuld, ga verder"
**AI Response**: "Read 12/12 answers from u6-deploy-workflow-infrastructure-design-plan.md. Resolved an apparent Q1/Q10 tension (same Pi, directory-only split, yet an existing nginx reverse proxy routes environments) by concluding port assignment is a pre-existing host/nginx fact the workflow never manages. Generated infrastructure-design.md and deployment-architecture.md covering host topology, systemd --user process management (identifying a new finding, INFRA-U6-01: loginctl enable-linger is required or the service dies when the deploy SSH session closes), release/shared/current directory layout with 2-release retention, the full deployment sequence including production-only DB backup via a host-side script (no DB credentials touch the workflow), health verification via public URL through nginx with no automatic rollback on failure, the reusable workflow_call interface shared with U5, and Gitea variable/secret naming (PI_MAIN_ADDRESS renamed per user's answer)."
**Context**: CONSTRUCTION — Round 3 — U6 Infrastructure Design, Steps 6-8 complete; awaiting approval to proceed to Code Generation
---
## Infrastructure Design — U6 Deploy Workflow (Approved)
**Timestamp**: 2026-07-28T00:25:00Z
**User Input**: "Continue to Next Stage"
**AI Response**: "Infrastructure Design for U6 approved. Proceeding to Code Generation, starting with U6 (deploy-scp.yaml, implementing the just-approved interface) then U5 (continuous_integration.yaml, which calls it), since U5 depends on U6's concrete interface."
**Context**: CONSTRUCTION — Round 3 — Infrastructure Design U6 marked complete; entering Code Generation for U6
---
## Code Generation Plan — U6 Deploy Workflow (Planning)
**Timestamp**: 2026-07-28T00:30:00Z
**User Input**: "(implicit — continuing after Infrastructure Design approval)"
**AI Response**: "Created code generation plan for U6 (3 steps: generate deploy-scp.yaml, validate YAML, generate documentation), presenting for approval before generation starts."
**Context**: CONSTRUCTION — Round 3 — U6 Code Generation Part 1 (Planning), awaiting plan approval
---
## Code Generation Plan — U6 Deploy Workflow (Approved)
**Timestamp**: 2026-07-28T00:32:00Z
**User Input**: "Approve Plan"
**AI Response**: "Plan approved. Proceeding to Code Generation Part 2 for U6."
**Context**: CONSTRUCTION — Round 3 — U6 Code Generation Part 1 complete, entering Part 2 (Generation)
---
## Code Generation — U6 Deploy Workflow (Complete)
**Timestamp**: 2026-07-28T00:40:00Z
**User Input**: "(implicit — plan approved, generation executed)"
**AI Response**: "Generated .gitea/workflows/deploy-scp.yaml (7 workflow_call inputs, 1 job, 8 steps) implementing the approved Infrastructure Design. Validated YAML syntax with PyYAML (confirmed the 'on:' key resolves to boolean True under YAML 1.1 rules, matching the reference project's own working deploy.yaml — a parser quirk, not a defect). Documented in construction/u6-deploy-workflow/code/generation-summary.md. No C# test project applies to this Pipeline-type unit."
**Context**: CONSTRUCTION — Round 3 — U6 Code Generation Part 2 complete, presenting completion message
---
@@ -0,0 +1,62 @@
# Code Generation Plan — U6 Deploy Workflow
## Unit Context
- **Unit**: U6 Deploy Workflow (Pipeline-type — YAML only, no C#/frontend code)
- **Component**: C-11 Deploy transport workflows (`.gitea/workflows/`)
- **Requirements**: FR-02, FR-03, FR-06, FR-08, FR-20
- **Depends on**: U1, U2, U5 (per `unit-of-work-dependency.md`) — U1/U2 are already committed
(Rounds 12); U5 does not exist yet, but U6 only needs to **expose** the interface U5 will call,
not the other way around, so generating U6 first is safe
- **Depended on by**: U5 (invokes this workflow), U7, Operations
- **Basis**: `construction/u6-deploy-workflow/infrastructure-design/infrastructure-design.md`
(approved) and `deployment-architecture.md`
- **No test project applies**: `.gitea/workflows/` is pipeline configuration, not an application
project — there is no C# test project for it, mirroring how the Slave API has none (CLAUDE.md).
Verification for this unit is YAML validity plus logical review; real execution requires the
actual Pi, SSH credentials and database, which is explicitly out of scope until Operations (per
U6's Definition of Done in `unit-of-work.md`).
## Steps
- [x] Step 1: Generate `.gitea/workflows/deploy-scp.yaml` — reusable `workflow_call` workflow
- `on.workflow_call.inputs`: `artifact_name`, `environment`, `deploy_path`, `service_name`,
`health_check_url` (all required strings), `run_db_backup` (optional boolean, default `false`),
`transport` (optional string, default `scp`)
- Single `deploy` job, plain shell steps only (D-05 — no container actions)
- Step: download artifact (`actions/download-artifact`)
- Step: production-only DB backup — `if: ${{ inputs.run_db_backup }}`, SSH into the host and
invoke `~/scripts/backup-slpmodularcms-db.sh` (script itself is an Operations/host-setup
artifact, not generated here)
- Step: `sshpass` + `scp` the artifact into `${{ inputs.deploy_path }}/releases/<timestamp>/`
(timestamp computed once, reused across steps via a job output — same pattern the reference
project uses for the `config` job)
- Step: ensure `${{ inputs.deploy_path }}/shared/wwwroot-web` exists (`mkdir -p`, idempotent),
remove the publish output's own `wwwroot/web`, symlink it to the shared directory
- Step: atomically switch `${{ inputs.deploy_path }}/current` to the new release (`ln -sfn`)
- Step: restart `systemctl --user restart ${{ inputs.service_name }}` over SSH
- Step: health check — retry loop, `curl -f` against `${{ inputs.health_check_url }}`, bounded
attempts with a short sleep between tries
- Step: prune releases beyond the retention count of 2, **conditional on the health check having
passed** (`if: success()` on the pruning step, following the preceding step's outcome)
- Inline comments explaining each non-obvious step (Podman container-action failure, why pruning
is conditional, why the timestamp is a job output), matching the reference project's comment
density for anything a future reader would otherwise have to re-derive
- [x] Step 2: Validate YAML
- Parse `deploy-scp.yaml` to confirm it is syntactically valid YAML and produces the exact
`workflow_call` input shape listed above (no test framework applies — see Unit Context)
- Result: valid; 7 inputs (5 required, 2 optional with correct defaults), 1 job, 8 steps —
matches plan exactly
- [x] Step 3: Documentation — `construction/u6-deploy-workflow/code/generation-summary.md`
- What was generated, key decisions carried from Infrastructure Design, what is explicitly
deferred to Operations (host script, real Pi verification, variable/secret runtime values)
## Story / Requirement Traceability
| Step | Covers |
|---|---|
| 1 | FR-02 (reusable workflow, minimum inputs + extensions), FR-03/FR-04 (environment-agnostic — U5 controls when each environment is called), FR-06 (atomic switch, restart), FR-08/ASM-01 (persistent website symlink), FR-20 (production DB backup) |
| 2 | Definition of Done: "workflow YAML valid" |
| 3 | Definition of Done: "deployment sequence documented step by step including the rollback path" (rollback path itself is documented in `infrastructure-design.md` § 4 and carried to Operations — this summary cross-references it, not duplicates it) |
@@ -0,0 +1,66 @@
# Code Generation Summary — U6 Deploy Workflow
**Date**: 2026-07-28
## Files Created
- `.gitea/workflows/deploy-scp.yaml` — reusable `workflow_call` deploy workflow (C-11)
## What It Implements
A single `deploy` job with 8 shell steps, executed in order: compute a release timestamp,
download the build artifact, back up the database (production only, conditional on
`run_db_backup`), upload to a fresh `releases/{timestamp}/` directory over SCP, link the persistent
`shared/wwwroot-web/` into that release, atomically switch `current` and restart the
`systemctl --user` service, verify `/health` with a bounded retry loop, and — only if that check
passed — prune releases beyond the retention count of 2.
Every design decision here traces back to
`construction/u6-deploy-workflow/infrastructure-design/infrastructure-design.md`; this file does not
repeat the rationale, only the resulting code.
## Interface (shared with U5)
| Input | Required | Default |
|---|---|---|
| `artifact_name` | yes | — |
| `environment` | yes | — |
| `deploy_path` | yes | — |
| `service_name` | yes | — |
| `health_check_url` | yes | — |
| `run_db_backup` | no | `false` |
| `transport` | no | `scp` |
U5 (Code Generation next) must call this workflow with `secrets: inherit` and supply all five
required inputs from Gitea variables, per `infrastructure-design.md` § 6.
## Verification (Step 13.5 — this unit's own check)
No C# or frontend test project applies to `.gitea/workflows/` (pipeline configuration, not an
application project — same category exception as the Slave API having none, per CLAUDE.md).
Verification performed:
- **YAML syntax**: parsed successfully with PyYAML. The parser resolves the unquoted `on:` key to
the boolean `True` under YAML 1.1's implicit-boolean rules — this is a PyYAML quirk, not a defect;
the reference project's own working `deploy.yaml` (`K:\Development\SlpSoftware\Projects\SlpSoftware`)
parses identically, and Gitea/GitHub Actions' own workflow parsers treat `on` as a literal key.
- **Structural check**: confirmed 7 `workflow_call` inputs (5 required, 2 optional with the correct
defaults `run_db_backup: false` and `transport: scp`), 1 job (`deploy`), 8 steps — matching the
code generation plan exactly.
- **Logical review**: step ordering matches `infrastructure-design.md` § 4 (backup → upload → link
→ switch/restart → verify → prune-if-passed); no `sudo` used anywhere (Q4 = C); no container-based
actions (D-05).
Real execution — an actual Pi, live SSH credentials, and a real database — is out of scope until
Operations, per U6's Definition of Done in `unit-of-work.md`. That is not a gap in this unit; it is
the documented boundary between Construction and Operations for this specific unit.
## Deferred to Operations (not built here, by design)
- `~/scripts/backup-slpmodularcms-db.sh` on the host (referenced, not created — a host-side script
is out of this repository's scope)
- `loginctl enable-linger` for the deploy user (**INFRA-U6-01**, carried from Infrastructure Design)
- Actual values for `DEPLOY_PATH_*`, `SERVICE_NAME_*`, `HEALTH_CHECK_URL_*` Gitea variables and the
`PI_MAIN_*` secrets
- nginx routing configuration (already exists per the Infrastructure Design's Q10 answer; not part
of this repository)