docs: add auto-update implementation plan and rescope roadmap

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mixemeandCursor committed 2026-10-21 23:52:31 +03:00
1 parent 33d0d179e0
commit fac189a22b
4 files changed
+266 -28

No files matched your search

+252
View File
@@ -0,0 +1,252 @@
# 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` |
+3
View File
@@ -16,6 +16,9 @@ restructured around what actually blocks each item.**
**Documentation:**
- **`AUTO_UPDATE_PLAN.md`** — design-first plan for GitHub Release checks,
signed portable installs, and in-app restart. **`ROADMAP.md`** retitled the
feature to auto-update and points at the plan instead of detection-only scope.
- **`ROADMAP.md`** — reorganized into Features, Platform, Fyne/UI, and
Maintenance. The over-the-guideline file-split item is closed; each deferred
UI item now states whether it is blocked on a missing Fyne API, on UX
+10 -27
View File
@@ -7,37 +7,20 @@ Completed work is recorded in [CHANGELOG.md](CHANGELOG.md), not here.
User-facing functionality that is not blocked on a framework or platform gap.
### Update check from GitHub releases
### Auto-update from GitHub releases
Releases are published as GitHub Releases (tags like `v0.12.0`, built by
Releases are published as GitHub Releases (tags like `v1.0.4`, built by
`.github/workflows/release.yml`), but the app never tells the user a newer
version exists — they have to check the releases page by hand.
version exists — they have to open the releases page by hand.
Add an update check that queries the GitHub Releases API
(`GET /repos/mixeme/gosentry/releases/latest`) for the latest published tag,
strips the leading `v`, and compares it against `app.Version`. When a newer
version is available, surface it non-intrusively — an "Update available"
line in Settings (next to the existing version/build info) with a hyperlink
to the release page, not a modal on launch.
Ship opt-in update checks and one-click install for the portable archives:
semver compare against `app.Version`, signed downloads, replace the running
executable, and restart. Settings shows status in the About section; no modal
on launch.
Design notes / open questions:
- *Opt-in and offline-safe.* The check makes a network request, so it must be
off by default (or clearly consented) and never block startup. Failures
(offline, rate-limited, API change) should be silent — no error dialogs for a
best-effort convenience feature.
- *Version comparison.* Compare semantic versions, not strings, so `0.12.0`
reads as newer than `0.9.0`. A tiny semver comparator in `app` (or a small
dependency) avoids lexical bugs.
- *Where the check lives.* Keep it in the `app` layer behind the Service so the
UI only renders the result, and cache the last check so opening Settings
repeatedly does not spam the API (unauthenticated GitHub allows 60 req/h).
- *Repo coordinates.* The primary remote is Gitea; the GitHub repo used for
releases is [`mixeme/gosentry`](https://github.com/mixeme/gosentry) and must
be wired in explicitly (constant or build-time value) rather than derived from
`origin`.
- *No auto-download.* Scope is detection and notification only; installing the
update stays a manual click-through to the release page.
Implementation is **design-first** (UI on a stub, then backend). Phases,
security, and platform details live in
[AUTO_UPDATE_PLAN.md](AUTO_UPDATE_PLAN.md).
### Import/export jobs as a cron table
+1 -1
View File
@@ -140,6 +140,6 @@ change to their shape has to stay compatible on its own.
## Out of scope
Larger or blocked work is tracked in [ROADMAP.md](ROADMAP.md) (update check from
Larger or blocked work is tracked in [ROADMAP.md](ROADMAP.md) (auto-update from
GitHub releases, cron-table import/export, window size persistence, History
column filters).