OTA updates and rollback

install/update.py installs GitHub Releases transactionally. The release must contain exactly one .tar.gz application asset. It must also have either a GitHub asset sha256: digest or a companion <asset>.sha256 release asset. Unsigned source archives without either checksum are rejected.

The archive must contain firmware/main.py, webui/app.py, and the generated requirements.lock. Dependencies, including PyIndi, are installed only from that fully pinned SHA-256 hash lock. The updater rejects absolute paths, traversal, links, and device nodes before extraction, creates a fresh virtual environment in a new versioned release directory, then atomically flips /opt/japyscope/current.

Commands

sudo /opt/japyscope/current/.venv/bin/python /opt/japyscope/current/install/update.py check
sudo /opt/japyscope/current/.venv/bin/python /opt/japyscope/current/install/update.py apply
sudo /opt/japyscope/current/.venv/bin/python /opt/japyscope/current/install/update.py system-upgrade

The enabled japyscope-update.timer runs both system-upgrade and apply once per day with a randomized delay (japyscope-update.service has two ExecStart lines). apply is idempotent when the latest tag is already active.

The Web UI’s System tab has a “Check for update now” button that runs sudo systemctl start japyscope-update.service immediately, without waiting for the timer. Progress and results can be followed in Diagnostics under the japyscope-update service.

System package upgrades

system-upgrade runs apt-get update && apt-get upgrade — patches within the currently configured Raspberry Pi OS repos only. It never runs full-upgrade or dist-upgrade, and never touches /etc/apt/sources.list, so it cannot move the device onto Bookworm or any other release on its own. It’s a separate, best-effort step from the JapyScope release install above (see the --prefixed ExecStart in japyscope-update.service): a transient apt failure is logged as SYS-001 but never blocks or gets rolled back with an app update.

Before every attempt (up to 3, spaced 30s then 2 minutes apart — not a tight loop, so a struggling card/power supply gets breathing room instead of being hit again immediately) it also self-heals what a previous interrupted run could have left behind: dpkg --configure -a fixes a half-configured package, and it waits for any held apt/dpkg lock rather than racing it. The one thing it deliberately does not try to fix is a read-only root filesystem — that fails immediately with SYS-001, no retries, since retrying against corrupted storage can make it worse (see docs/TROUBLESHOOTING.md).

Rollback behavior

The previous symlink target is retained. If dependency installation, byte-compilation, service restart, or the 30-second systemd health check fails, the updater restores the previous symlink and restarts both application services. The failed version is left in releases/ for diagnosis and will not be overwritten automatically.

Relevant log codes are OTA-001 (network/API/update failure), OTA-002 (checksum mismatch), and OTA-003 (new release failed and rollback ran).