# 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 1–2); 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 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) |