2 Commits

Author SHA1 Message Date
root 15495d41ea auto-update: drop confirmation dialog, do it silently
Build / test (push) Successful in 1m14s
Build / build-windows (push) Successful in 57s
Release / release (push) Successful in 2m34s
Chrome-style silent updater: no prompt, no progress UI, no buttons.
On startup we check for updates (8s timeout), and if one is found
we download + verify + apply + relaunch — total ~3-5s on a fast
connection. The user sees the new version's window instead of the
old one, period.

Errors that do warrant a dialog (apply failed mid-write, sha256
mismatch, can't relaunch) still surface as a message box so the
user knows their copy is on the previous version. 'No update' and
network timeouts stay silent.

Split timeouts so a 60s download doesn't get killed by the 8s
check budget.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 03:58:49 +03:00
root 9d174d8db1 release.yml: drop apt cache (Gitea restore times out on 300MB)
Build / test (push) Successful in 1m12s
Build / build-windows (push) Successful in 56s
Release / release (push) Successful in 2m59s
Apt cache save works (28s in v0.1.4) but the next run can't restore
it: 'getCacheEntry failed: Request timeout' — Gitea cache backend
chokes on the ~300MB archive. Drop the cache step rather than burn
30s every run on a save that the next run can't read.

Real fix: bake wine + innoextract + xauth + wine32:i386 into a
custom CI image on git.okcu.io's registry, use container.image to
pull it. Defer until release frequency justifies the setup work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 03:26:37 +03:00
2 changed files with 30 additions and 56 deletions
+7 -10
View File
@@ -48,16 +48,13 @@ jobs:
key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
restore-keys: go-${{ runner.os }}-
# Cache apt downloads — saves ~50s on the wine + innoextract install.
# Bump the cache key (-v2, -v3, ...) when the package list changes.
- name: Cache apt packages
id: apt-cache
uses: actions/cache@v4
with:
path: |
/var/cache/apt/archives
/var/lib/apt/lists
key: apt-trixie-wine-innoextract-v2
# NOTE: actions/cache for the apt archive (~300 MB) is disabled. The
# save step works (~28s in v0.1.4) but restore times out on the
# next run — Gitea's cache server can't push 300 MB back fast
# enough. The Wine + Inno Setup install stays at ~1m20s. The
# right fix is a pre-baked Docker image (golang:1.25 + wine +
# innoextract + xauth + wine32:i386) pushed to git.okcu.io as
# the job's container.image. Tracked as future work.
- name: Extract version from tag
id: version
+23 -46
View File
@@ -14,32 +14,35 @@ import (
"golang.org/x/sys/windows"
)
// autoUpdateOnStartup runs a non-interactive update check whenever drover.exe
// starts as a GUI app (no CLI subcommand). If an update is available, a
// Yes/No message box prompts the user; on Yes we download, verify, apply
// the update, then re-launch the binary so the new version is what they see.
// autoUpdateOnStartup silently checks for and applies updates whenever
// drover.exe starts as a GUI app (no CLI subcommand). Chrome-style: no
// prompt, no progress bar, no questions — if an update is available we
// download, verify, apply, and re-launch. The user just sees the new
// version's window appear instead of the old one.
//
// Network failures, server outages, and "no updates available" are silent
// fall-throughs — startup must never block on them.
// Network failures, server outages, slow downloads, and "no updates
// available" are silent fall-throughs — startup must never block on them.
//
// Two split contexts:
// - check (8s) — quick HEAD-equivalent against the releases API
// - apply (60s) — actual download + sha256 + atomic replace
func autoUpdateOnStartup() {
// Tight timeout — startup, not a long-running task.
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
src := updater.NewForgejoSource("git.okcu.io", "root", "drover-go", "windows-amd64.exe")
rel, hasUpdate, err := updater.CheckForUpdate(ctx, src, Version)
checkCtx, cancelCheck := context.WithTimeout(context.Background(), 8*time.Second)
rel, hasUpdate, err := updater.CheckForUpdate(checkCtx, src, Version)
cancelCheck()
if err != nil || !hasUpdate {
// Silent: offline, slow network, or already up to date — none of
// these should interrupt the user.
return
}
if !confirmUpdateDialog(rel) {
return
}
if err := updater.ApplyUpdate(ctx, rel, nil); err != nil {
errorDialog(fmt.Sprintf("Update failed: %v", err))
applyCtx, cancelApply := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelApply()
if err := updater.ApplyUpdate(applyCtx, rel, nil); err != nil {
// Apply failed — surface this one (sha mismatch, write error,
// disk full are not silent-fail cases). The user can keep using
// the current version after dismissing the dialog.
errorDialog(fmt.Sprintf("Update to %s failed: %v\n\nContinuing on current version.", rel.TagName, err))
return
}
@@ -48,36 +51,10 @@ func autoUpdateOnStartup() {
return
}
// Successfully spawned the new version — exit cleanly so it can take over.
// Successfully spawned the new version — exit cleanly so it takes over.
os.Exit(0)
}
// confirmUpdateDialog asks the user whether to apply the available update.
// Returns true on Yes (IDYES = 6).
func confirmUpdateDialog(rel *updater.Release) bool {
user32 := windows.NewLazySystemDLL("user32.dll")
messageBox := user32.NewProc("MessageBoxW")
body := fmt.Sprintf(
"A new version is available.\n\n"+
"Current: v%s\n"+
"Latest: %s\n\n"+
"Install it now? Drover-Go will restart automatically.",
Version, rel.TagName,
)
title := "Drover-Go — Update available"
bodyW, _ := windows.UTF16PtrFromString(body)
titleW, _ := windows.UTF16PtrFromString(title)
// MB_YESNO | MB_ICONQUESTION | MB_SETFOREGROUND | MB_TOPMOST | MB_DEFBUTTON1
const flags = 0x00000004 | 0x00000020 | 0x00010000 | 0x00040000 | 0x00000000
r, _, _ := messageBox.Call(0, uintptr(unsafe.Pointer(bodyW)), uintptr(unsafe.Pointer(titleW)), flags)
const IDYES = 6
return r == IDYES
}
func errorDialog(msg string) {
user32 := windows.NewLazySystemDLL("user32.dll")
messageBox := user32.NewProc("MessageBoxW")