From 8eff1497a96cd78f55ed0d106ff67d0a5012f207 Mon Sep 17 00:00:00 2001 From: Matee Ullah Malik Date: Thu, 21 Aug 2025 02:44:58 +0500 Subject: [PATCH 1/2] fix: enable minor version auto-updates and prevent unstable release installation --- sn-manager/cmd/init.go | 6 ++--- sn-manager/cmd/start.go | 4 +-- sn-manager/go.mod | 1 + sn-manager/go.sum | 2 ++ sn-manager/internal/github/client.go | 20 +++++++++++++- sn-manager/internal/github/client_mock.go | 29 ++++++++++++++++++--- sn-manager/internal/updater/updater.go | 8 +++--- sn-manager/internal/updater/updater_test.go | 16 ++++++------ 8 files changed, 64 insertions(+), 22 deletions(-) diff --git a/sn-manager/cmd/init.go b/sn-manager/cmd/init.go index 07078fc9..a0353955 100644 --- a/sn-manager/cmd/init.go +++ b/sn-manager/cmd/init.go @@ -177,10 +177,10 @@ func runInit(cmd *cobra.Command, args []string) error { versionMgr := version.NewManager(managerHome) client := github.NewClient(config.GitHubRepo) - // Get latest release - release, err := client.GetLatestRelease() + // Get latest stable release + release, err := client.GetLatestStableRelease() if err != nil { - return fmt.Errorf("failed to get latest release: %w", err) + return fmt.Errorf("failed to get latest stable release: %w", err) } targetVersion := release.TagName diff --git a/sn-manager/cmd/start.go b/sn-manager/cmd/start.go index a7c3c809..c13a8df6 100644 --- a/sn-manager/cmd/start.go +++ b/sn-manager/cmd/start.go @@ -183,9 +183,9 @@ func ensureBinaryExists(home string, cfg *config.Config) error { fmt.Println("No SuperNode binary found. Downloading latest version...") client := github.NewClient(config.GitHubRepo) - release, err := client.GetLatestRelease() + release, err := client.GetLatestStableRelease() if err != nil { - return fmt.Errorf("failed to get latest release: %w", err) + return fmt.Errorf("failed to get latest stable release: %w", err) } targetVersion := release.TagName diff --git a/sn-manager/go.mod b/sn-manager/go.mod index de638b50..65961fb8 100644 --- a/sn-manager/go.mod +++ b/sn-manager/go.mod @@ -8,6 +8,7 @@ require ( github.com/golang/mock v1.6.0 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.10.0 + go.uber.org/mock v0.5.2 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/sn-manager/go.sum b/sn-manager/go.sum index 3225e6f0..35802a5a 100644 --- a/sn-manager/go.sum +++ b/sn-manager/go.sum @@ -88,6 +88,8 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= diff --git a/sn-manager/internal/github/client.go b/sn-manager/internal/github/client.go index 1c26d614..cd50d59b 100644 --- a/sn-manager/internal/github/client.go +++ b/sn-manager/internal/github/client.go @@ -1,4 +1,4 @@ -//go:generate mockgen -destination=client_mock.go -package=github -source=client.go +//go:generate go run go.uber.org/mock/mockgen -destination=client_mock.go -package=github -source=client.go package github @@ -15,6 +15,7 @@ import ( type GithubClient interface { GetLatestRelease() (*Release, error) + GetLatestStableRelease() (*Release, error) ListReleases() ([]*Release, error) GetRelease(tag string) (*Release, error) GetSupernodeDownloadURL(version string) (string, error) @@ -124,6 +125,23 @@ func (c *Client) ListReleases() ([]*Release, error) { return releases, nil } +// GetLatestStableRelease fetches the latest stable (non-prerelease, non-draft) release from GitHub +func (c *Client) GetLatestStableRelease() (*Release, error) { + releases, err := c.ListReleases() + if err != nil { + return nil, fmt.Errorf("failed to list releases: %w", err) + } + + // Filter for stable releases (not draft, not prerelease) + for _, release := range releases { + if !release.Draft && !release.Prerelease { + return release, nil + } + } + + return nil, fmt.Errorf("no stable releases found") +} + // GetRelease fetches a specific release by tag func (c *Client) GetRelease(tag string) (*Release, error) { url := fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", c.repo, tag) diff --git a/sn-manager/internal/github/client_mock.go b/sn-manager/internal/github/client_mock.go index 79d863fc..ad8496d0 100644 --- a/sn-manager/internal/github/client_mock.go +++ b/sn-manager/internal/github/client_mock.go @@ -1,5 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: client.go +// +// Generated by this command: +// +// mockgen -destination=client_mock.go -package=github -source=client.go +// // Package github is a generated GoMock package. package github @@ -7,13 +12,14 @@ package github import ( reflect "reflect" - gomock "github.com/golang/mock/gomock" + gomock "go.uber.org/mock/gomock" ) // MockGithubClient is a mock of GithubClient interface. type MockGithubClient struct { ctrl *gomock.Controller recorder *MockGithubClientMockRecorder + isgomock struct{} } // MockGithubClientMockRecorder is the mock recorder for MockGithubClient. @@ -42,7 +48,7 @@ func (m *MockGithubClient) DownloadBinary(url, destPath string, progress func(in } // DownloadBinary indicates an expected call of DownloadBinary. -func (mr *MockGithubClientMockRecorder) DownloadBinary(url, destPath, progress interface{}) *gomock.Call { +func (mr *MockGithubClientMockRecorder) DownloadBinary(url, destPath, progress any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadBinary", reflect.TypeOf((*MockGithubClient)(nil).DownloadBinary), url, destPath, progress) } @@ -62,6 +68,21 @@ func (mr *MockGithubClientMockRecorder) GetLatestRelease() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestRelease", reflect.TypeOf((*MockGithubClient)(nil).GetLatestRelease)) } +// GetLatestStableRelease mocks base method. +func (m *MockGithubClient) GetLatestStableRelease() (*Release, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetLatestStableRelease") + ret0, _ := ret[0].(*Release) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetLatestStableRelease indicates an expected call of GetLatestStableRelease. +func (mr *MockGithubClientMockRecorder) GetLatestStableRelease() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestStableRelease", reflect.TypeOf((*MockGithubClient)(nil).GetLatestStableRelease)) +} + // GetRelease mocks base method. func (m *MockGithubClient) GetRelease(tag string) (*Release, error) { m.ctrl.T.Helper() @@ -72,7 +93,7 @@ func (m *MockGithubClient) GetRelease(tag string) (*Release, error) { } // GetRelease indicates an expected call of GetRelease. -func (mr *MockGithubClientMockRecorder) GetRelease(tag interface{}) *gomock.Call { +func (mr *MockGithubClientMockRecorder) GetRelease(tag any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRelease", reflect.TypeOf((*MockGithubClient)(nil).GetRelease), tag) } @@ -87,7 +108,7 @@ func (m *MockGithubClient) GetSupernodeDownloadURL(version string) (string, erro } // GetSupernodeDownloadURL indicates an expected call of GetSupernodeDownloadURL. -func (mr *MockGithubClientMockRecorder) GetSupernodeDownloadURL(version interface{}) *gomock.Call { +func (mr *MockGithubClientMockRecorder) GetSupernodeDownloadURL(version any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSupernodeDownloadURL", reflect.TypeOf((*MockGithubClient)(nil).GetSupernodeDownloadURL), version) } diff --git a/sn-manager/internal/updater/updater.go b/sn-manager/internal/updater/updater.go index 0405e961..6e829dc8 100644 --- a/sn-manager/internal/updater/updater.go +++ b/sn-manager/internal/updater/updater.go @@ -140,10 +140,10 @@ func (u *AutoUpdater) shouldUpdate(current, latest string) bool { return false } - // Only update within same major.minor version - if currentParts[0] != latestParts[0] || currentParts[1] != latestParts[1] { - log.Printf("Major/minor version mismatch, skipping update: %s.%s vs %s.%s", - currentParts[0], currentParts[1], latestParts[0], latestParts[1]) + // Only update within same major version (allow minor and patch updates) + if currentParts[0] != latestParts[0] { + log.Printf("Major version mismatch, skipping update: %s vs %s", + currentParts[0], latestParts[0]) return false } diff --git a/sn-manager/internal/updater/updater_test.go b/sn-manager/internal/updater/updater_test.go index 0660dc84..6568c8e8 100644 --- a/sn-manager/internal/updater/updater_test.go +++ b/sn-manager/internal/updater/updater_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/golang/mock/gomock" + "go.uber.org/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" @@ -91,8 +91,8 @@ func TestAutoUpdater_ShouldUpdate(t *testing.T) { {"patch_update", "v1.0.0", "v1.0.1", true}, {"patch_update_no_prefix", "1.0.0", "1.0.1", true}, - // Minor version updates (should NOT update based on current logic) - {"minor_update", "v1.0.0", "v1.1.0", false}, + // Minor version updates (should update within same major version) + {"minor_update", "v1.0.0", "v1.1.0", true}, {"major_update", "v1.0.0", "v2.0.0", false}, // Same version (should not update) @@ -288,11 +288,11 @@ func TestAutoUpdater_CheckAndUpdate(t *testing.T) { expectUpdate: false, }, { - name: "minor_version_update_should_skip", + name: "minor_version_update_should_proceed", currentVersion: "v1.0.0", latestVersion: "v1.1.0", gatewayIdle: true, - expectUpdate: false, + expectUpdate: true, }, } @@ -1198,11 +1198,11 @@ func TestAutoUpdater_UpdatePolicyLogic(t *testing.T) { description: "Patch updates (1.2.3 -> 1.2.4) should be allowed", }, { - name: "minor_update_blocked", + name: "minor_update_allowed", currentVersion: "v1.2.3", latestVersion: "v1.3.0", - shouldUpdate: false, - description: "Minor updates (1.2.x -> 1.3.x) should be blocked", + shouldUpdate: true, + description: "Minor updates (1.2.x -> 1.3.x) should be allowed within same major version", }, { name: "major_update_blocked", From 1fe7119bf58d4f055e3167c0cc8e2bb78f91498b Mon Sep 17 00:00:00 2001 From: Matee Ullah Malik Date: Thu, 21 Aug 2025 02:56:37 +0500 Subject: [PATCH 2/2] feat: enhance update checking logic --- sn-manager/cmd/check.go | 33 +++++++++++++++------ sn-manager/internal/updater/updater.go | 4 +-- sn-manager/internal/updater/updater_test.go | 4 +-- 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/sn-manager/cmd/check.go b/sn-manager/cmd/check.go index c38a6c0a..e3e7e530 100644 --- a/sn-manager/cmd/check.go +++ b/sn-manager/cmd/check.go @@ -5,6 +5,7 @@ import ( "github.com/LumeraProtocol/supernode/v2/sn-manager/internal/config" "github.com/LumeraProtocol/supernode/v2/sn-manager/internal/github" + "github.com/LumeraProtocol/supernode/v2/sn-manager/internal/updater" "github.com/LumeraProtocol/supernode/v2/sn-manager/internal/utils" "github.com/spf13/cobra" ) @@ -33,10 +34,10 @@ func runCheck(cmd *cobra.Command, args []string) error { // Create GitHub client client := github.NewClient(config.GitHubRepo) - // Get latest release - release, err := client.GetLatestRelease() + // Get latest stable release + release, err := client.GetLatestStableRelease() if err != nil { - return fmt.Errorf("failed to check for updates: %w", err) + return fmt.Errorf("failed to check for stable updates: %w", err) } fmt.Printf("\nLatest release: %s\n", release.TagName) @@ -46,19 +47,33 @@ func runCheck(cmd *cobra.Command, args []string) error { cmp := utils.CompareVersions(cfg.Updates.CurrentVersion, release.TagName) if cmp < 0 { - fmt.Printf("\n✓ Update available: %s → %s\n", cfg.Updates.CurrentVersion, release.TagName) - fmt.Printf("Published: %s\n", release.PublishedAt.Format("2006-01-02 15:04:05")) + // Use the same logic as auto-updater to determine update eligibility + managerHome := config.GetManagerHome() + autoUpdater := updater.New(managerHome, cfg) + wouldAutoUpdate := autoUpdater.ShouldUpdate(cfg.Updates.CurrentVersion, release.TagName) + + if wouldAutoUpdate { + fmt.Printf("\n✓ Update available: %s → %s\n", cfg.Updates.CurrentVersion, release.TagName) + fmt.Printf("Published: %s\n", release.PublishedAt.Format("2006-01-02 15:04:05")) + fmt.Println("\n✓ This update will be applied automatically if auto-upgrade is enabled") + fmt.Println(" Or manually with: sn-manager get") + } else { + fmt.Printf("\n⚠ Major update available: %s → %s\n", cfg.Updates.CurrentVersion, release.TagName) + fmt.Printf("Published: %s\n", release.PublishedAt.Format("2006-01-02 15:04:05")) + fmt.Println("\n⚠ Major version updates require manual installation:") + fmt.Printf(" sn-manager get %s\n", release.TagName) + fmt.Printf(" sn-manager use %s\n", release.TagName) + fmt.Println("\n⚠ Auto-updater will not automatically install major version updates") + } if release.Body != "" { fmt.Println("\nRelease notes:") fmt.Println(release.Body) } - - fmt.Println("\nTo download this version, run: sn-manager get") } else if cmp == 0 { - fmt.Println("\n✓ You are running the latest version") + fmt.Println("\n✓ You are running the latest stable version") } else { - fmt.Printf("\n⚠ You are running a newer version than the latest release\n") + fmt.Printf("\n⚠ You are running a newer version than the latest stable release\n") } return nil diff --git a/sn-manager/internal/updater/updater.go b/sn-manager/internal/updater/updater.go index 6e829dc8..a825f9e8 100644 --- a/sn-manager/internal/updater/updater.go +++ b/sn-manager/internal/updater/updater.go @@ -96,7 +96,7 @@ func (u *AutoUpdater) checkAndUpdate(ctx context.Context) { log.Printf("Version comparison: current=%s, latest=%s", currentVersion, latestVersion) - if !u.shouldUpdate(currentVersion, latestVersion) { + if !u.ShouldUpdate(currentVersion, latestVersion) { log.Printf("Current version %s is up to date", currentVersion) return } @@ -116,7 +116,7 @@ func (u *AutoUpdater) checkAndUpdate(ctx context.Context) { log.Printf("Updated to %s", latestVersion) } -func (u *AutoUpdater) shouldUpdate(current, latest string) bool { +func (u *AutoUpdater) ShouldUpdate(current, latest string) bool { current = strings.TrimPrefix(current, "v") latest = strings.TrimPrefix(latest, "v") diff --git a/sn-manager/internal/updater/updater_test.go b/sn-manager/internal/updater/updater_test.go index 6568c8e8..1725659e 100644 --- a/sn-manager/internal/updater/updater_test.go +++ b/sn-manager/internal/updater/updater_test.go @@ -109,7 +109,7 @@ func TestAutoUpdater_ShouldUpdate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - result := updater.shouldUpdate(tt.current, tt.latest) + result := updater.ShouldUpdate(tt.current, tt.latest) assert.Equal(t, tt.expected, result, "shouldUpdate(%s, %s) = %v, want %v", tt.current, tt.latest, result, tt.expected) }) } @@ -1225,7 +1225,7 @@ func TestAutoUpdater_UpdatePolicyLogic(t *testing.T) { cfg := createTestConfig(t, homeDir, scenario.currentVersion, true, 3600) updater := New(homeDir, cfg) - result := updater.shouldUpdate(scenario.currentVersion, scenario.latestVersion) + result := updater.ShouldUpdate(scenario.currentVersion, scenario.latestVersion) assert.Equal(t, scenario.shouldUpdate, result, scenario.description) }) }