# Deployment Instructions ## Overview Deployment gebeurt via Gitea Actions, opgesplitst in twee bestanden: - `.gitea/workflows/continuous_integration.yaml` — build/test/lint-gate, plus de `deploy-test`- en `deploy-production`-jobs. - `.gitea/workflows/deploy.yaml` — herbruikbare workflow die `dist/` via SCP naar een omgeving uploadt. Sinds deze stap wordt er automatisch gedeployed naar een **testomgeving**: een Raspberry Pi die de site serveert via nginx, bereikbaar achter een tweede Raspberry Pi met een nginx reverse proxy. Een **productiedeploy** is ook mogelijk, maar bewust niet automatisch — zie "How to Deploy to Production" hieronder. ## Pipeline Files - `continuous_integration.yaml` — getriggerd door `pull_request` (build/test/lint-gate, ongeacht branch), `push` naar `master` (build/test/lint-gate + `deploy-test`), en handmatig via `workflow_dispatch` (met een optioneel `deploy_production`-vinkje). - Heeft bovenaan een `env:`-blok met alle aanpasbare waarden op één plek: `NODE_VERSION`, `PNPM_VERSION`, `ARTIFACT_NAME` (`dist`), `ARTIFACT_NAME_PRODUCTION` (`dist-production`), `ARTIFACT_PATH` (`dist/`), `DEPLOY_ENVIRONMENT` (`test`), `DEPLOY_PATH` (gelezen uit de Gitea repository variable `DEPLOY_PATH_TEST`, zie hieronder — bewust geen hardcoded pad), `DEPLOY_ENVIRONMENT_PRODUCTION` (`production`) en `DEPLOY_PATH_PRODUCTION` (gelezen uit de variable `DEPLOY_PATH_PRODUCTION`). - `prepare` → `build` (test-bundel, `VITE_APP_ENV=test`) → `test` (lint + unit tests) - `build-production` draait ernaast, alléén als `deploy_production` is aangevinkt bij een handmatige `workflow_dispatch`-run. Dit is een aparte build (niet hetzelfde artifact als `build`) omdat `VITE_APP_ENV` een build-time Vite-variabele is: één bundel kan niet tegelijk als `test` én `production` getagd zijn in Sentry/analytics. - Een losse `config`-job zet alle `env`-waarden (test én productie) om in job-outputs (zie hieronder waarom dat nodig is). - `deploy-test` (bij `workflow_dispatch` of een push naar `master`) roept `deploy.yaml` aan met `artifact_name`/`environment`/`deploy_path` afkomstig van `needs.config.outputs.*` (dus indirect uit het `env:`-blok). - `deploy-production` (alléén bij `workflow_dispatch` mét `deploy_production: true`) roept `deploy.yaml` op dezelfde manier aan, maar met de productie-artifact/omgeving/pad. Draait **nooit** automatisch bij een push naar `master`. - `deploy.yaml` — download de artifact (naam/lokaal pad = `inputs.artifact_name`) en upload de inhoud via een `scp`-commando (met `sshpass` voor het wachtwoord) in een gewone shell-stap naar de opgegeven `deploy_path` op de host uit de meegegeven secrets. Deze workflow is omgeving-agnostisch (test/productie) en hoefde niet gewijzigd te worden. ### Waarom `sshpass`/`scp` in een shell-stap in plaats van de `appleboy/scp-action` Docker-action? De oorspronkelijke aanpak gebruikte de `appleboy/scp-action` (een Docker-container-action). Dit werkte niet op deze zelf-gehoste Gitea-runner: de stap faalde met `failed to attach to container: unable to upgrade to tcp, received 409`, een bekende beperking van Podman's Docker-compatibele API, die het attach/log-streaming-mechanisme voor container-based actions niet volledig ondersteunt. De huidige aanpak (een normale `run:`-stap die `sshpass` installeert en zelf `scp` aanroept) heeft geen geneste container nodig en werkt daardoor wel. ### Waarom een aparte `config`-job in plaats van rechtstreeks het `env:`-blok? Gitea/GitHub Actions ondersteunt geen `env`-context in de `with:`-sectie waarmee een reusable workflow wordt aangeroepen (`jobs..with`) — dat werkt alléén binnen `jobs..steps`. De oplossing is een klein voorloop-job (`config`) dat de gewenste `env`-waarden via `$GITHUB_OUTPUT` naar job-outputs schrijft; die outputs (`needs.config.outputs.*`) zijn wél bruikbaar in `jobs..with`. Zo hoef je, om de artifact-naam/pad of de testdeploy-bestemming te wijzigen, alléén het `env:`-blok bovenaan `continuous_integration.yaml` aan te passen — niet de `deploy-test`-job zelf. ## Eenmalige Setup — Gitea Secrets Voeg deze secrets toe in Gitea: **Repository → Settings → Actions → Secrets**: | Secret | Waarde | |---|---| | `PI_MAIN_ADDRESS` | Intern IP-adres van de webserver-Pi (`192.168.1.103`) | | `PI_MAIN_PORT` | SSH-poort (`2224`) | | `PI_MAIN_USERNAME` | SSH-gebruikersnaam (`webadmin`) | | `PI_MAIN_PASSWORD` | Het SSH-wachtwoord van deze gebruiker | Deze secrets heten `PI_MAIN_*` (niet `PI_TEST_*`), omdat dezelfde Pi (Pi Main) en dezelfde inloggegevens naar verwachting ook voor toekomstige omgevingen/webhosts gebruikt worden. Mocht dat later veranderen, dan worden hiervoor alsnog omgeving-specifieke secrets geïntroduceerd. ## Eenmalige Setup — Gitea Variables Voeg deze variable toe in Gitea: **Repository → Settings → Actions → Variables** (geen secret — het is geen gevoelige waarde, net als `VITE_UMAMI_SCRIPT_URL`/`VITE_SENTRY_DSN`): | Variable | Waarde | |---|---| | `DEPLOY_PATH_TEST` | `/html/test/slpsoftware` | | `DEPLOY_PATH_PRODUCTION` | het uploadpad voor productie op Pi Main (bepaal dit zodra de productiemap op de Pi is aangemaakt, analoog aan stap 4 van "Eenmalige Setup — nginx & SSL"; bijv. `/html/slpsoftware` als de nginx `root` `/mnt/storage1/www/html/slpsoftware` wordt) | Dit vervangt het eerder hardcoded `DEPLOY_PATH` in het `env:`-blok van `continuous_integration.yaml`, zodat het upload-pad aangepast kan worden zonder de workflow zelf te wijzigen. Zonder `DEPLOY_PATH_TEST` is `DEPLOY_PATH` leeg en faalt de `deploy-test`-job bij de SCP-upload — deze variable moet dus vóór de eerste deploy zijn ingesteld. Zonder `DEPLOY_PATH_PRODUCTION` faalt op dezelfde manier de `deploy-production`-job; die hoeft pas ingesteld te zijn vóór de eerste keer dat je `deploy_production` aanvinkt. ## Eenmalige Setup — Domeinnaam & DNS - **Test**: `test.slpsoftware.nl` → moet als DNS A-record wijzen naar het publieke IP van de reverse-proxy-Pi. - **Productie** (domein al bekend, nginx/SSL-configuratie op de Pi's moet nog opgezet worden voor de react-frontend): `slpsoftware.nl` (en `www.slpsoftware.nl`) → zelfde reverse-proxy-Pi. De deploy-pipeline zelf ondersteunt productie al (zie hieronder); wat nog ontbreekt is de webroot-map op de webserver-Pi, analoog aan de teststappen hieronder. ## Eenmalige Setup — nginx & SSL op de Raspberry Pi's ### Test 1. Kopieer `operations/deployment/nginx/webserver-nginx.conf.example` naar `/etc/nginx/sites-available/` op de webserver-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Dit bestand is de daadwerkelijk in gebruik zijnde configuratie (`server_name test.slpsoftware.nl`, luistert op poort 80, serveert vanaf `/mnt/storage1/www/html/test/slpsoftware`). 2. Kopieer `operations/deployment/nginx/reverse-proxy-nginx.conf.example` naar `/etc/nginx/sites-available/slpsoftware-test.conf` op de reverse-proxy-Pi, maak een symlink in `sites-enabled/`, en herlaad nginx. Dit is de versie van vóór certbot (alleen poort 80, geen SSL), met `server_name test.slpsoftware.nl`. 3. Vraag op de reverse-proxy-Pi een SSL-certificaat aan met certbot (Let's Encrypt), nadat het DNS-record klopt: `sudo certbot --nginx -d test.slpsoftware.nl`. Certbot herschrijft dit bestand automatisch met de HTTPS-configuratie en de HTTP→HTTPS-redirect — zie `operations/deployment/nginx/reverse-proxy-nginx.conf.post-certbot.example` voor hoe het er dan uitziet (referentie, niet zelf kopiëren). 4. Zorg dat de map `/mnt/storage1/www/html/test/slpsoftware` bestaat op de webserver-Pi en schrijfbaar is voor de gebruiker `webadmin` (bijv. `sudo mkdir -p /mnt/storage1/www/html/test/slpsoftware && sudo chown webadmin:webadmin /mnt/storage1/www/html/test/slpsoftware`). ### Productie De reverse-proxy-Pi bedient op `slpsoftware.nl`/`www.slpsoftware.nl` in werkelijkheid meer dan alleen deze react-frontend-site (o.a. mail/iRedAdmin-proxying naar een aparte host) — dat valt buiten de scope van deze feature. De onderstaande voorbeeldbestanden dekken alléén het react-frontend-gedeelte: 1. Kopieer `operations/deployment/nginx/production-nginx.conf.example` naar `/etc/nginx/sites-available/slpsoftware.conf` op de reverse-proxy-Pi (of voeg het `location`/`server`-gedeelte toe aan een bestaand bestand als daar al andere server-blocks voor dit domein in staan), maak een symlink in `sites-enabled/`, en herlaad nginx. Dit is de versie van vóór certbot (alleen poort 80, geen SSL). 2. Vraag op de reverse-proxy-Pi een SSL-certificaat aan met certbot, nadat de DNS-records kloppen: `sudo certbot --nginx -d slpsoftware.nl -d www.slpsoftware.nl`. Certbot herschrijft dit bestand automatisch — zie `operations/deployment/nginx/production-nginx.conf.post-certbot.example` voor hoe het er dan uitziet (referentie, niet zelf kopiëren). Let op: de acme-challenge location hoort in het 443-blok, niet in het losse poort-80-blok — zie de toelichting in dat referentiebestand. 3. Zorg dat de productie-webroot-map bestaat op de webserver-Pi en schrijfbaar is voor `webadmin` (bijv. `sudo mkdir -p /mnt/storage1/www/html/slpsoftware && sudo chown webadmin:webadmin /mnt/storage1/www/html/slpsoftware`), analoog aan stap 4 van de testomgeving. > **Waarom `deploy_path` en de nginx `root` niet hetzelfde pad zijn**: de pipeline uploadt via SCP naar `deploy_path` = `/html/test/slpsoftware` (de waarde van de Gitea variable `DEPLOY_PATH_TEST`, ingelezen via `env.DEPLOY_PATH` in `continuous_integration.yaml`), terwijl de nginx `root` in `webserver-nginx.conf.example` het volledige pad `/mnt/storage1/www/html/test/slpsoftware` is. Dit is geen fout of inconsistentie: de SSH/SCP-gebruiker (`webadmin`) heeft `/mnt/storage1/www` als root (vergelijkbaar met een FTP-chroot), dus vanuit het perspectief van deze gebruiker is `/html/test/slpsoftware` het juiste (relatieve) pad, terwijl dat op het bestandssysteem van de Pi zelf overeenkomt met het volledige pad `/mnt/storage1/www/html/test/slpsoftware` dat nginx als `root` gebruikt. Kortom: `deploy_path` (`/html/test/slpsoftware`) + de root van de `webadmin`-gebruiker (`/mnt/storage1/www`) = de nginx `root` (`/mnt/storage1/www/html/test/slpsoftware`). ## How to Deploy to Test ### Automatisch Merge een pull request naar `master` — de `deploy-test` job draait dan automatisch na een groene build/test-run. ### Handmatig 1. In Gitea, open de repository's **Actions** tab. 2. Selecteer de **Continuous Integration** workflow. 3. Klik **Run workflow**, kies de gewenste branch/ref, en start. ## How to Deploy to Production Productie deployt **nooit** automatisch bij een push naar `master` — alleen via een expliciete, handmatige actie: 1. In Gitea, open de repository's **Actions** tab. 2. Selecteer de **Continuous Integration** workflow. 3. Klik **Run workflow**, kies de gewenste branch/ref (meestal `master`). 4. Vink **`deploy_production`** aan voordat je de run start. 5. Dit triggert naast de gebruikelijke `build`/`test`/`deploy-test` ook `build-production` en `deploy-production`. Vereist eenmalig vooraf: - De Gitea-variable `DEPLOY_PATH_PRODUCTION` (zie boven). - De Gitea-variable `VITE_UMAMI_WEBSITE_ID_PRODUCTION` (zie `umami-setup.md` stap 4/5) — zónder deze wordt productieverkeer per ongeluk meegeteld bij de teststatistieken. - De productie-nginx/SSL-configuratie + webroot-map op de Pi's (zie "Eenmalige Setup — nginx & SSL op de Raspberry Pi's → Productie"). ## Verifying a Deployment 1. Bevestig dat de Gitea Actions run succesvol is (alle relevante jobs groen — `deploy-test` altijd, `build-production`/`deploy-production` alleen als je `deploy_production` had aangevinkt). 2. Open de omgeving in de browser (test: via het adres/IP dat je bij de reverse-proxy hebt ingesteld; productie: `slpsoftware.nl` zodra de nginx/SSL-setup daar staat) en controleer dat de site correct laadt (check de browserconsole op fouten, zoals in de handmatige smoke test in `construction/build-and-test/integration-test-instructions.md`). ## Future Work - **Van wachtwoord naar SSH-key**: vervang `sshpass -p "${{ secrets.PI_MAIN_PASSWORD }}" scp ...` in `deploy.yaml` door een `scp`-commando met `-i ` (een nieuwe secret `PI_MAIN_SSH_KEY` die je eerst als bestand wegschrijft in de run-stap), en zet de bijbehorende public key in `~/.ssh/authorized_keys` van de `webadmin`-gebruiker op de webserver-Pi. Verwijder daarna het wachtwoord-secret. - **Webserver-Pi-vhost voor productie**: er is nog geen `webserver-nginx.conf.example`-tegenhanger voor productie (de vhost op de webserver-Pi zelf die `slpsoftware.nl` serveert vanaf de productie-webroot). Bouw die op naar analogie van `nginx/webserver-nginx.conf.example`, met `server_name slpsoftware.nl www.slpsoftware.nl` en de productie-webroot als `root`. - **Apart productie-Sentry-project**: `build-production` gebruikt momenteel dezelfde `VITE_SENTRY_DSN` als de testbuild (Umami heeft al aparte website-ID's per omgeving, zie `umami-setup.md`). Overweeg dit te splitsen zodra test- en productie-events niet meer door elkaar gemengd mogen worden in hetzelfde Sentry-project.