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>
This commit is contained in:
@@ -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 `<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?**
|
||||
Reference in New Issue
Block a user