Files
haul/docs/auto-update-plan.md
T
bsncubed 481b998525 docs: plan for an auto-update feature
Modelled on ShippingTracker's Updater.vb: gitea releases API, silent startup
check vs. loud manual one, SHA256 verification, and the rename-swap with .old
cleanup on next launch.

Records the research so it doesn't need repeating — haul's repo is readable
anonymously, so the updater needs no token, and the 404 on releases/latest is
just an empty release list.

Also records the two blockers: the release pipeline has to exist first (cgo
means make dist can't cross-compile), and dynamically linked Linux binaries may
not run on the machine that downloads them.

Not started; four open decisions listed at the end of the doc.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 00:13:34 +10:00

6.9 KiB

Auto-update for haul — plan

Status: planned, not started. Blocked on four decisions (bottom of this file). No code has been written.

The model is the self-updater in ShippingTracker (HelloWorld/Updater.vb), which haul should behave like.

Research already done — don't redo this

Verified 2026-07-29 against the live gitea instance:

  • https://gitea.apointless.space/api/v1/repos/bsncubed/ShippingTracker/releases/latest returns HTTP 200 anonymously. No token needed.
  • .../repos/bsncubed/haul also returns 200 anonymously, and .../repos/bsncubed/haul/releases returns []. So haul's repo is public; the 404 on releases/latest is simply "no releases exist yet", not a permissions problem. The updater will work unauthenticated.
  • Release JSON exposes tag_name, draft, prerelease, and an assets array whose entries carry name and browser_download_url. ShippingTracker's latest (1.1.0.2) attaches exactly two assets: Shipping Tracker.exe and checksums.txt.

What ShippingTracker's updater does

  1. GET /api/v1/repos/{owner}/{repo}/releases/latest; take tag_name, match assets by exact filename.
  2. Parse the tag into a version — tolerates a leading v, pads missing components with 0 (an unset System.Version component is -1, which sorts wrong).
  3. Silent vs. loud. A startup check swallows every failure (offline, DNS, timeout, non-200, malformed JSON) with no UI at all. A user-clicked check reports "you're up to date" and surfaces errors.
  4. Prompts with current vs. available before doing anything.
  5. Pre-flights write permission next to the executable, so a read-only install directory fails early rather than half-applying an update.
  6. Streams the download to <exe>.new behind a small progress window.
  7. Verifies SHA256 against a checksums.txt asset in sha256sum format (<hash> <filename>). If that asset is absent the update still installs, unverified.
  8. Swaps by rename: running exe → .old, .new → exe, rolling back if the second rename fails. .old is deleted on the next launch.
  9. On any failure, offers to open the releases page in a browser instead.

Ports over unchanged

The API shape, the silent/loud split, tag parsing, checksum verification, the rename-swap with .old cleanup, and the manual-download fallback.

The rename dance exists because Windows won't delete a running binary but will rename one. Linux and macOS also allow renaming a running executable (the inode stays live), so a single code path covers all three platforms.

Has to change for haul

  1. haul has no version at all. ShippingTracker compares against AssemblyVersion; cmd/haul has no equivalent, which is why the Makefile deliberately omits -X main.version (a -X against a non-existent symbol is silently a no-op). Adding one is a prerequisite — without it every check reports "up to date".
  2. One asset becomes many. ShippingTracker is Windows-only, so its AssetName is a single constant. haul must select an asset by runtime.GOOS/runtime.GOARCH (haul-linux-amd64, haul-darwin-arm64, haul-windows-amd64.exe).
  3. The executable bit. On Unix the downloaded file needs chmod 0755. Nothing in the VB version corresponds to this.
  4. Fyne threading. internal/ui only touches widgets from the UI thread and returns from I/O via fyne.Do. The updater must follow the same shape as session.transfer and pane.navigateKeepMarks.

Blockers — read before starting

The release assets can't be built yet. This gates everything. haul is cgo, so make dist produces the native binary only; there is currently no way to produce macOS or Windows assets from the Linux box. An updater with nothing to download is pointless, so the release pipeline must come first (fyne-cross over Docker, which is installed, or real machines).

Linux binaries don't travel. haul links dynamically against libGL, libX11, libc and ten others. A binary built on Ubuntu 24.04 may not start on a different or older distro — it would download, swap itself in, and then fail to launch, which is the worst failure mode an updater has. ShippingTracker never hit this (.NET Framework, stable Windows ABI). Options: build against the oldest glibc worth supporting, ship Linux without auto-update, or accept it.

checksums.txt is not a signature. It lives on the same server as the binary, so it catches corruption and truncation but not a compromised gitea — anyone who can replace one can replace the other. ShippingTracker accepts this. Closing the gap means a minisign/ed25519 signature with the public key compiled into haul, roughly 40 extra lines.

Proposed shape

New package internal/selfupdate, UI-free so it can be tested against a fake HTTP server:

Piece Responsibility
Check(ctx) (*Release, error) Fetch latest, parse tag, pick this platform's asset
Release.Newer(current) Version comparison
Download(ctx, rel, progress) Stream to <exe>.new, report progress
Verify(path, checksumURL) SHA256 against checksums.txt
Apply(newPath) Rename dance, rollback, chmod
CleanupOld() Delete <exe>.old; never errors

Plus a thin internal/ui/update.go for dialogs, reusing confirmDialog from dialogs.go and a progress dialog modelled on the existing transfer bar.

Integration points, mirroring ShippingTracker's Form1_Load and Form3:

  • CleanupOld() and a silent check when the connect window opens (newConnectWindow).
  • A manual "Check for updates" button. The connect window is the natural home — haul has no settings screen.

Release process: a make release VERSION=x.y.z target that builds, writes checksums.txt, and prints the tag/upload steps. Tag as 1.2.0 or v1.2.0, and don't mark it pre-release — /releases/latest skips drafts and pre-releases.

Phasing

Phase Work
0 Version stamping — var version in cmd/haul, -X in the Makefile, haul --version
1 Release pipeline — cross-builds via fyne-cross, checksums, first tagged release
2 internal/selfupdate with tests against a fake HTTP server
3 UI wiring — startup silent check, manual button, progress and error dialogs
4 README: how updating works, and how to publish a release

Phases 0 and 1 are worth doing regardless — there is currently no way to ship haul to anyone at all.

Open decisions — needed before building

  1. Auto-check on startup, or manual only? ShippingTracker does both. Silent startup checks mean haul contacts gitea on every launch.
  2. Which platforms get auto-update? Suggestion: exclude Linux at first, for the dynamic-linking reason above.
  3. Checksums only, or add signatures?
  4. Do phase 1 first, or write the updater against ShippingTracker's existing releases as a test fixture?