docs: add auto-update implementation plan and rescope roadmap
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
33d0d179e0
commit
fac189a22b
4 files changed
+266
-28
No files matched your search
@@ -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` |
|
||||
@@ -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
@@ -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
@@ -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).
|
||||
Reference in new issue
Block a user