Implement manual versioned Synology deployments for S6
All checks were successful
Build, test and publish / verify (push) Successful in 6m34s

This commit is contained in:
2026-11-03 14:16:52 +01:00
parent c9db528756
commit 525e9f9335
22 changed files with 2263 additions and 3 deletions

View File

@@ -2,7 +2,9 @@
De workflow staat in `.gitea/workflows/build.yaml`. Elke branchpush bouwt en test
backend en frontend, bouwt twee images en draait Playwright tegen die containers.
Alleen `main` mag daarna images publiceren. Er vindt geen deployment plaats.
Alleen `main` mag daarna images publiceren. Deze buildworkflow voert geen deployment uit.
S6 voegt aparte handmatige workflows toe voor productie en rollback; zie
[productiedeployment](production-deployment.md).
Tagpushes en aparte pull-requestevents starten geen extra workflow. Een auteur
kan een branchbuild bewust overslaan met een door Gitea ondersteunde markering,
zoals `[skip ci]`; dit is expliciet toegestaan.

View File

@@ -0,0 +1,281 @@
# Productie uitrollen op de Synology (S6)
De buildworkflow bouwt, test en publiceert automatisch. Productie verandert
alleen via **Actions → Deploy production → Run workflow** of **Rollback
production**. Start beide vanaf `main`; de workflows verschijnen na de merge.
**Tot S16 is geïmplementeerd kan iedere deployment, retry, rollback of automatisch
herstel alle applicatiegegevens wissen.** Informeer de launching partner vooraf.
Een onderbroken berekening moet opnieuw worden gestart. Een herstelde
applicatieversie herstelt geen verloren gegevens.
## Eenmalige DSM-inrichting
Gebruik je bestaande beheeraccount voor deze stappen:
1. Maak onder **Configuratiescherm → Gebruiker en groep → Gebruiker → Maken**
het account `agiledancers-deploy` met een uniek sterk wachtwoord. Voeg het toe
aan **administrators**: DSM vereist deze groep voor SSH. Het is daardoor een
beheerder, geen beperkt bestandsaccount.
2. Bij **Gedeelde mapmachtigingen**: `docker` op **Lezen/Schrijven**, `homes`
ongewijzigd, overige mappen op **Geen toegang**. `git` is niet nodig: deployment
gebruikt de Gitea-API en registry. Blokkeer `homes` niet, omdat dit ook de eigen
thuismap kan blokkeren.
3. De toepassingen onder **Toegangsmachtigingen toewijzen** zijn niet nodig en
mogen op **Weigeren**. Gebruik je eigen beheeraccount voor DSM. Deployment
gebruikt SSH-shellopdrachten, geen SFTP, rsync of bestandsdeling.
4. Schakel onder **Gebruiker en groep → Geavanceerd** de
**gebruikersbasismapservice** in als deze uitstaat.
5. Schakel **Terminal en SNMP → Terminal → SSH-service inschakelen** in. De
maintainer heeft SSH-poort **22** bevestigd. Beperk toegang tot mini-pc/LAN/VPN;
configureer geen internet-portforwarding.
6. Controleer dat Container Manager/Docker draait, dat Docker Compose **v2**,
`curl`, `timeout` met `-k` en `sh` beschikbaar zijn. Op de NAS is geen Java,
Python of Git nodig. Controleer het Docker-pad, doorgaans `/usr/local/bin/docker`.
Map- en toepassingsrechten vormen geen beveiligde isolatie voor een account met
beheerder-/Dockerrechten. De runner voert ook vertrouwde repositorycode uit.
Geef alleen vertrouwde beheerders schrijftoegang tot workflows en credentials.
Bronnen: [SSH op DSM](https://kb.synology.com/en-au/DSM/help/DSM/AdminCenter/system_terminal),
[sleutelaanmelding en basismappen](https://kb.synology.com/en-us/DSM/tutorial/How_to_log_in_to_DSM_with_key_pairs_as_admin_or_root_permission_via_SSH_on_computers),
[maprechten](https://kb.synology.com/nl-nl/DSM/help/DSM/AdminCenter/file_share_privilege?version=7).
## SSH-sleutel en hostkey
Maak op de mini-pc een aparte sleutel buiten de repository. De lege passphrase
maakt niet-interactieve uitvoering mogelijk; behandel de private sleutel als secret.
```sh
ssh-keygen -t ed25519 -f "$HOME/.config/agiledancers-runner/s6-deploy-key" -N '' -C agiledancers-production
ssh-copy-id -i "$HOME/.config/agiledancers-runner/s6-deploy-key.pub" -p 22 agiledancers-deploy@192.168.2.28
```
Genereer niet opnieuw als de sleutel al bestaat. De implementatiesessie heeft
deze dedicated sleutel op de mini-pc aangemaakt. De gebruiker installeert de
publieke sleutel via een eigen interactieve terminal; deel geen wachtwoorden.
Controleer de serverfingerprint via een reeds vertrouwde beheerverbinding, tegen
`/etc/ssh/ssh_host_ed25519_key.pub`. Een keyscan alleen bewijst de serveridentiteit
niet. Bewaar de geverifieerde known_hosts-regel; bij een andere poort gebruikt
die `[host]:poort`. De helper vereist `StrictHostKeyChecking=yes`, gebruikt alleen
de dedicated sleutel en negeert SSH-agent en lokale SSH-configuratie.
Op de NAS: eigen `~/.ssh` mode `700`, `authorized_keys` mode `600`, juiste eigenaar,
thuismap niet schrijfbaar voor andere gebruikers. De public-keyregel kan worden
beperkt met de werkelijke vaste mini-pc-IP en zonder forwarding/TTY:
```text
from="<mini-pc-LAN-IP>",no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty ssh-ed25519 <public-key> agiledancers-production
```
Als de sleutel na `ssh-copy-id` nog wordt geweigerd door ruime DSM-basismaprechten,
voer dan eenmaal met wachtwoordauthenticatie uit als dit account:
```sh
chmod go-w "$HOME"
chmod 700 "$HOME/.ssh"
chmod 600 "$HOME/.ssh/authorized_keys"
```
## Productiemap en Dockerrechten
Controleer het volume: `/volume1/docker/agiledancers-production` is een voorbeeld,
geen vastgestelde locatie. Maak de map als deploymentaccount, mode `700`, met
dat account als eigenaar. Houd deze stack gescheiden van Gitea en andere projecten.
Kopieer `deploy/production.env.example` naar `<productiemap>/production.env`, mode
`600`. Stel de private NAS-IP, vrije frontendpoort (voorbeeld `8088`), absolute
Docker-locatie en `DOCKER_CONFIG` in. Gebruik een eigen `docker-auth`-submap.
Alleen de frontend bindt aan de private IPv4; de backend krijgt geen hostpoort.
De Docker-projectnaam is vast `agiledancers-production`.
Als Docker sudo vereist, laat een beheerder met `visudo` een afzonderlijke
sudoers-regel installeren en valideren, met het gecontroleerde Docker-pad:
```text
agiledancers-deploy ALL=(root) NOPASSWD: /usr/local/bin/docker
```
Dit staat alleen dat executable toe, maar Dockerbeheer geeft in de praktijk
rootmogelijkheden. Gebruik geen `NOPASSWD: ALL`, wereldschrijfbare Docker-socket
of wachtwoord in de workflow. De helper gebruikt `sudo -n`. Als Docker al direct
toegankelijk is voor het account, stel `DOCKER_SUDO=0` in.
Provision apart een **pull-only** Gitea-packagecredential. Log als deploymentaccount
op de NAS in, via de interactieve wachtwoordprompt:
```sh
mkdir -p /volume1/docker/agiledancers-production/docker-auth
chmod 700 /volume1/docker/agiledancers-production/docker-auth
sudo -n /usr/local/bin/docker --config /volume1/docker/agiledancers-production/docker-auth login 127.0.0.1:3000 --username <pull-account>
sudo -n /usr/local/bin/docker compose version
sudo -n /usr/local/bin/docker info --format '{{.OSType}}/{{.Architecture}}'
```
Als DSM geen `visudo` bevat, is `deploy/enable-docker.sh` het eenmalige alternatief.
Kopieer het naar de eigen NAS-thuismap en voer het daar met sudo uit. Het maakt
uitsluitend de bovengenoemde regel, controleert die door als deploymentaccount
Docker te starten en verwijdert een nieuwe regel als die controle faalt. Een
afwijkende bestaande regel wordt niet overschreven. De implementatiesessie heeft
deze stap op de NAS uitgevoerd; Docker rapporteert `linux/x86_64`.
Pas paden aan voor de echte configuratie. De bevestigde NAS-Dockerdaemon staat
HTTP-registryverkeer alleen op loopback toe. Gebruik daarom vanaf die NAS
`127.0.0.1:3000`: dezelfde Gitea-registry en gepubliceerde SHA-images, zonder een
daemonherstart die Gitea en Kimai zou onderbreken. De mini-pc blijft naar
`192.168.2.28:3000` publiceren. Deploymentrecords bevatten de op de NAS bruikbare
loopback-digestreferenties. Authenticatie-/netwerkfouten zijn deploymentfouten.
## Gitea-configuratie
Stel onder repository **Settings → Actions** in:
| Soort | Naam | Inhoud |
| --- | --- | --- |
| Variabele | `DEPLOY_HOST` | Private NAS-IP/hostnaam, passend bij known_hosts |
| Variabele | `DEPLOY_USER` | `agiledancers-deploy` |
| Variabele | `DEPLOY_SSH_PORT` | `22` |
| Variabele | `DEPLOY_DIRECTORY` | Absolute productiemap |
| Variabele | `DEPLOY_REGISTRY` | `127.0.0.1:3000`, de NAS-lokale alias van dezelfde registry |
| Secret | `DEPLOY_SSH_KEY` | Volledige private sleutel, inclusief begin/eindregels |
| Secret | `DEPLOY_KNOWN_HOSTS` | Onafhankelijk geverifieerde NAS-hostkey-regel |
| Secret | `RELEASE_TOKEN` | Apart Gitea-token voor Actions lezen en Git-tags maken |
Het releaseaccount heeft repositorylees-/schrijfrechten en Actions-metadatatoegang
nodig (`write:repository` op deze Gitea). Het NAS-registrycredential is apart en
heeft alleen de noodzakelijke package-leestoegang. Hergebruik de publicatie-PAT
niet voor deployment. Bewaar secrets buiten Git en logs.
De setuphelper kan deze configuratie en de productiemap zonder containerstart
inrichten. Bewaar `admin-token`, `s6-release-token`, `s6-pull-token` en de private
`s6-deploy-key` als owner-only bestanden (mode `600`) in
`~/.config/agiledancers-runner/`. Gebruik de eerder vertrouwde NAS-hostkey in
`~/.ssh/known_hosts` en voer uit:
```sh
java ci/Ci.java configure-deployment
```
Defaults zijn de bevestigde NAS/account/SSH-poort, repository
`wanjakrah/agiledancers`, productiemap `/volume1/docker/agiledancers-production`,
NAS-IP-binding `192.168.2.28` en frontendpoort `8088`. De helper controleert een
geslaagde main-build, weigert een bezette poort bij eerste inrichting, bewaart
bestaande productieconfiguratie en controleert de aparte registry-login voordat
Gitea-secrets/variabelen worden gezet. Er wordt geen release/tag gemaakt en geen
container vervangen. Variabelen met de namen uit bovenstaande tabel en
`DEPLOY_BIND_IP`, `DEPLOY_FRONTEND_PORT`, `PULL_USER` en de credential-`*_FILE`-
paden kunnen de defaults expliciet vervangen.
Bescherm release-tags `v*` in Gitea en beperk tagcreatie tot de releasebeheerder.
De helper maakt alleen nieuwe tags; hij verplaatst of verwijdert ze nooit. Laat
registry-cleanup de images van huidig/vorig niet verwijderen. Gebruik geen
algemene Docker-image/volume-prune als onderhoud van deze stack.
## Nieuwe release, retry en rollback
1. Merge naar `main` en wacht op een geslaagde **Build, test and publish** voor
die SHA, inclusief imagepublicatie.
2. Open **Deploy production → Run workflow**, branch `main`, mode `new`, keuze
`patch`, `minor` of `major`. Laat het versieveld leeg. De eerste release is
`v1.0.0`; daarna telt de hoogste bestaande release-tag, inclusief mislukte
deployments. Minor/major zetten de lagere componenten op nul.
3. De workflow toont versie/SHA. De immutable annotated Git-tag wordt na
imagevoorbereiding, vóór vervanging gemaakt en blijft bij falen bestaan.
Een tag bewijst geen succesvolle productiedeployment.
4. Gebruik mode `retry` met een bestaande tag om dezelfde SHA/versie opnieuw uit
te rollen. De incrementkeuze wordt genegeerd: Gitea toont alle velden, ook
wanneer ze voor de gekozen mode niet relevant zijn. Een retry mag afwijken
van huidig main, maar geen nieuwere succesvolle release verdringen.
5. **Rollback production → Run workflow → main** herstelt de vorige succesvolle
release zonder versie/tag. Na succes wisselen huidig/vorig om; nogmaals
rollback kan dus terugschakelen. De hoogste succesvolle versie blijft bewaard.
De NAS-lock omvat alle modes en versieallocatie. Een gelijktijdige run faalt
zichtbaar. Gitea 1.25 negeert workflow-`concurrency`; de serverlock is leidend.
Main wordt direct vóór vervanging opnieuw gecontroleerd. Zodra vervanging start,
wordt de gepinde versie afgemaakt of hersteld. Branchbuilds rollen niets uit.
## Checks en operationele state
De helper haalt beide SHA-images terug, controleert revisie/platform en legt
registry-digests vast. Compose gebruikt die digests, nooit de bewegende main-tag.
De recovery-images worden eveneens vooraf gecontroleerd. Binnen vijf minuten
moeten backend-readiness, frontendpagina, GET `/api/dance-schools` via de
gepubliceerde frontendpoort en containeridentiteit slagen. Deze checks schrijven
geen gebruikersdata. De frontend herstart ook om de nieuwe backend-IP te resolven.
De productiemap bevat:
- `state.json`: atomisch geschreven huidig/vorig en hoogste succesvolle versie.
Redeployment van dezelfde versie behoudt de afzonderlijke rollbackversie.
- `releases/<attempt>/`: behouden Compose- en digest-/versieconfiguratie.
- `pending.json`: kandidaat en vorige state, vóór iedere vervanging geschreven.
- `deploy.lock/owner`: unieke run/poging; `operation/` markeert een actieve
vervanging op de NAS.
- `history/`: pogingjournalen en uitkomsten; niet opruimen tijdens een release.
Een ongezonde kandidaat herstelt automatisch de vorige succesvolle release en
blijft een mislukte workflow. Bij de eerste release zonder voorganger, mislukt
herstel, runner/SSH-onderbreking of onduidelijke statecommit blijven journal en
lock staan. Nieuwe runs weigeren dan wijzigingen. Remote opdrachten hebben ook
op de NAS een timeout; herstel overlapt geen nog actieve vervanging. Geen
eindeloze herstelpogingen.
Download artifact `deployment-<run>-<attempt>` of `rollback-<run>-<attempt>` voor
log, uitkomst, versie/SHA/digests en beschikbare foutdiagnostiek. Retentie is
veertien dagen; serverinstellingen mogen dit niet inkorten. Log geen environment,
Docker-authconfig of volledige container-inspect met eventuele secrets.
## Handmatig herstel
Gebruik een vertrouwde beheerverbinding. Verwijder nooit alleen de lock om een
nieuwe release door te drukken.
1. Annuleer de betrokken Gitea-run. Controleer op de NAS dat geen SSH-deployment,
timeout, Compose-vervanging of herstel van deze poging meer actief is. Onderzoek
`deploy.lock/operation`; wacht op de timeout of beëindig alleen de geverifieerde
vastgelopen deploymentprocessen.
2. Bewaar state, pending journal, history en containerlogs. Vergelijk de kandidaat,
de `before`-state en werkelijke images. De statecommit kan al zijn uitgevoerd
terwijl de runner geen antwoord ontving.
3. Behoud een aantoonbaar gezonde kandidaat met overeenkomende state. Anders:
herstel `before.current` uit het journal met de behouden Compose-/env-bestanden,
dezelfde projectnaam en digest-images. Start geen tweede stack.
4. Voer dezelfde checks uit. Vanaf de checkout op de mini-pc kun je het echte
controlescript uitvoeren met bestaande lockowner en gekozen release-directory:
```sh
ssh -i <private-key-path> -p <ssh-port> <deploy-user>@<nas> \
'timeout -k 5 300 sh -s -- apply <production-directory> <lock-owner> <release-directory-id>' < deploy/remote.sh
```
Gebruik de eerder gepinde hostkey en gecontroleerde waarden uit het journal.
Verwijder een achtergebleven lege `operation/` alleen als stap 1 heeft bewezen
dat de oude operatie gestopt is.
5. Bij herstel van de oude release: herstel ook de volledige `before`-state
atomisch, inclusief high-watermark en rollbackdoel. Bij een mislukte eerste
release: stop de defecte stack en behoud de lege succesvolle state. Verwijder
geen volumes of release-tags.
6. Noteer het herstel in history, archiveer het opgeloste journal en verwijder
daarna pas de lockowner en lege lockdirectory. Start vervolgens een retry.
Laat de blokkade staan als gezondheid of state onduidelijk is.
## Na S16 en lokale verificatie
S16 blokkeert S6 niet. Zodra S16 is ingevoerd, moeten database/volumes buiten de
vervangbare containers blijven en saved data zowel deployment als rollback
overleven. De gewone procedure vereist dan databasecompatibiliteit met de vorige
applicatieversie. Plan incompatibele migraties apart: imageherstel maakt geen
migratie ongedaan. Backup/restore blijven S17-werk.
```sh
java ci/Ci.java test
java ci/Ci.java release-test
sh -n deploy/remote.sh
mvn -N -Dformat.mode=check exec:exec@format
```
De buildworkflow voert beide Java-testsuites uit vóór publicatie. Release-tests
wijzigen geen productie, registry of tags. Leg echte NAS-/Gitea-bewijzen vast in
`docs/s6-validation.md`; configuratie en lokale tests alleen voltooien S6 niet.

107
docs/s6-validation.md Normal file
View File

@@ -0,0 +1,107 @@
# S6 implementation and operational evidence
Status: IN_PROGRESS. Repository implementation and local validation do not yet
establish an operational Synology deployment. No production release tags have
been created by this implementation session.
## Confirmed environment
- Work is on `feature/S6_deployment`.
- Read-only authenticated inspection of the installed Gitea confirmed run fields
`path=build.yaml@refs/heads/main`, `status=completed`, `conclusion=success`,
`event=push`, `head_branch=main` and `head_sha`.
- No remote Git tags existed at the time of that read-only inspection.
- The maintainer created the dedicated DSM account `agiledancers-deploy` and
confirmed SSH port 22. A separate Ed25519 deployment key was generated outside
Git on the mini-pc and installed on the NAS.
Subsequent live checks confirmed key-based access after removing group/other
write permissions from the account home. The maintainer installed the dedicated
Docker sudo rule using `deploy/enable-docker.sh`; a subsequent noninteractive
check reports `linux/x86_64`. NAS model is **DS224+**, Compose is
**v2.20.1-6047-g6817716**, `timeout -k` and `curl` are available, and port **8088**
was free. The daemon allows HTTP registry access only through `127.0.0.0/8`.
Deployment therefore uses the NAS-local `127.0.0.1:3000` alias of the same Gitea
registry, without restarting Gitea/Kimai or changing the shared Docker daemon.
## NAS setup and real image preparation
`java ci/Ci.java configure-deployment` succeeded using separate owner-only local
pull/release tokens. It provisioned `/volume1/docker/agiledancers-production`,
the private IP/port configuration and registry pull login, verified the real NAS
preflight, and stored the three deployment secrets and five variables in Gitea.
No secret values are recorded here.
A real preparation probe exercised the production coordinator/API/SSH adapter
and NAS script, stopping explicitly at the tag-creation boundary. It retrieved
both images for verified main commit
`b1544357bf6ea5e1607e1de1fa4872b9510690a8` (Gitea API build run ID **35**), verified
revision/platform labels, selected registry digests and passed Compose v2.20
configuration validation. It released the deployment lock without creating a
Git tag, pending journal, successful release state or application containers.
The displayed candidate `v1.0.0` was not allocated as a release.
| Component | Retrieved digest (NAS registry alias `127.0.0.1:3000/wanjakrah/`) |
| --- | --- |
| Backend | `sha256:d46af705c527a8eaa506407206cfce927edbc4b774951cf13da8702b5cb8c56d` |
| Frontend | `sha256:d6c09ad839b929591878195c7403cbf0bbf24ac6e8d335ef90d72b7982c4ba31` |
Local probe evidence: `ci/artifacts/deployment/setup.log`, `nas-image-probe.log`
and `nas-image-probe.json` (ignored by Git). Retained configuration on the NAS:
`releases/image-probe-bbb2a980-e624-4331-adb6-b8fffd51fa5d/`. This is preparation
evidence, not a successful production release or Gitea dispatch run.
## Local checks
- `java ci/Ci.java test`: **17 passed**, including publication gates and process
deadlines.
- `java ci/Ci.java release-test`: **32 passed**, including all version increments,
input/main/manual gates, verified-build provenance, digest preparation failures,
duplicate/stale release rejection, retry, version high-watermark after rollback,
automatic recovery, failed recovery/first release, ambiguous state commits,
lock contention, annotated-tag identity and registry aliases.
- Seven of those release tests execute the actual POSIX NAS script against
disposable command adapters, covering image/platform checks, published endpoint
requests, retained-configuration mismatch, bounded readiness failure, overlapping
replacement fencing and atomic state/journal handling. These are simulated
failure-path tests, separate from the real NAS preparation probe above.
- `sh -n` passed separately for `deploy/remote.sh`, `deploy/provision.sh` and
`deploy/enable-docker.sh`.
- `mvn -N -Dformat.mode=check exec:exec@format`: passed with no differences.
- All workflow YAML parsed; assertions confirmed automatic builds, manual-only
release/rollback triggers and the patch/minor/major form choices.
- `docker compose -f deploy/compose.yaml config --quiet` passed with representative
digest references; real NAS Compose validation passed during preparation too.
- `git diff --check`: passed.
Backend/frontend application code is unchanged. A full Gitea feature/main build
and dispatched production/recovery evidence remain to be collected after review.
## Required operational acceptance
- [x] Record NAS model/platform, Docker/Compose versions, private endpoint and
production directory (no secret values).
- [x] Verify pinned SSH hostkey, key-only automation and separate registry pull
credential. Release-token read access is verified; actual tag-writing
acceptance awaits the first release.
- [ ] Verify release-tag creation/protection with the dedicated token.
- [ ] Merge workflows through the protected-branch process and identify the
successful main build/publication run and SHA.
- [ ] Demonstrate release form, first release tag and real Synology access;
record workflow URL, version, SHA and both deployed digests.
- [ ] Demonstrate rejected unverified/missing-image release and branch exclusion
without modifying production.
- [ ] Demonstrate duplicate/stale/concurrent release protection, retained tags
after failure and exact-SHA retry without another version.
- [ ] Demonstrate failed candidate recovery while the workflow reports failure,
with downloadable diagnostics.
- [ ] Demonstrate manual rollback without a new version and retained image pairs.
- [ ] Exercise first-release/recovery failure and interruption/manual recovery
using isolated acceptance resources where appropriate.
- [ ] Confirm fourteen-day artifact retention.
- [ ] Confirm operator communication of the accepted pre-S16 data-loss policy.
- [ ] Once S16 applies, verify data preservation and migration compatibility
during deployment and rollback (conditional on S16 implementation).
Identify isolated acceptance resources explicitly. Simulated tests are not real
production evidence.