From 481b998525595ed91313a6556a5bfb9b21b8605b Mon Sep 17 00:00:00 2001 From: bsncubed Date: Thu, 30 Jul 2026 00:13:34 +1000 Subject: [PATCH] docs: plan for an auto-update feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/auto-update-plan.md | 143 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 docs/auto-update-plan.md diff --git a/docs/auto-update-plan.md b/docs/auto-update-plan.md new file mode 100644 index 0000000..6ffd73d --- /dev/null +++ b/docs/auto-update-plan.md @@ -0,0 +1,143 @@ +# 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](https://gitea.apointless.space/bsncubed/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 `.new` behind a small progress window. +7. Verifies SHA256 against a `checksums.txt` asset in `sha256sum` format + (` `). 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 `.new`, report progress | +| `Verify(path, checksumURL)` | SHA256 against `checksums.txt` | +| `Apply(newPath)` | Rename dance, rollback, `chmod` | +| `CleanupOld()` | Delete `.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?**