Files
gosentry/docs/AUTO_UPDATE_PLAN.md
T

253 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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-<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](../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=<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)
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` |