Vertrag: Releases und Update-Check
Gemeinsame Schnittstelle zwischen der App (/, nativ auf Debian) und der Homepage (site/, Docker, https://disk-tester.znbw.de).
Änderungen nur abgestimmt und abwärtskompatibel (Felder nur hinzufügen).
1. Release-Paket (erzeugt von der CI bei Tag vX.Y.Z)
Das Repo bleibt privat. Die CI baut bei jedem Tag v* in einem Lauf das App-Paket und das Homepage-Image; das Paket wird in das Image eingebaut. Die Homepage braucht dadurch keinen GitHub-Zugriff und keinen Token. Zusätzlich legt die CI ein GitHub-Release mit denselben Dateien als Archiv an.
Dateien:
| Asset | Inhalt |
|---|---|
disk-tester-vX.Y.Z-linux-x64.tar.gz | fertig gebaute App, kein Build auf dem Zielsystem nötig |
disk-tester-vX.Y.Z-linux-x64.tar.gz.sha256 | eine Zeile im sha256sum-Format: <hex> disk-tester-vX.Y.Z-linux-x64.tar.gz |
Inhalt des Tarballs (oberstes Verzeichnis disk-tester-vX.Y.Z/):
disk-tester-vX.Y.Z/
server.js, .next/, node_modules/, public/ Next.js standalone output inkl. .next/static
package.json version = X.Y.Z
deploy/install.sh erkennt, dass es aus einem Release läuft (kein npm ci/build)
deploy/update.sh
deploy/disk-tester.service
VERSION Inhalt "X.Y.Z\n"
Release-Notes = Abschnitt ## [X.Y.Z] aus CHANGELOG.md (Markdown).
Im Homepage-Image liegen unter /app/releases/: der Tarball, die .sha256-Datei und release.json ({ version, tag, publishedAt, notes, asset, sha256, sizeBytes }). Es ist immer genau das neueste Release enthalten.
2. Update-Check-API (bereitgestellt von der Homepage)
GET https://disk-tester.znbw.de/api/latest → 200 application/json, CORS *, Cache max. 5 min:
{
"name": "disk-tester",
"version": "0.3.0",
"tag": "v0.3.0",
"publishedAt": "2026-11-01T12:00:00Z",
"notes": "Markdown der Release-Notes",
"notesUrl": "https://disk-tester.znbw.de/download#v0.3.0",
"download": {
"url": "https://disk-tester.znbw.de/download/v0.3.0/disk-tester-v0.3.0-linux-x64.tar.gz",
"sha256": "<64 hex>",
"sizeBytes": 12345678
}
}
404 {"error": "..."}wenn das Image kein Release enthält.GET /api/releases→{ "releases": [ <gleiches Objekt wie oben> ] }(nur das enthaltene, neueste Release).GET /download/<tag>/<asset>→ liefert die Datei aus/app/releases/(Stream,Content-Length,Content-Disposition: attachment). Nur die enthaltenen Dateien, sonst 404.GET /install.sh→ Bootstrap-Skript für Debian (root): lädt/api/latest, lädt den Tarball, prüft SHA256, entpackt nach einem Temp-Verzeichnis und führtdeploy/install.shdaraus aus.
3. Verhalten der App
- Basis-URL per Env
DISK_TESTER_UPDATE_URL(Defaulthttps://disk-tester.znbw.de),DISK_TESTER_UPDATE_CHECK=0schaltet ab. - Prüft höchstens alle 6 h
GET <base>/api/latest(nach einem fehlgeschlagenen Abruf erneut nach 30 min); Versionsvergleich per SemVer gegen die eigene Version; Fehler werden still ignoriert. - Die Fußzeile zeigt bei einer neueren Version „Update verfügbar: vX“ mit Link auf
notesUrlund dem Befehl/appdata/disk-tester/app/deploy/update.sh. deploy/update.sh [latest|vX.Y.Z|--rollback] [--force]lädt überdownload.url, prüftsha256, installiert nach/appdata/disk-tester/releases/<version>, schaltet/appdata/disk-tester/app(Symlink) um, startet den Dienst neu.--rollbackschaltet den Symlink auf die zuvor installierte Version zurück.- Die Oberfläche kann das Update selbst starten (
POST /api/update, startetupdate.shpersystemd-run --unit=disk-tester-update), wenn die Installation das Release-Layout nutzt. Optional installiert die App neue Versionen automatisch (Settings.autoUpdate), sobald nichts läuft. - Installation per Einzeiler:
curl -fsSL https://disk-tester.znbw.de/install.sh | sudo bash.
4. Laufzeit-Schnittstellen der App
GET /api/healthenthält zusätzlichactiveJobs(Anzahl laufender Jobs) undautoPending(offene Automatik-Countdowns).update.shverweigert den Neustart, wenn eine der Zahlen größer als 0 ist (--forceüberschreibt). Fehlen die Felder (ältere App) oder antwortet der Dienst nicht, läuft das Update weiter.update.shschreibt<DISK_TESTER_DATA_DIR>/update-status.json(Standard/appdata/disk-tester/data, atomar, Modus 0600):
{ "state": "running", "from": "0.3.0", "to": "0.4.0", "at": "2026-11-01T12:00:00Z", "detail": null }
state: running (beim Start), danach succeeded, failed, rolled-back (neue Version nicht gesund, alte wiederhergestellt) oder refused (Jobs/Countdowns aktiv). detail ist ein kurzer englischer Grund. Ein running-Eintrag, der älter als 30 min ist, gilt als Überbleibsel eines abgebrochenen Laufs.
- Gleichzeitige Läufe verhindert
flockauf<base>/update.lock.