Contract: releases and update check
Shared interface between the app (/, native on Debian) and the homepage (site/, Docker, https://disk-tester.znbw.de).
Changes only in agreement and backwards compatible (only add fields).
1. Release package (produced by CI on tag vX.Y.Z)
The repo stays private. On every tag v*, CI builds the app package and the homepage image in one run; the package is built into the image. The homepage therefore needs no GitHub access and no token. In addition, CI creates a GitHub release with the same files as an archive.
Files:
| Asset | Contents |
|---|---|
disk-tester-vX.Y.Z-linux-x64.tar.gz | prebuilt app, no build needed on the target system |
disk-tester-vX.Y.Z-linux-x64.tar.gz.sha256 | one line in sha256sum format: <hex> disk-tester-vX.Y.Z-linux-x64.tar.gz |
Contents of the tarball (top-level directory disk-tester-vX.Y.Z/):
disk-tester-vX.Y.Z/
server.js, .next/, node_modules/, public/ Next.js standalone output incl. .next/static
package.json version = X.Y.Z
deploy/install.sh detects that it runs from a release (no npm ci/build)
deploy/update.sh
deploy/disk-tester.service
VERSION contents "X.Y.Z\n"
Release notes = section ## [X.Y.Z] from CHANGELOG.md (Markdown).
The homepage image contains under /app/releases/: the tarball, the .sha256 file and release.json ({ version, tag, publishedAt, notes, asset, sha256, sizeBytes }). Exactly the latest release is always included.
2. Update check API (provided by the homepage)
GET https://disk-tester.znbw.de/api/latest → 200 application/json, CORS *, cache at most 5 min:
{
"name": "disk-tester",
"version": "0.3.0",
"tag": "v0.3.0",
"publishedAt": "2026-11-01T12:00:00Z",
"notes": "Markdown of the 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": "..."}if the image contains no release.GET /api/releases→{ "releases": [ <same object as above> ] }(only the included, latest release).GET /download/<tag>/<asset>→ serves the file from/app/releases/(stream,Content-Length,Content-Disposition: attachment). Only the included files, otherwise 404.GET /install.sh→ bootstrap script for Debian (root): fetches/api/latest, downloads the tarball, verifies SHA256, unpacks into a temp directory and runsdeploy/install.shfrom it.
3. Behavior of the app
- Base URL via env
DISK_TESTER_UPDATE_URL(defaulthttps://disk-tester.znbw.de),DISK_TESTER_UPDATE_CHECK=0disables it. - Checks
GET <base>/api/latestat most every 6 h (again after 30 min when a request failed); version comparison via SemVer against its own version; errors are ignored silently. - If a newer version exists, the footer shows "Update available: vX" with a link to
notesUrland the command/appdata/disk-tester/app/deploy/update.sh. deploy/update.sh [latest|vX.Y.Z|--rollback] [--force]downloads viadownload.url, verifiessha256, installs to/appdata/disk-tester/releases/<version>, switches/appdata/disk-tester/app(symlink) and restarts the service.--rollbackswitches the symlink back to the previously installed version.- The UI can start the update itself (
POST /api/update, runsupdate.shviasystemd-run --unit=disk-tester-update) when the installation uses the release layout. Optionally the app installs new versions automatically (Settings.autoUpdate) once nothing is running. - Installation with the one-liner:
curl -fsSL https://disk-tester.znbw.de/install.sh | sudo bash.
4. Runtime interfaces of the app
GET /api/healthadditionally containsactiveJobs(number of running jobs) andautoPending(pending auto countdowns).update.shrefuses to restart the service if either is above 0 (--forceoverrides). If the fields are missing (older app) or the service does not answer, the update proceeds.update.shwrites<DISK_TESTER_DATA_DIR>/update-status.json(default/appdata/disk-tester/data, atomic, mode 0600):
{ "state": "running", "from": "0.3.0", "to": "0.4.0", "at": "2026-11-01T12:00:00Z", "detail": null }
state: running (at start), then succeeded, failed, rolled-back (new version unhealthy, old one restored) or refused (jobs/countdowns active). detail is a short English reason. A running entry older than 30 min is treated as the leftover of an aborted run.
- Concurrent runs are prevented by
flockon<base>/update.lock.