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

144 lines
6.9 KiB
Markdown

# 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 `<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?**