From b8cf85af3b19a566cdc777d8029cbc925c57efa9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 19 Apr 2026 13:43:58 +0000 Subject: [PATCH] refactor(updater): default auto-update to notify-only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-upgrades are a piece of remote-driven control state: they can silently change what `openboot ` does from one run to the next. For a tool that people run occasionally rather than daily, that tradeoff isn't worth it — a visible "new version available" notice is enough. LoadUserConfig now defaults to AutoUpdateNotify. Users who want the previous silent self-upgrade behavior can still opt in by setting "autoupdate": "true" in ~/.openboot/config.json; "false" still fully silences the check. Tests updated to match the new default. --- internal/updater/updater.go | 16 +++++++++++----- internal/updater/updater_extra_test.go | 4 ++-- internal/updater/updater_test.go | 6 +++--- test/integration/updater_integration_test.go | 4 ++-- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/internal/updater/updater.go b/internal/updater/updater.go index fb1565f..cb0bf4f 100644 --- a/internal/updater/updater.go +++ b/internal/updater/updater.go @@ -52,8 +52,13 @@ type UserConfig struct { AutoUpdate AutoUpdateMode `json:"autoupdate"` } +// LoadUserConfig returns the user's update preference from +// ~/.openboot/config.json. The default is AutoUpdateNotify: we surface a +// one-line "new version available" message but never auto-upgrade the binary +// during a normal command. Users who want silent upgrades can opt in by +// setting "autoupdate": "true"; users who want silence can set "false". func LoadUserConfig() UserConfig { - cfg := UserConfig{AutoUpdate: AutoUpdateEnabled} + cfg := UserConfig{AutoUpdate: AutoUpdateNotify} path, err := getUserConfigPath() if err != nil { return cfg @@ -66,7 +71,7 @@ func LoadUserConfig() UserConfig { return cfg } if cfg.AutoUpdate == "" { - cfg.AutoUpdate = AutoUpdateEnabled + cfg.AutoUpdate = AutoUpdateNotify } return cfg } @@ -98,15 +103,16 @@ func IsHomebrewInstall() bool { return isHomebrewPath(exe) } -// AutoUpgrade checks for a newer version and upgrades if appropriate. +// AutoUpgrade checks for a newer version and, by default, only prints a notice. +// Silent self-upgrades are opt-in via ~/.openboot/config.json. // // Flow: // 1. Kill switch: OPENBOOT_DISABLE_AUTOUPDATE=1 // 2. Dev guard: currentVersion == "dev" // 3. UserConfig (applies to ALL install methods): -// disabled → exit, notify → show message, enabled → upgrade +// disabled → exit, notify (default) → show message, enabled → upgrade // 4. resolveLatestVersion: uses 24h cache, falls back to sync GitHub API -// 5. Upgrade method: Homebrew → brew upgrade, Direct → download binary +// 5. Upgrade method (enabled mode only): Homebrew → brew upgrade, Direct → download binary func AutoUpgrade(currentVersion string) { if os.Getenv("OPENBOOT_DISABLE_AUTOUPDATE") == "1" { return diff --git a/internal/updater/updater_extra_test.go b/internal/updater/updater_extra_test.go index fc22054..588e4c3 100644 --- a/internal/updater/updater_extra_test.go +++ b/internal/updater/updater_extra_test.go @@ -250,13 +250,13 @@ func TestIsNewerVersion_ExtendedCases(t *testing.T) { } // --------------------------------------------------------------------------- -// LoadUserConfig — AutoUpdate empty field defaults to Enabled +// LoadUserConfig — AutoUpdate empty field defaults to Notify // --------------------------------------------------------------------------- func TestLoadUserConfig_DefaultWhenMissing(t *testing.T) { t.Setenv("HOME", t.TempDir()) cfg := LoadUserConfig() - assert.Equal(t, AutoUpdateEnabled, cfg.AutoUpdate) + assert.Equal(t, AutoUpdateNotify, cfg.AutoUpdate) } // --------------------------------------------------------------------------- diff --git a/internal/updater/updater_test.go b/internal/updater/updater_test.go index 274be20..2b9e1f8 100644 --- a/internal/updater/updater_test.go +++ b/internal/updater/updater_test.go @@ -212,7 +212,7 @@ func TestGetUserConfigPath(t *testing.T) { func TestLoadUserConfig_Default_NoFile(t *testing.T) { t.Setenv("HOME", t.TempDir()) cfg := LoadUserConfig() - assert.Equal(t, AutoUpdateEnabled, cfg.AutoUpdate) + assert.Equal(t, AutoUpdateNotify, cfg.AutoUpdate) } func TestLoadUserConfig_FromFile(t *testing.T) { @@ -236,7 +236,7 @@ func TestLoadUserConfig_InvalidJSON(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(cfgDir, "config.json"), []byte("{bad json"), 0644)) cfg := LoadUserConfig() - assert.Equal(t, AutoUpdateEnabled, cfg.AutoUpdate) + assert.Equal(t, AutoUpdateNotify, cfg.AutoUpdate) } func TestLoadUserConfig_EmptyAutoUpdate(t *testing.T) { @@ -247,7 +247,7 @@ func TestLoadUserConfig_EmptyAutoUpdate(t *testing.T) { require.NoError(t, os.WriteFile(filepath.Join(cfgDir, "config.json"), []byte(`{"autoupdate":""}`), 0644)) cfg := LoadUserConfig() - assert.Equal(t, AutoUpdateEnabled, cfg.AutoUpdate) + assert.Equal(t, AutoUpdateNotify, cfg.AutoUpdate) } func TestLoadUserConfig_DisabledMode(t *testing.T) { diff --git a/test/integration/updater_integration_test.go b/test/integration/updater_integration_test.go index 2d206a1..a677df9 100644 --- a/test/integration/updater_integration_test.go +++ b/test/integration/updater_integration_test.go @@ -83,8 +83,8 @@ func TestIntegration_Updater_LoadUserConfig_Default(t *testing.T) { // When: we load user config cfg := updater.LoadUserConfig() - // Then: defaults to auto-update enabled - assert.Equal(t, updater.AutoUpdateEnabled, cfg.AutoUpdate) + // Then: defaults to notify-only (no silent self-upgrades). + assert.Equal(t, updater.AutoUpdateNotify, cfg.AutoUpdate) } func TestIntegration_Updater_LoadUserConfig_AllModes(t *testing.T) {