253 lines
12 KiB
Markdown
253 lines
12 KiB
Markdown
# 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` |
|