Skip to main content

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:

AssetContents
disk-tester-vX.Y.Z-linux-x64.tar.gzprebuilt app, no build needed on the target system
disk-tester-vX.Y.Z-linux-x64.tar.gz.sha256one 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 runs deploy/install.sh from it.

3. Behavior of the app​

  • Base URL via env DISK_TESTER_UPDATE_URL (default https://disk-tester.znbw.de), DISK_TESTER_UPDATE_CHECK=0 disables it.
  • Checks GET <base>/api/latest at 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 notesUrl and the command /appdata/disk-tester/app/deploy/update.sh.
  • deploy/update.sh [latest|vX.Y.Z|--rollback] [--force] downloads via download.url, verifies sha256, installs to /appdata/disk-tester/releases/<version>, switches /appdata/disk-tester/app (symlink) and restarts the service. --rollback switches the symlink back to the previously installed version.
  • The UI can start the update itself (POST /api/update, runs update.sh via systemd-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/health additionally contains activeJobs (number of running jobs) and autoPending (pending auto countdowns). update.sh refuses to restart the service if either is above 0 (--force overrides). If the fields are missing (older app) or the service does not answer, the update proceeds.
  • update.sh writes <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 flock on <base>/update.lock.