Maak DEPLOY_PATH configureerbaar via Gitea Actions variable i.p.v. hardcoded #7

Merged
Sluijsens merged 10 commits from feature/production_deploy_automation into master 2026-07-30 23:58:06 +02:00
9 changed files with 337 additions and 50 deletions
+109 -6
View File
@@ -1,6 +1,11 @@
name: Continuous Integration
on:
workflow_dispatch: {}
workflow_dispatch:
inputs:
deploy_production:
description: 'Na een succesvolle build/test ook naar productie deployen (naast de automatische testdeploy)?'
type: boolean
default: false
pull_request:
types: [opened, synchronize, reopened]
push:
@@ -16,9 +21,16 @@ env:
NODE_VERSION: '20'
PNPM_VERSION: '9'
ARTIFACT_NAME: dist
ARTIFACT_NAME_PRODUCTION: dist-production
ARTIFACT_PATH: dist/
DEPLOY_ENVIRONMENT: test
DEPLOY_PATH: /html/test/slpsoftware
# Niet hardcoded: DEPLOY_PATH komt uit een Gitea Actions repository variable
# (Repository → Settings → Actions → Variables), zodat het pad per omgeving
# aangepast kan worden zonder de workflow zelf te wijzigen. Zie
# deployment-instructions.md voor de eenmalige setup van deze variable.
DEPLOY_PATH: ${{ vars.DEPLOY_PATH_TEST }}
DEPLOY_ENVIRONMENT_PRODUCTION: production
DEPLOY_PATH_PRODUCTION: ${{ vars.DEPLOY_PATH_PRODUCTION }}
jobs:
# Geeft de env-variabelen hierboven door als job-outputs, zodat ze ook
@@ -29,14 +41,20 @@ jobs:
runs-on: ubuntu-latest
outputs:
artifact_name: ${{ steps.set.outputs.artifact_name }}
artifact_name_production: ${{ steps.set.outputs.artifact_name_production }}
deploy_environment: ${{ steps.set.outputs.deploy_environment }}
deploy_path: ${{ steps.set.outputs.deploy_path }}
deploy_environment_production: ${{ steps.set.outputs.deploy_environment_production }}
deploy_path_production: ${{ steps.set.outputs.deploy_path_production }}
steps:
- id: set
run: |
echo "artifact_name=${{ env.ARTIFACT_NAME }}" >> "$GITHUB_OUTPUT"
echo "artifact_name_production=${{ env.ARTIFACT_NAME_PRODUCTION }}" >> "$GITHUB_OUTPUT"
echo "deploy_environment=${{ env.DEPLOY_ENVIRONMENT }}" >> "$GITHUB_OUTPUT"
echo "deploy_path=${{ env.DEPLOY_PATH }}" >> "$GITHUB_OUTPUT"
echo "deploy_environment_production=${{ env.DEPLOY_ENVIRONMENT_PRODUCTION }}" >> "$GITHUB_OUTPUT"
echo "deploy_path_production=${{ env.DEPLOY_PATH_PRODUCTION }}" >> "$GITHUB_OUTPUT"
prepare:
runs-on: ubuntu-latest
@@ -94,7 +112,12 @@ jobs:
- name: Build
env:
VITE_UMAMI_SCRIPT_URL: ${{ vars.VITE_UMAMI_SCRIPT_URL }}
VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID }}
# Gitea-variable heet VITE_UMAMI_WEBSITE_ID_TEST (niet zonder suffix),
# analoog aan VITE_UMAMI_WEBSITE_ID_PRODUCTION in de build-production
# job hieronder. Linkerkant (VITE_UMAMI_WEBSITE_ID) is wél altijd
# gelijk: dat is de Vite-buildtime-envvar-naam die de app verwacht
# (zie src/components/UmamiAnalytics.tsx), niet aan te passen.
VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_TEST }}
# Sentry DSN is niet gevoelig (veilig om in de client-bundle te zitten),
# daarom een Gitea Actions "vars"-waarde i.p.v. een secret. Optioneel:
# als deze niet is ingesteld, wordt Sentry-logging simpelweg overgeslagen
@@ -102,8 +125,9 @@ jobs:
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
# Build-time tag die bepaalt of dev/test-only UI (zoals de tijdelijke
# SentryTestButton) zichtbaar is; zie src/components/SentryTestButton.tsx.
# Zolang er nog geen aparte productie-build/deploy bestaat, is dit altijd
# gelijk aan DEPLOY_ENVIRONMENT ('test').
# Dit is de testomgeving-build, dus altijd gelijk aan DEPLOY_ENVIRONMENT
# ('test'). Zie de build-production job hieronder voor de aparte
# productie-build met VITE_APP_ENV=production.
VITE_APP_ENV: ${{ env.DEPLOY_ENVIRONMENT }}
run: pnpm run build
@@ -114,6 +138,65 @@ jobs:
path: ${{ env.ARTIFACT_PATH }}
retention-days: 1
# Aparte build voor productie, alleen nodig/gedraaid als deploy_production
# is aangevinkt bij een handmatige workflow_dispatch-run. Dit bestaat naast
# de gewone `build`-job (in plaats van die job te hergebruiken) omdat
# VITE_APP_ENV een build-time Vite-variabele is: één en dezelfde dist/-bundel
# kan niet zowel als 'test' als 'production' getagd zijn. Zonder deze aparte
# build zou de test-bundel (met environment: test) naar productie
# gedeployed worden, wat Sentry-events/analytics verkeerd zou taggen.
build-production:
needs: prepare
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_production == 'true'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: Get pnpm store directory
id: pnpm-store
run: echo "path=$(pnpm store path)" >> "$GITHUB_OUTPUT"
- name: Restore pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.path }}
key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build
env:
# Umami-website-ID's zijn NIET gedeeld tussen omgevingen: elke Umami
# "website"-entry (test.slpsoftware.nl vs slpsoftware.nl) heeft een
# eigen ID, anders komt productieverkeer in de teststatistieken
# terecht (of andersom). Vereist dus een eigen Umami-website +
# Gitea-variable VITE_UMAMI_WEBSITE_ID_PRODUCTION — zie umami-setup.md.
VITE_UMAMI_SCRIPT_URL: ${{ vars.VITE_UMAMI_SCRIPT_URL }}
VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_PRODUCTION }}
# VITE_SENTRY_DSN wordt wél gedeeld met de testbuild: één Sentry-
# project voor beide omgevingen, VITE_APP_ENV hieronder tagt de
# events al als 'test' vs 'production'. Splits dit pas op als je
# ooit aparte Sentry-projecten per omgeving wilt.
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
VITE_APP_ENV: ${{ env.DEPLOY_ENVIRONMENT_PRODUCTION }}
run: pnpm run build
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: ${{ env.ARTIFACT_NAME_PRODUCTION }}
path: ${{ env.ARTIFACT_PATH }}
retention-days: 1
test:
needs: build
runs-on: ubuntu-latest
@@ -149,7 +232,11 @@ jobs:
deploy-test:
needs: [build, test, config]
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/master')
# TIJDELIJK: 'pull_request' is toegevoegd zodat elke PR ook automatisch
# naar de testomgeving deployed, om testen tijdens deze werkbranch te
# vereenvoudigen. Verwijder de 'pull_request'-conditie hieronder weer
# zodra dat niet meer nodig is.
if: github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/master')
uses: ./.gitea/workflows/deploy.yaml
secrets: inherit
# Deze waarden komen uit het `env:`-blok bovenaan dit bestand, via de
@@ -159,3 +246,19 @@ jobs:
artifact_name: ${{ needs.config.outputs.artifact_name }}
environment: ${{ needs.config.outputs.deploy_environment }}
deploy_path: ${{ needs.config.outputs.deploy_path }}
# Productie-deploy is bewust NIET automatisch bij elke push naar master
# (in tegenstelling tot deploy-test hierboven): dit is pas een expliciete,
# bewuste actie via workflow_dispatch met het "deploy_production"-vinkje
# aangevinkt. Zo blijft de bestaande testdeploy-flow ongewijzigd en kan
# niemand per ongeluk productie deployen door simpelweg naar master te
# pushen of de workflow handmatig te starten zonder dat vinkje.
deploy-production:
needs: [build-production, test, config]
if: github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_production == 'true'
uses: ./.gitea/workflows/deploy.yaml
secrets: inherit
with:
artifact_name: ${{ needs.config.outputs.artifact_name_production }}
environment: ${{ needs.config.outputs.deploy_environment_production }}
deploy_path: ${{ needs.config.outputs.deploy_path_production }}
+16 -3
View File
@@ -37,10 +37,23 @@ jobs:
# voor containeracties niet volledig ondersteunt. Een scp-commando
# in een normale run-stap heeft die geneste container niet nodig.
- name: Upload dist to ${{ inputs.environment }} web server via SCP
env:
# Secrets via env: in plaats van rechtstreeks in het `run:`-script
# ge-interpoleerd: Gitea Actions plakt ${{ secrets.* }} als platte
# tekst in het script vóórdat bash het uitvoert. Staat er een
# shell-metateken in de waarde (zoals '$' of '`'), dan probeert
# bash dat alsnog te interpreteren, waardoor een ander wachtwoord
# bij sshpass terechtkomt dan verwacht ("Permission denied"). Via
# env: krijgt bash de waarde als kant-en-klare string doorgegeven,
# zonder die tweede interpretatieslag.
SSHPASS: ${{ secrets.PI_MAIN_PASSWORD }}
PI_MAIN_PORT: ${{ secrets.PI_MAIN_PORT }}
PI_MAIN_USERNAME: ${{ secrets.PI_MAIN_USERNAME }}
PI_MAIN_ADDRESS: ${{ secrets.PI_MAIN_ADDRESS }}
run: |
sudo apt-get update && sudo apt-get install -y sshpass
sshpass -p "${{ secrets.PI_MAIN_PASSWORD }}" scp \
-P ${{ secrets.PI_MAIN_PORT }} \
sshpass -e scp \
-P "$PI_MAIN_PORT" \
-o StrictHostKeyChecking=no \
-r ${{ inputs.artifact_name }}/* \
${{ secrets.PI_MAIN_USERNAME }}@${{ secrets.PI_MAIN_HOST }}:${{ inputs.deploy_path }}
"$PI_MAIN_USERNAME@$PI_MAIN_ADDRESS:${{ inputs.deploy_path }}"
@@ -2,18 +2,20 @@
## Overview
Deployment gebeurt via Gitea Actions, opgesplitst in twee bestanden:
- `.gitea/workflows/continuous_integration.yaml` — build/test/lint-gate, plus de `deploy-test` job.
- `.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.
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`.
- Heeft bovenaan een `env:`-blok met alle aanpasbare waarden op één plek: `NODE_VERSION`, `PNPM_VERSION`, `ARTIFACT_NAME` (`dist`), `ARTIFACT_PATH` (`dist/`), `DEPLOY_ENVIRONMENT` (`test`) en `DEPLOY_PATH` (`/html/test/slpsoftware`).
- `prepare``build` (uploadt de artifact, naam/pad uit `env.ARTIFACT_NAME`/`env.ARTIFACT_PATH`) → `test` (lint + unit tests)
- Een losse `config`-job zet deze `env`-waarden om in job-outputs (zie hieronder waarom dat nodig is).
- `deploy-test` (alleen 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.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.
- `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.
@@ -26,24 +28,41 @@ Voeg deze secrets toe in Gitea: **Repository → Settings → Actions → Secret
| Secret | Waarde |
|---|---|
| `PI_MAIN_HOST` | Intern IP-adres van de webserver-Pi (`192.168.1.103`) |
| `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** (nog niet automatisch gedeployed, maar domein al bekend): `slpsoftware.nl` (en `www.slpsoftware.nl`) → zelfde reverse-proxy-Pi, zodra productie wordt opgezet.
- **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`).
> **Waarom `deploy_path` en de nginx `root` niet hetzelfde pad zijn**: de pipeline uploadt via SCP naar `deploy_path` = `/html/test/slpsoftware` (zie `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`).
### 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
@@ -54,10 +73,24 @@ Merge een pull request naar `master` — de `deploy-test` job draait dan automat
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 jobs groen, inclusief `deploy-test`).
2. Open de testomgeving in de browser (via het adres/IP dat je bij de reverse-proxy hebt ingesteld) 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`).
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 <key-bestand>` (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.
- **Productie-omgeving**: voeg een `deploy-production`-job toe zodra de definitieve productiehosting bekend is (zie `deployment-plan.md`'s "Open Item"). De productie-nginx-voorbeeldconfiguratie is op verzoek van de gebruiker verwijderd totdat er een goed-werkende versie is; die kan later opnieuw opgebouwd worden naar analogie van `nginx/webserver-nginx.conf.example` en `nginx/reverse-proxy-nginx.conf.example`, met domein `slpsoftware.nl`.
- **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.
@@ -10,24 +10,24 @@ Sinds deze stap is er een echte, geautomatiseerde upload naar een **testomgeving
## How It Works
1. Bij elke pull request draait automatisch de build/test/lint-gate (`prepare``build``test`), zodat merge requests direct gevalideerd worden.
2. Zodra een pull request naar `master` gemerged wordt (of de workflow handmatig via `workflow_dispatch` gestart wordt), draait aanvullend de `deploy-test` job.
3. `deploy-test` roept de herbruikbare `deploy.yaml` workflow aan met `artifact_name`/`environment`/`deploy_path`, en geeft via `secrets: inherit` de Pi-inloggegevens door. Deze drie waarden (samen met de artifact-naam/pad die de `build`-job gebruikt) staan als variabelen in het `env:`-blok bovenaan `continuous_integration.yaml` (`ARTIFACT_NAME`, `ARTIFACT_PATH`, `DEPLOY_ENVIRONMENT`, `DEPLOY_PATH`), en worden via een kleine `config`-job als job-outputs doorgegeven aan `deploy-test` (nodig omdat de `env`-context zelf niet werkt in de `with:`-sectie van een reusable-workflow-aanroep).
3. `deploy-test` roept de herbruikbare `deploy.yaml` workflow aan met `artifact_name`/`environment`/`deploy_path`, en geeft via `secrets: inherit` de Pi-inloggegevens door. Deze drie waarden (samen met de artifact-naam/pad die de `build`-job gebruikt) staan als variabelen in het `env:`-blok bovenaan `continuous_integration.yaml` (`ARTIFACT_NAME`, `ARTIFACT_PATH`, `DEPLOY_ENVIRONMENT`, `DEPLOY_PATH`), en worden via een kleine `config`-job als job-outputs doorgegeven aan `deploy-test` (nodig omdat de `env`-context zelf niet werkt in de `with:`-sectie van een reusable-workflow-aanroep). `DEPLOY_PATH` zelf is geen hardcoded waarde meer, maar wordt gelezen uit de Gitea repository variable `DEPLOY_PATH_TEST` (`Repository → Settings → Actions → Variables`), zodat het uploadpad aangepast kan worden zonder de workflow te wijzigen — zie `deployment-instructions.md`.
4. `deploy.yaml` downloadt de artifact en uploadt de inhoud via een `scp`-commando (met `sshpass` voor het wachtwoord) in een gewone shell-stap naar de webserver-Pi op het interne netwerk (`192.168.1.103`, poort `2224`). Dit vervangt de eerdere `appleboy/scp-action` (Docker-container-action), die faalde op de zelf-gehoste Podman-runner (`failed to attach to container: unable to upgrade to tcp, received 409`).
5. nginx op de webserver-Pi serveert de bestanden vanaf `/mnt/storage1/www/html/test/slpsoftware`; de reverse-proxy-Pi stuurt binnenkomend verkeer door naar deze webserver-Pi. Voorbeeldconfiguraties staan in `operations/deployment/nginx/` en zijn de daadwerkelijk in gebruik zijnde configuraties (niet langer illustratieve concepten).
6. De reverse-proxy-Pi is ook verantwoordelijk voor SSL: certificaten worden net als voorheen aangevraagd via certbot (Let's Encrypt) en HTTP-verkeer wordt doorverwezen naar HTTPS.
## Environments
- **Test** (nieuw, geautomatiseerd): zoals hierboven beschreven — de enige omgeving die op dit moment daadwerkelijk automatisch gedeployed wordt. Domeinnaam: `test.slpsoftware.nl` (SSL via certbot op de reverse-proxy-Pi).
- **Productie**: nog niet geautomatiseerd. Zodra de definitieve productiehosting bekend is, kan een vergelijkbare `deploy-production`-job worden toegevoegd die `deploy.yaml` aanroept met `environment: production` en de productie-secrets/pad. Domeinnaam ligt al vast: `slpsoftware.nl` (SSL eveneens via certbot). Er is (nog) geen productie-nginx-voorbeeldconfiguratie; deze is op verzoek verwijderd totdat er een goed-werkende, foutloze versie is, en kan later opnieuw opgebouwd worden naar analogie van de testomgeving-configuraties.
- **Test** (geautomatiseerd, altijd): zoals hierboven beschreven. Draait automatisch bij elke merge naar `master` en bij elke handmatige `workflow_dispatch`-run. Domeinnaam: `test.slpsoftware.nl` (SSL via certbot op de reverse-proxy-Pi).
- **Productie** (geautomatiseerd, opt-in): via een eigen `build-production`- en `deploy-production`-job in `continuous_integration.yaml`, die `deploy.yaml` aanroepen met `environment: production`. In tegenstelling tot de testdeploy draait dit **niet** automatisch bij een push naar `master` — alleen wanneer je de workflow handmatig start via `workflow_dispatch` mét het `deploy_production`-vinkje aangevinkt. Dit is bewust: zo kan niemand per ongeluk productie deployen door simpelweg naar `master` te pushen. Reden voor een aparte `build-production`-job (in plaats van hetzelfde artifact als de testbuild te hergebruiken): `VITE_APP_ENV` is een build-time Vite-variabele, dus één bundel kan niet tegelijk als `test` én `production` getagd zijn in Sentry/analytics. Domeinnaam: `slpsoftware.nl` (SSL eveneens via certbot). Productie-nginx-voorbeeldconfiguratie: `nginx/production-nginx.conf.example` (vóór certbot) en `nginx/production-nginx.conf.post-certbot.example` (referentie, hoe het bestand er na certbot uitziet) — dekt alléén de react-frontend-site; de daadwerkelijke productieserver regelt op hetzelfde domein ook mail/iRedAdmin-proxying, wat buiten de scope van deze feature valt. Vereist eenmalig de Gitea-variable `DEPLOY_PATH_PRODUCTION` (zie `deployment-instructions.md`) — zonder deze faalt de upload.
## Automation Level
Volledig geautomatiseerd voor de testomgeving: build, test, lint én upload naar de test-Pi gebeuren zonder handmatige tussenstap, zodra er gemerged wordt naar `master` (of handmatig getriggerd wordt). Alleen productie is nog niet geautomatiseerd.
Volledig geautomatiseerd voor de testomgeving: build, test, lint én upload naar de test-Pi gebeuren zonder handmatige tussenstap, zodra er gemerged wordt naar `master` (of handmatig getriggerd wordt). Productie is ook geautomatiseerd, maar alleen als bewuste, expliciete actie (handmatige `workflow_dispatch` met het `deploy_production`-vinkje) — nooit automatisch bij een push.
## Rollback Strategy
Zie `rollback-plan.md` — voor de testomgeving kan een eerdere commit/branch opnieuw gebouwd en geüpload worden door de workflow opnieuw te triggeren.
## Secrets & Configuration
Voor de testomgeving zijn de volgende Gitea Actions Secrets (repository-niveau) vereist:
- `PI_MAIN_HOST``192.168.1.103` (intern IP van de webserver-Pi)
- `PI_MAIN_ADDRESS``192.168.1.103` (intern IP van de webserver-Pi)
- `PI_MAIN_PORT``2224`
- `PI_MAIN_USERNAME``webadmin`
- `PI_MAIN_PASSWORD` — het SSH-wachtwoord van deze gebruiker
@@ -36,8 +36,8 @@ Deze secrets heten bewust `PI_MAIN_*` in plaats van `PI_TEST_*`: alle webhosts g
Dit is bewust wachtwoord-authenticatie (voor nu, zoals gekozen), zodat de testomgeving snel werkend is. Zie "Future Work" in `deployment-instructions.md` voor de overstap naar SSH-key-authenticatie.
## Open Item — Productie-deploy Nog Niet Geautomatiseerd
Zodra de definitieve productiehosting bekend is (en of dit dezelfde soort Raspberry Pi-opstelling is, of een externe hostingpartij), voeg een `deploy-production`-job toe aan `continuous_integration.yaml` die `deploy.yaml` aanroept met `environment: production`. Zolang het dezelfde webhost (Pi Main) blijft, kunnen de bestaande `PI_MAIN_*` secrets hergebruikt worden; pas dit pas aan naar omgeving-specifieke secrets als productie daadwerkelijk op een andere host komt. Domeinnaam (`slpsoftware.nl`) en SSL-aanpak (certbot/Let's Encrypt op de reverse-proxy-Pi) liggen al vast; er is bewust (nog) geen productie-nginx-voorbeeldconfiguratie, deze wordt later opnieuw opgebouwd zodra er een goed-werkende, foutloze versie is.
## Resolved Item — Productie-deploy Geautomatiseerd
`continuous_integration.yaml` bevat nu een `build-production`- en `deploy-production`-job, alleen actief bij een handmatige `workflow_dispatch`-run met het `deploy_production`-vinkje aangevinkt (zie "Environments" hierboven). Aangezien de webhost (Pi Main) dezelfde blijft als de testomgeving, worden de bestaande `PI_MAIN_*` secrets hergebruikt pas dit pas aan naar omgeving-specifieke secrets als productie daadwerkelijk op een andere host komt. Domeinnaam (`slpsoftware.nl`) en SSL-aanpak (certbot/Let's Encrypt op de reverse-proxy-Pi) liggen al vast; zie `nginx/production-nginx.conf.example` / `nginx/production-nginx.conf.post-certbot.example` voor de voorbeeldconfiguratie (react-frontend-only, geen mail/iRedAdmin). **Voordat dit voor het eerst gebruikt wordt**, moet de Gitea-variable `DEPLOY_PATH_PRODUCTION` nog worden aangemaakt (zie `deployment-instructions.md`) — zonder deze faalt de upload.
## Verified Build Prerequisite
Dit plan bouwt voort op de Build and Test-stage (`construction/build-and-test/build-and-test-summary.md`): `pnpm run build` produceert een statische `dist/`-bundel zonder server-side vereisten, geschikt om direct door nginx geserveerd te worden.
@@ -0,0 +1,55 @@
# Voorbeeldconfiguratie voor de nginx reverse proxy op de reverse-proxy-Pi
# (dezelfde Pi als reverse-proxy-nginx.conf.example) voor de PRODUCTIEOMGEVING
# van de react-frontend, bereikbaar via slpsoftware.nl / www.slpsoftware.nl.
#
# Dit bestand dekt ALLEEN de react-frontend-site. De daadwerkelijke productie-
# server (pi-entry) regelt op hetzelfde domein ook mail/iRedAdmin-proxying
# (naar een Odroid C4 op 192.168.1.104) — dat zijn aanvullende server-blocks
# die niets met deze feature te maken hebben en bewust buiten dit voorbeeld
# gelaten zijn. Voeg ze desgewenst zelf toe naast dit blok.
#
# Dit is de versie die je gebruikt VOORDAT certbot gedraaid heeft: alleen
# poort 80, geen SSL. Certbot heeft dit HTTP-server-block namelijk nodig om
# de ACME-challenge te kunnen afhandelen en zal, zodra je hem draait, dit
# bestand zelf herschrijven om er de HTTPS-configuratie en de HTTP→HTTPS-
# redirect aan toe te voegen. Zie production-nginx.conf.post-certbot.example
# voor hoe het bestand er na die stap uit gaat zien (puur ter referentie —
# dat bestand hoef je niet zelf te kopiëren, certbot genereert het).
#
# Kopieer dit bestand handmatig naar
# /etc/nginx/sites-available/slpsoftware.conf op de reverse-proxy-Pi, maak
# een symlink in sites-enabled, herlaad nginx, en draai dan pas certbot:
# sudo certbot --nginx -d slpsoftware.nl -d www.slpsoftware.nl
# Zorg dat de DNS-records voor beide domeinen al naar het publieke IP van
# deze Pi wijzen voordat je certbot draait.
server {
listen 80;
listen [::]:80;
server_name slpsoftware.nl www.slpsoftware.nl;
error_log /var/log/nginx/slpsoftware_error.log;
access_log /var/log/nginx/slpsoftware_access.log;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
# Zelfde Sentry-tunnel-endpoint als de testomgeving (zie
# reverse-proxy-nginx.conf.example): VITE_SENTRY_DSN is bewust gedeeld
# tussen test en productie (één Sentry-project, environment-tag
# onderscheidt ze), dus dezelfde org-/project-id hieronder is correct.
location /sentry-tunnel {
proxy_pass https://o4511795618185216.ingest.de.sentry.io/api/4511795622838352/envelope/;
proxy_set_header Host o4511795618185216.ingest.de.sentry.io;
proxy_ssl_server_name on;
}
location / {
proxy_pass http://192.168.1.103:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
@@ -0,0 +1,69 @@
# REFERENTIE ALLEEN — dit bestand hoef je niet handmatig te kopiëren.
#
# Dit toont hoe /etc/nginx/sites-available/slpsoftware.conf op de
# reverse-proxy-Pi er automatisch uit komt te zien NADAT je certbot hebt
# gedraaid (`sudo certbot --nginx -d slpsoftware.nl -d www.slpsoftware.nl`)
# op basis van production-nginx.conf.example. Certbot voegt zelf de HTTPS-
# configuratie en het HTTP→HTTPS-redirect-blok toe (herkenbaar aan de
# "managed by Certbot" commentaren), en zet de error_log/access_log en
# overige location-blocks gewoon over naar het nieuwe HTTPS-serverblok.
#
# Let op de plek van de acme-challenge location: die hoort in dit 443-blok,
# niet in het losse poort-80-blok onderaan. De "if ($host = ...)"-redirects
# daar worden door nginx in de rewrite-fase uitgevoerd, vóór location-
# matching — een acme-challenge location in dat blok zou dus alsnog altijd
# overruled worden door de redirect, en de eerstvolgende certificate-
# renewal zou stilzwijgend falen.
#
# Dit bestand dekt ALLEEN de react-frontend-site (geen mail/iRedAdmin-
# proxying) — zie production-nginx.conf.example voor de toelichting.
server {
server_name slpsoftware.nl www.slpsoftware.nl;
error_log /var/log/nginx/slpsoftware_error.log;
access_log /var/log/nginx/slpsoftware_access.log;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location /sentry-tunnel {
proxy_pass https://o4511795618185216.ingest.de.sentry.io/api/4511795622838352/envelope/;
proxy_set_header Host o4511795618185216.ingest.de.sentry.io;
proxy_ssl_server_name on;
}
location / {
proxy_pass http://192.168.1.103:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
listen 443 ssl; # managed by Certbot
listen [::]:443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/slpsoftware.nl/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/slpsoftware.nl/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = www.slpsoftware.nl) {
return 301 https://$host$request_uri;
} # managed by Certbot
if ($host = slpsoftware.nl) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name slpsoftware.nl www.slpsoftware.nl;
return 404; # managed by Certbot
}
@@ -28,7 +28,7 @@ A combination of:
## Open Action Items
1. ~~Decide logging destination~~**Resolved and verified**: Sentry free tier, wired into `ErrorBoundary`/`main.tsx` (see `monitoring-setup.md`), including tracing, environment/release tags, and a tunnel to bypass ad-blocker blocking. The user confirmed errors, logs, and metrics are received correctly, both locally and on the test environment. The Gitea Actions variable `VITE_SENTRY_DSN` still needs to be created by the user with a real Sentry project DSN for the test/production build; until then that build simply skips Sentry initialization and only console-logging is active (local development already has its own DSN via `.env.local`).
2. ~~Pick concrete analytics + uptime tools~~**Resolved**: self-hosted Umami + UptimeRobot (see `monitoring-setup.md` and `umami-setup.md`). Remaining manual follow-ups: actually deploying the Umami containers on the Pi, registering DNS/SSL for `analytics.slpsoftware.nl`, creating the UptimeRobot monitor, and setting the `VITE_UMAMI_SCRIPT_URL`/`VITE_UMAMI_WEBSITE_ID` Gitea repository variables.
2. ~~Pick concrete analytics + uptime tools~~**Resolved**: self-hosted Umami + UptimeRobot (see `monitoring-setup.md` and `umami-setup.md`). Remaining manual follow-ups: actually deploying the Umami containers on the Pi, registering DNS/SSL for `analytics.slpsoftware.nl`, creating the UptimeRobot monitor, and setting the `VITE_UMAMI_SCRIPT_URL`/`VITE_UMAMI_WEBSITE_ID_TEST` (and, once production is set up, `VITE_UMAMI_WEBSITE_ID_PRODUCTION`) Gitea repository variables.
## Rationale
Given this is a simple static marketing site with no backend and no existing monitoring, the aim is lightweight, low/no-cost observability: enough to know if the site is down (uptime) and how it's being used (analytics), plus a documented (if not yet finalized) path for capturing client-side errors. Alerting was deliberately left out to avoid over-engineering a notification pipeline before there's a concrete trigger/audience for it.
@@ -185,28 +185,41 @@ uit stap 0.3 is hiervoor al gezet, dus deze service blijft ook draaien zonder da
wijzigen na eerste login**).
## 4. Website registreren in Umami en het website-ID ophalen
Maak een **aparte Umami-website-entry per omgeving** aan — test en productie zijn
verschillende domeinen en horen dus niet in dezelfde statistieken te belanden:
1. Log in op `https://analytics.slpsoftware.nl` en wijzig direct het standaardwachtwoord.
2. Ga naar **Settings → Websites → Add website** en vul in:
- Name: `SLP Software` (of naar keuze)
- Domain: het domein van de daadwerkelijke website (bv. `slpsoftware.nl` of
`test.slpsoftware.nl`, afhankelijk van welke omgeving je eerst wilt meten)
3. Na het opslaan toont Umami een **Website ID** (een UUID) — dit heb je nodig voor de
volgende stap.
2. Ga naar **Settings → Websites → Add website** en maak een entry voor de testomgeving:
- Name: `SLP Software (test)` (of naar keuze)
- Domain: `test.slpsoftware.nl`
3. Na het opslaan toont Umami een **Website ID** (een UUID) voor deze entry — nodig voor
`VITE_UMAMI_WEBSITE_ID_TEST` in de volgende stap.
4. Herhaal stap 2-3 voor productie zodra die omgeving wordt opgezet: een tweede website-
entry met Domain `slpsoftware.nl`, met een eigen Website ID voor
`VITE_UMAMI_WEBSITE_ID_PRODUCTION`.
## 5. Tracking script koppelen aan de website (build-configuratie)
De React-app (`src/components/UmamiAnalytics.tsx`) injecteert het Umami tracking-script
automatisch, mits de volgende twee build-time variabelen zijn ingesteld — beide zijn
**geen secrets** (client-side zichtbaar), dus als Gitea Actions **repository variables**
(niet secrets), net als `VITE_SENTRY_DSN`:
automatisch, mits de volgende build-time variabelen zijn ingesteld — geen van deze zijn
**secrets** (client-side zichtbaar), dus als Gitea Actions **repository variables**
(niet secrets), net als `VITE_SENTRY_DSN`. Omgevingsspecifieke variabelen (die per
omgeving een andere waarde hebben) krijgen consistent een `_TEST`/`_PRODUCTION`-postfix;
gedeelde variabelen (die bewust voor beide omgevingen identiek zijn) niet:
| Variabele | Waarde |
|---|---|
| `VITE_UMAMI_SCRIPT_URL` | `https://analytics.slpsoftware.nl/script.js` |
| `VITE_UMAMI_WEBSITE_ID` | het Website ID uit stap 4 |
| `VITE_UMAMI_SCRIPT_URL` | `https://analytics.slpsoftware.nl/script.js` (gedeeld tussen test en productie — zelfde Umami-instance/endpoint voor beide, geen postfix) |
| `VITE_UMAMI_WEBSITE_ID_TEST` | het Website ID van de **test**-website-entry uit stap 4 |
| `VITE_UMAMI_WEBSITE_ID_PRODUCTION` | het Website ID van de **productie**-website-entry uit stap 4 (pas nodig vóór de eerste `deploy_production`-run) |
Stel deze in via **Gitea → Repository Settings → Actions → Variables**. Zodra beide
bestaan, pakt de eerstvolgende build ze automatisch op; zonder deze variabelen slaat de
app het inladen van het script gewoon over (geen crash, geen tracking).
Stel deze in via **Gitea → Repository Settings → Actions → Variables**. Zodra ze bestaan,
pakt de eerstvolgende build ze automatisch op; zonder deze variabelen slaat de app het
inladen van het script gewoon over (geen crash, geen tracking). Let op: dit zijn de
namen van de Gitea-variabelen — de app zelf verwacht altijd de envvar-naam
`VITE_UMAMI_WEBSITE_ID` (zie `UmamiAnalytics.tsx`), dus in `continuous_integration.yaml`
wordt bv. `VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_TEST }}` gebruikt in de
`build`-job en `VITE_UMAMI_WEBSITE_ID: ${{ vars.VITE_UMAMI_WEBSITE_ID_PRODUCTION }}` in
`build-production` — verwissel deze niet, anders komt productieverkeer in de
teststatistieken terecht (of andersom).
**Lokaal (`pnpm dev`)**: het tracking-script wordt hier bewust nooit geladen (zie
`UmamiAnalytics.tsx`), zodat lokaal testen de bezoekersstatistieken niet vervuilt. Wil je
@@ -220,8 +233,9 @@ dit toch lokaal testen, zet dan tijdelijk beide waarden in `.env.local` (zie
- [ ] Systemd user-service instellen (onder de `umami`-user) voor auto-start na reboot.
- [ ] DNS-record + certbot voor `analytics.slpsoftware.nl` op de reverse-proxy-Pi.
- [ ] Standaard Umami-wachtwoord direct wijzigen na eerste login.
- [ ] Website aanmaken in Umami en het Website ID overnemen.
- [ ] `VITE_UMAMI_SCRIPT_URL` en `VITE_UMAMI_WEBSITE_ID` als Gitea repository variables instellen.
- [ ] Website aanmaken in Umami voor de testomgeving en het Website ID overnemen.
- [ ] `VITE_UMAMI_SCRIPT_URL` en `VITE_UMAMI_WEBSITE_ID_TEST` als Gitea repository variables instellen.
- [ ] Zodra productie wordt opgezet: aparte website aanmaken in Umami voor `slpsoftware.nl` en het Website ID als `VITE_UMAMI_WEBSITE_ID_PRODUCTION` instellen (zie stap 4/5 hierboven — vereist vóór de eerste `deploy_production`-run).
## Vervolgstappen voor toekomstige self-hosted diensten
Dit dedicated-user-patroon (stap 0 hierboven) is bewust generiek gehouden zodat het
@@ -2,7 +2,7 @@
## Deployment
- **Status**: Configured
- **Method**: Manually-triggered Gitea Actions pipeline (`workflow_dispatch`, also runs automatically as a build/test/lint gate on pull requests) that packages the `dist/` build as a downloadable artifact; actual upload to the host is currently a manual step (see `operations/deployment/deployment-plan.md`)
- **Method**: Gitea Actions pipeline (`workflow_dispatch`, also runs automatically as a build/test/lint gate on pull requests, and as a full build/test/deploy-test on every push to `master`) that builds and uploads `dist/` straight to the test host via SCP. Production deploy is also automated (`build-production`/`deploy-production` jobs), but intentionally opt-in only — triggered by a manual `workflow_dispatch` run with the `deploy_production` checkbox, never automatically on push (see `operations/deployment/deployment-plan.md`)
- **Rollback Plan**: Yes — `operations/deployment/rollback-plan.md` (manual re-upload of a previous `release/*` build; no persistent/database state to roll back)
## Monitoring
@@ -11,14 +11,14 @@
## Additional Readiness Items
- **Backups**: N/A — this unit is a static marketing site (`dist/` bundle) with no database and no server-side/persistent state; nothing to back up beyond the source repository itself, which is already under git version control.
- **Secrets Management**: N/A for now — no automated host upload exists yet, so no host credentials are configured in Gitea Actions at this stage. When the automatic deploy step is added later (tracked as an open item in `deployment-plan.md`), credentials must be stored as Gitea Actions Secrets, never committed to the repo.
- **Secrets Management**: Configured — host credentials (`PI_MAIN_ADDRESS`/`PORT`/`USERNAME`/`PASSWORD`) are stored as Gitea Actions Secrets (never committed to the repo) and reused for both test and production deploys, since both target the same host for now; see `deployment-instructions.md`.
- **Runbook/Support Handover**: Partially covered — `operations/deployment/deployment-instructions.md` and `operations/deployment/rollback-plan.md` document how to trigger a release, upload the build, and roll back. No separate incident-response runbook exists beyond these documents, which is proportionate given this is a single-owner static site with no backend to page someone about.
- **Alert Ownership**: N/A — Alerting was explicitly declared out of scope in Monitoring Setup, so there is no alert-on-call/ownership rotation to define. If the chosen uptime dashboard tool (UptimeRobot/Better Uptime) is configured with its own opportunistic e-mail notification, the site owner is the sole recipient.
## Overall Readiness
- **Ready for Production**: Yes with caveats
- **Open Follow-ups**:
- Finalize the hosting/domain setup and extend `.gitea/workflows/deploy.yml`'s `deploy` job to actually upload `dist/` to the host, instead of only packaging it as a downloadable artifact (see `deployment-plan.md` "Open Item")
- ~~Finalize the hosting/domain setup and extend the deploy pipeline to actually upload `dist/` to the host~~ — **Resolved**: `.gitea/workflows/continuous_integration.yaml` now has automated `build`/`deploy-test` (always, on push to `master` or manual dispatch) and `build-production`/`deploy-production` (opt-in, manual `workflow_dispatch` with `deploy_production` checked). Remaining one-time manual setup: create the `DEPLOY_PATH_PRODUCTION` Gitea variable and the production nginx/SSL configuration on the Pi's before the first production run (see `deployment-instructions.md`)
- ~~Decide the client-side error logging destination~~ — **Resolved and verified**: console + Sentry free tier, implemented and confirmed working end-to-end (errors, logs, metrics) locally and on the test environment, including tracing, environment/release tags, and an ad-blocker-proof tunnel (see `monitoring-setup.md`); remaining manual step is creating the `VITE_SENTRY_DSN` Gitea Actions variable for the test/production build
- ~~Pick and configure the concrete analytics tool and uptime dashboard tool~~ — **Resolved**: self-hosted Umami (Podman on Pi Main) + UptimeRobot decided; see `monitoring-setup.md` and the new `umami-setup.md`. Remaining manual follow-ups: actually deploy the Umami containers, set up `analytics.slpsoftware.nl` DNS/SSL, create the UptimeRobot monitor, and register the final production URL once hosting is finalized
- Run a formal Lighthouse performance check before the first real production deployment (flagged as not yet run in `build-and-test-summary.md`)