4 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
root 19f851afb0 Auto-update on startup: prompt + apply + auto-restart
Build / test (push) Successful in 1m15s
Build / build-windows (push) Successful in 57s
Release / release (push) Successful in 3m16s
Double-clicking drover.exe now silently checks for updates first
(8s timeout, ignores network failures). If a newer release is
published, a YES/NO message box asks the user; on YES we download,
verify SHA256, apply via selfupdate, and re-launch the binary so
the user immediately sees the new version's window.

NO or no update → straight to the smoke-test window as before.
Update errors show an error dialog, then continue.

Implementation:
- internal/updater is reused as-is — the existing CheckForUpdate +
  ApplyUpdate API is enough.
- autoupdate_windows.go owns the dialog and re-launch logic
  (golang.org/x/sys/windows for MessageBoxW, os/exec for fresh
  process). autoupdate_other.go is the no-op stub for Linux CI.
- relaunchSelf uses cmd.Start (fire and forget) — no Wait. Current
  process os.Exit(0) immediately so the OS doesn't keep both
  generations of drover running.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 03:21:59 +03:00
root 9bdbcd4d88 workflows: disable docker-clean so apt cache survives between runs
Build / test (push) Successful in 1m13s
Build / build-windows (push) Successful in 56s
Release / release (push) Successful in 3m15s
Debian-based Docker images install /etc/apt/apt.conf.d/docker-clean
which runs 'apt-get clean' after every apt-get install — wipes out
/var/cache/apt/archives so there's nothing for actions/cache to save.

Fix: remove the docker-clean drop-in and add keep-cache config first
thing in each job, before any apt activity. Also bump apt cache key
to v2 since the previous v1 was always empty.

Wait time on the Wine + Inno Setup step should drop from ~1m20s to
~10s on warm runs (debs already in /var/cache/apt/archives, dpkg
just installs them locally).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 03:16:09 +03:00
5 changed files with 119 additions and 12 deletions
+10
View File
@@ -25,6 +25,11 @@ jobs:
test: test:
runs-on: go runs-on: go
steps: steps:
- name: Disable apt auto-clean (preserve cache for actions/cache)
run: |
rm -f /etc/apt/apt.conf.d/docker-clean
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
- name: Install Node (required by actions/cache) - name: Install Node (required by actions/cache)
run: | run: |
apt-get update >/dev/null apt-get update >/dev/null
@@ -61,6 +66,11 @@ jobs:
runs-on: go runs-on: go
needs: test needs: test
steps: steps:
- name: Disable apt auto-clean (preserve cache for actions/cache)
run: |
rm -f /etc/apt/apt.conf.d/docker-clean
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
- name: Install Node (required by actions/cache) - name: Install Node (required by actions/cache)
run: | run: |
apt-get update >/dev/null apt-get update >/dev/null
+16 -9
View File
@@ -16,6 +16,15 @@ jobs:
release: release:
runs-on: go runs-on: go
steps: steps:
# Debian-based Docker images ship /etc/apt/apt.conf.d/docker-clean
# which deletes /var/cache/apt/archives after every apt-get install.
# That defeats actions/cache for the apt cache; disable it before
# any apt operations.
- name: Disable apt auto-clean (preserve cache for actions/cache)
run: |
rm -f /etc/apt/apt.conf.d/docker-clean
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
- name: Install Node (required by actions/cache) - name: Install Node (required by actions/cache)
run: | run: |
apt-get update >/dev/null apt-get update >/dev/null
@@ -39,15 +48,13 @@ jobs:
key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }} key: go-${{ runner.os }}-${{ hashFiles('**/go.sum') }}
restore-keys: go-${{ runner.os }}- restore-keys: go-${{ runner.os }}-
# Cache apt downloads — saves ~50s on the wine + innoextract install. # NOTE: actions/cache for the apt archive (~300 MB) is disabled. The
# Bump the cache key (-v2, -v3, ...) when the package list changes. # save step works (~28s in v0.1.4) but restore times out on the
- name: Cache apt packages # next run — Gitea's cache server can't push 300 MB back fast
uses: actions/cache@v4 # enough. The Wine + Inno Setup install stays at ~1m20s. The
with: # right fix is a pre-baked Docker image (golang:1.25 + wine +
path: | # innoextract + xauth + wine32:i386) pushed to git.okcu.io as
/var/cache/apt/archives # the job's container.image. Tracked as future work.
/var/lib/apt/lists
key: apt-trixie-wine-innoextract-v1
- name: Extract version from tag - name: Extract version from tag
id: version id: version
+5
View File
@@ -0,0 +1,5 @@
//go:build !windows
package main
func autoUpdateOnStartup() {}
+83
View File
@@ -0,0 +1,83 @@
//go:build windows
package main
import (
"context"
"fmt"
"os"
"os/exec"
"time"
"unsafe"
"git.okcu.io/root/drover-go/internal/updater"
"golang.org/x/sys/windows"
)
// 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, 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() {
src := updater.NewForgejoSource("git.okcu.io", "root", "drover-go", "windows-amd64.exe")
checkCtx, cancelCheck := context.WithTimeout(context.Background(), 8*time.Second)
rel, hasUpdate, err := updater.CheckForUpdate(checkCtx, src, Version)
cancelCheck()
if err != nil || !hasUpdate {
return
}
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
}
if err := relaunchSelf(); err != nil {
errorDialog(fmt.Sprintf("Update applied but re-launch failed: %v\n\nPlease restart drover.exe manually.", err))
return
}
// Successfully spawned the new version — exit cleanly so it takes over.
os.Exit(0)
}
func errorDialog(msg string) {
user32 := windows.NewLazySystemDLL("user32.dll")
messageBox := user32.NewProc("MessageBoxW")
bodyW, _ := windows.UTF16PtrFromString(msg)
titleW, _ := windows.UTF16PtrFromString("Drover-Go — Error")
// MB_OK | MB_ICONERROR | MB_TOPMOST
const flags = 0x00000000 | 0x00000010 | 0x00040000
messageBox.Call(0, uintptr(unsafe.Pointer(bodyW)), uintptr(unsafe.Pointer(titleW)), flags)
}
// relaunchSelf starts a fresh copy of the (now-updated) executable in the
// background and returns. The caller is expected to os.Exit(0) immediately
// after — the OS handles the brief overlap fine, and the new process inherits
// nothing from us beyond the working directory and arguments.
func relaunchSelf() error {
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("locate self: %w", err)
}
cmd := exec.Command(exe, os.Args[1:]...)
cmd.Stdin, cmd.Stdout, cmd.Stderr = nil, nil, nil
return cmd.Start()
}
+5 -3
View File
@@ -45,10 +45,12 @@ func newRootCmd() *cobra.Command {
Version: fmt.Sprintf("%s (commit %s, built %s)", Version, Commit, BuildDate), Version: fmt.Sprintf("%s (commit %s, built %s)", Version, Commit, BuildDate),
SilenceUsage: true, SilenceUsage: true,
SilenceErrors: false, SilenceErrors: false,
// No subcommand and no flags = end-user double-clicked the exe; // No subcommand and no flags = end-user double-clicked the exe.
// open the smoke-test window instead of dumping CLI help to a // First do a quick update check (silent if no network or already
// console they didn't ask for. // current); if an update is available we prompt, apply, and
// re-launch ourselves. Then show the smoke-test window.
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
autoUpdateOnStartup()
showTestWindow() showTestWindow()
return nil return nil
}, },