# Auto-update implementation plan Working document for in-app updates from GitHub Releases. Retire this file and record the shipped behaviour in [CHANGELOG.md](CHANGELOG.md), [ARCHITECTURE.md](ARCHITECTURE.md), and [STANDARDS.md](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`](../.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](../src/app/version.go); release builds override via `-ldflags -X` in [scripts/ci-build-release.sh](../scripts/ci-build-release.sh). | | Artifacts | `gosentry--linux-{amd64,arm64}.tar.gz`, `gosentry--windows-amd64.zip`. Executable at archive root: `gosentry` / `gosentry.exe`. | | Portable layout | Config and jobs live beside the executable ([src/storage/paths.go](../src/storage/paths.go)). Autostart shortcuts keep working after replace. | | Single instance | `127.0.0.1:37653` ([src/ui/singleinstance.go](../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`](https://github.com/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](DEVELOPMENT.md) §Releasing through the GitHub push mirror): 1. A release can appear on GitHub **before** the workflow attaches archives and checksum files (build takes several minutes). If `releases/latest` has no matching asset or no `SHA256SUMS` / `.sig`, treat as **not yet available**: no UI error, no “up to date”; retry on the next check. 2. 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) 1. Auto-check off, never checked. 2. **Checking** — primary button disabled. 3. **Up to date** — includes last-check time. 4. **Available** — **Install and restart** + Release notes. 5. **Downloading** — progress + **Cancel**. 6. **Restarting** — short transitional copy. 7. **Failed** — status + **Check now** (or equivalent). 8. **Available**, app directory not writable — Release notes link only (no Install). 9. **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 - `updateView` in `ui`: 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.go` with build tag `updatepreview`: 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 `settingsCaptions` for 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 ```mermaid 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=` 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) 1. User clicks **Install and restart** → confirmation dialog. 2. If any job is running → error dialog (same rule as `UpdateSettings` jobs-file switch). 3. Download, verify signature and hash, extract binary, replace executable. 4. `svc.Stop()`, close single-instance listener, `Relaunch`, `a.Quit()`. 5. New process: bind port (retry), `CleanupPrevious()`, History event. 6. If `Relaunch` fails → restore from `.old`, `dialog.ShowError`. --- ## 6. Integrity and security - CI produces `SHA256SUMS` and `SHA256SUMS.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 in `DefaultConfig()`: 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 → `UpdateState` for “link only” UI. --- ## 8. Release pipeline changes - [scripts/ci-build-release.sh](../scripts/ci-build-release.sh): emit `SHA256SUMS`; sign with `openssl pkeyutl` or small `cmd/signsums` helper. - [.github/workflows/release.yml](../.github/workflows/release.yml): secret key; attach `SHA256SUMS` and `SHA256SUMS.sig` in `action-gh-release` `files:` (same job as build so assets and signature land together). - [DEVELOPMENT.md](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](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 1. `CheckForUpdates` default in `DefaultConfig()`: **off** vs first-run prompt. 2. Signature format: raw ed25519 (stdlib) vs minisign-compatible. 3. 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` |