12 KiB
Auto-update implementation plan
Working document for in-app updates from GitHub Releases. Retire this file and record the shipped behaviour in CHANGELOG.md, ARCHITECTURE.md, and STANDARDS.md when all phases are done.
Approach: design-first — build and agree the Settings UI on a stub before any network or install code. Backend work follows the UI contract.
1. Goal and non-goals
Goal: Let the user update the portable build (zip/tar.gz produced by
.github/workflows/release.yml) to the
latest GitHub Release in one confirmed action, with an application restart.
Non-goals:
- macOS (stub returns a clear error; no self-replace path in v1).
- Installers, package managers, or system-wide installs outside the app folder.
- Downgrades, pre-release channels, delta updates, or silent install without confirmation.
2. Current state
Facts the design depends on:
| Topic | Detail |
|---|---|
| Version | app.Version in src/app/version.go; release builds override via -ldflags -X in scripts/ci-build-release.sh. |
| Artifacts | gosentry-<ver>-linux-{amd64,arm64}.tar.gz, gosentry-<ver>-windows-amd64.zip. Executable at archive root: gosentry / gosentry.exe. |
| Portable layout | Config and jobs live beside the executable (src/storage/paths.go). Autostart shortcuts keep working after replace. |
| Single instance | 127.0.0.1:37653 (src/ui/singleinstance.go). A second process that starts before the port is free sends "show" and exits — the main restart risk. |
| Hosting | origin is Gitea (gitea.mixdep.ru/mix/gosentry). GitHub mixeme/gosentry is a pruning push mirror (git refs only). Release assets are attached by GitHub Actions, not mirrored. Update source: GitHub Releases API only. |
| Repository URL in app | Today projectRepositoryURL lives in ui; move release coordinates to app (constant or build-time -X). |
Mirror release ordering (DEVELOPMENT.md §Releasing through the GitHub push mirror):
- A release can appear on GitHub before the workflow attaches archives and
checksum files (build takes several minutes). If
releases/latesthas no matching asset or noSHA256SUMS/.sig, treat as not yet available: no UI error, no “up to date”; retry on the next check. - Deleting or moving a tag in Gitea removes or repoints the GitHub release. Re-fetch release metadata before download; HTTP 404 → treat as no update.
3. UI design (built first)
Implement the full user-facing surface on a stub (no network, no install). Capture screenshots for every state; Phase 2 does not start until the owner signs off on layout and copy in this document.
3.1 Elements
| Location | Control |
|---|---|
| Settings → Application | Checkbox Check for updates (saved with Save, like other toggles). |
| Settings → About | Row Updates: status label, one primary button (label depends on state), Release notes hyperlink, progress bar while downloading. |
| Modal | Install and restart confirmation: target version, download size, text that GoSentry will restart and scheduled runs pause briefly. |
| Modal | Refusal when a job is running (same policy as changing the jobs file). |
| Modal | Error for manual check or install failure (dialog.ShowError). |
| Tray menu | Update to X… when a newer version is known (tray enabled). |
| History | Session line after successful restart: Updated from X to Y. |
3.2 States (screenshot each)
- Auto-check off, never checked.
- Checking — primary button disabled.
- Up to date — includes last-check time.
- Available — Install and restart + Release notes.
- Downloading — progress + Cancel.
- Restarting — short transitional copy.
- Failed — status + Check now (or equivalent).
- Available, app directory not writable — Release notes link only (no Install).
- Unsupported — dev build (
0.0.0-dev/ unparseable version) or macOS.
A release that exists but has no assets yet does not get its own state; the UI matches Up to date until artifacts are present.
3.3 View model
updateViewinui: status text, primary button label/enabled/visible, release URL, progress 0–1, whether tray item shows.- Widgets render only from
updateView. - Phase 2 adds
updateViewFor(domain.UpdateState) updateView— the UI defines the contract the backend must satisfy.
3.4 Preview build
src/ui/update_preview.gowith build tagupdatepreview: cycle states on a timer and via buttons for screenshot work.- Production builds: no Updates row until Phase 2 wires real state (avoids shipping a non-functional row).
3.5 STANDARDS constraints
- Add new captions to
settingsCaptionsfor measured column width. - Widget updates from background goroutines via
fyne.Do.
3.6 Design gate
Insert screenshots under §3.2 in this file (or docs/screenshots/update_*.PNG).
Owner approves copy and layout → proceed to Phase 2.
4. Architecture
flowchart LR
settingsUI["ui Settings About/Updates"] -->|"CheckForUpdates, InstallUpdate"| svc["app.Service UpdateState"]
svc -->|"updater seam"| upd["src/update API download verify extract"]
svc -->|"ReplaceExecutable, Relaunch"| plat["platform/selfupdate"]
upd -->|"HTTPS"| gh["api.github.com"]
svc -->|"UpdateStateChanged"| settingsUI
| Layer | Responsibility |
|---|---|
domain |
UpdateState enum + data; CompareVersions (semver, pre-release rules). Dev/unparseable current version → never offer update. |
src/update |
stdlib only: LatestRelease, SelectAsset(goos, goarch), Download, Verify (SHA256 + ed25519), ExtractBinary (zip-slip safe, single binary entry). |
src/platform/selfupdate |
ReplaceExecutable, Relaunch, CleanupPrevious — _windows.go, _linux.go, _other.go like autostart. |
app.Service |
CheckForUpdates(manual), InstallUpdate, UpdateState(), emit UpdateStateChanged. Network behind updater interface (tests use fake). No network I/O under mu. |
Windows replace: rename running gosentry.exe → gosentry.exe.old, write new
exe via temp + rename; rollback rename on failure.
Linux replace: temp file in app dir, chmod 0755, rename over executable.
Relaunch: new process with --updated-from=<oldVersion> and --start-in-tray
when the window was hidden. New instance retries single-instance bind ~10s,
deletes .old, records History.
5. Update flow (install and restart)
- User clicks Install and restart → confirmation dialog.
- If any job is running → error dialog (same rule as
UpdateSettingsjobs-file switch). - Download, verify signature and hash, extract binary, replace executable.
svc.Stop(), close single-instance listener,Relaunch,a.Quit().- New process: bind port (retry),
CleanupPrevious(), History event. - If
Relaunchfails → restore from.old,dialog.ShowError.
6. Integrity and security
- CI produces
SHA256SUMSandSHA256SUMS.sig(ed25519). Public key embedded in the binary; private key in GitHub Actions secret. No new Go dependencies. - Install refuses without valid signature and matching hash for the selected archive (checksum alone does not defend a compromised release upload).
- HTTPS only; download size cap; allow redirects only to
objects.githubusercontent.com(and same release asset URLs from the API).
7. Check behaviour and config
Config.CheckForUpdates bool—json:"check_for_updates,omitempty". Zero = off (legacy configs unchanged). Default inDefaultConfig(): see §12 (recommend off).- When enabled: check after startup (delayed, non-blocking) and every 24h; cache
in Service. Automatic check errors are silent; manual check/install errors use
dialog.ShowError. - Writable app directory probe in Service →
UpdateStatefor “link only” UI.
8. Release pipeline changes
- scripts/ci-build-release.sh: emit
SHA256SUMS; sign withopenssl pkeyutlor smallcmd/signsumshelper. - .github/workflows/release.yml: secret key;
attach
SHA256SUMSandSHA256SUMS.siginaction-gh-releasefiles:(same job as build so assets and signature land together). - DEVELOPMENT.md: key generation, rotation, post-release
checklist — five files per release: three archives +
SHA256SUMS+.sig.
9. Phases (design-first order)
| Phase | Deliverable | Shippable? |
|---|---|---|
| 1 — UI design | §3: updateView, Updates row, checkbox, dialogs, tray item, updatepreview, UI tests, screenshots, design gate |
Preview tag only |
| 2 — Detection | domain.UpdateState, CompareVersions, update.LatestRelease, config field, Service + event, updateViewFor; Updates row in release builds; Available opens release page until Phase 4 |
Yes (ROADMAP notification scope) |
| 3 — Signed artifacts | CI checksums + signature on every GitHub release | Must ship before Phase 4 |
| 4 — Download and install | Full update + selfupdate, handoff, --updated-from, .old rollback; Install and restart live |
Yes |
| 5 — Landing | README screenshot, ARCHITECTURE, STANDARDS intentional behaviour; remove updatepreview if unused; delete this plan from ROADMAP |
Yes |
If Phase 2–4 needs a new UI state or field not in the signed-off §3, update screenshots and §3 first, then code.
10. Testing
Per TESTS.md:
| Phase | Package | Coverage |
|---|---|---|
| 1 | ui |
Each updateView state via Fyne test driver; assert no Updates row in default build |
| 2 | ui |
Table tests for updateViewFor |
| 2+ | domain |
CompareVersions (0.12.0 > 0.9.0, pre-releases, garbage) |
| 3–4 | update |
httptest: draft/prerelease, missing asset, 403, bad hash/sig, size limit, zip-slip |
| 4 | platform/selfupdate |
Temp-dir replace; Windows rename + rollback |
| 4 | app |
Fake updater; install blocked when job running; events; config compat trio (STANDARDS) |
Manual: N−1 → N on Windows and Linux; tray vs visible window; autostart enabled.
11. Docs to update when feature lands
- STANDARDS §Intentional: no install while job running; brief scheduler gap on restart; opt-in silent auto-check.
- ARCHITECTURE: package map, platform table, update flow diagram.
- ROADMAP: remove auto-update item.
- CHANGELOG: user-visible release notes.
- Delete this file after Phase 5.
12. Open questions
CheckForUpdatesdefault inDefaultConfig(): off vs first-run prompt.- Signature format: raw ed25519 (stdlib) vs minisign-compatible.
- Optional pre-release channel later?
Screenshot placeholders (Phase 1)
Add images here after the design gate.
| State | File |
|---|---|
| Auto-check off | docs/screenshots/update_idle.PNG |
| Checking | docs/screenshots/update_checking.PNG |
| Up to date | docs/screenshots/update_uptodate.PNG |
| Available | docs/screenshots/update_available.PNG |
| Downloading | docs/screenshots/update_downloading.PNG |
| Restarting | docs/screenshots/update_restarting.PNG |
| Failed | docs/screenshots/update_failed.PNG |
| Not writable | docs/screenshots/update_nowrite.PNG |
| Unsupported | docs/screenshots/update_unsupported.PNG |