diff --git a/CHANGELOG.md b/CHANGELOG.md index 38118cb..ca49641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.9.6] - 2026-05-26 + +### Added + +- **auto-upgrade**: when the web flags the agent for upgrade + (`POST /api/internal/agent/upgrade` or the "Force update now" button), + the daemon now downloads and replaces the binary in-place, then exits so + the service supervisor (`systemd Restart=always` on Linux, the equivalent + on macOS/Windows) respawns on the new version. No `unarr update` step + required from the user. Still opt-in — only fires when the server sends + the upgrade signal. + +### Changed + +- The `OnUpgrade` daemon callback no longer just logs `run unarr self-update`; + it now triggers the actual upgrade in a background goroutine. + ## [0.9.5] - 2026-05-26 ### Added diff --git a/internal/agent/daemon.go b/internal/agent/daemon.go index 109dfbb..e5e4c60 100644 --- a/internal/agent/daemon.go +++ b/internal/agent/daemon.go @@ -11,6 +11,8 @@ import ( "strings" "sync/atomic" "time" + + "github.com/torrentclaw/unarr/internal/upgrade" ) // DaemonConfig holds daemon runtime settings. @@ -231,10 +233,12 @@ func (d *Daemon) Run(ctx context.Context) error { } } d.sync.OnUpgrade = func(version string) { - if version != d.lastNotifiedVersion { - d.lastNotifiedVersion = version - log.Printf("New version available: %s (run `unarr self-update` to upgrade)", version) + if version == d.lastNotifiedVersion { + return } + d.lastNotifiedVersion = version + log.Printf("[upgrade] new version available: %s — applying auto-upgrade", version) + go d.applyAutoUpgrade(version) } d.sync.OnScan = func() { log.Printf("Library scan requested by server") @@ -281,6 +285,31 @@ func (d *Daemon) Deregister() { RemoveState() } +// applyAutoUpgrade downloads the target version and exits so the service +// supervisor (systemd Restart=always on Linux) respawns on the new binary. +// Triggered by the server's upgrade signal — opt-in flag set by the user from +// the web UI; the daemon never auto-upgrades on a passive version bump. +func (d *Daemon) applyAutoUpgrade(targetVersion string) { + currentClean := strings.TrimPrefix(d.cfg.Version, "v") + upgrader := &upgrade.Upgrader{ + CurrentVersion: currentClean, + OnProgress: func(msg string) { + log.Printf("[upgrade] %s", msg) + }, + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + result := upgrader.Execute(ctx, targetVersion) + if !result.Success { + log.Printf("[upgrade] auto-upgrade failed: %v", result.Error) + return + } + log.Printf("[upgrade] upgraded v%s → v%s; exiting so service supervisor restarts on new binary", + result.OldVersion, result.NewVersion) + time.Sleep(500 * time.Millisecond) + os.Exit(0) +} + // isTransientError returns true for errors worth retrying (429, 5xx, network). func isTransientError(err error) bool { if err == nil { diff --git a/internal/cmd/version.go b/internal/cmd/version.go index 3009c5a..32f1ce3 100644 --- a/internal/cmd/version.go +++ b/internal/cmd/version.go @@ -1,4 +1,4 @@ package cmd // Version is the CLI version. Overridden by goreleaser ldflags at release time. -var Version = "0.9.5" +var Version = "0.9.6"