Skip to content

Commit 4fa0ddf

Browse files
Copilotintel352
andcommitted
fix: use semver comparison for version update check
Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
1 parent ef14908 commit 4fa0ddf

2 files changed

Lines changed: 145 additions & 3 deletions

File tree

cmd/wfctl/update.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"runtime"
1414
"strings"
1515
"time"
16+
17+
"golang.org/x/mod/semver"
1618
)
1719

1820
const (
@@ -67,7 +69,7 @@ Options:
6769
current := strings.TrimPrefix(version, "v")
6870

6971
if *checkOnly {
70-
if current == "dev" || latest == current {
72+
if current == "dev" || !isNewerVersion(latest, current) {
7173
fmt.Printf("wfctl is up to date (version %s)\n", version)
7274
} else {
7375
fmt.Printf("Update available: %s → %s\n", version, rel.TagName)
@@ -77,7 +79,7 @@ Options:
7779
return nil
7880
}
7981

80-
if latest == current && current != "dev" {
82+
if current != "dev" && !isNewerVersion(latest, current) {
8183
fmt.Printf("wfctl %s is already the latest version.\n", version)
8284
return nil
8385
}
@@ -151,13 +153,32 @@ func checkForUpdateNotice() <-chan struct{} {
151153
}
152154
latest := strings.TrimPrefix(rel.TagName, "v")
153155
current := strings.TrimPrefix(version, "v")
154-
if latest != "" && latest != current {
156+
if isNewerVersion(latest, current) {
155157
fmt.Fprintf(os.Stderr, "\n⚡ wfctl %s is available (you have %s). Run 'wfctl update' to upgrade.\n\n", rel.TagName, version)
156158
}
157159
}()
158160
return done
159161
}
160162

163+
// isNewerVersion reports whether latestVer is strictly greater than currentVer
164+
// using semantic versioning. Both arguments may optionally include a "v" prefix.
165+
// Returns false if either version string is not valid semver.
166+
func isNewerVersion(latestVer, currentVer string) bool {
167+
// golang.org/x/mod/semver requires the "v" prefix.
168+
lv := latestVer
169+
if !strings.HasPrefix(lv, "v") {
170+
lv = "v" + lv
171+
}
172+
cv := currentVer
173+
if !strings.HasPrefix(cv, "v") {
174+
cv = "v" + cv
175+
}
176+
if !semver.IsValid(lv) || !semver.IsValid(cv) {
177+
return false
178+
}
179+
return semver.Compare(lv, cv) > 0
180+
}
181+
161182
// fetchLatestRelease queries the GitHub releases API for the latest release.
162183
func fetchLatestRelease() (*githubRelease, error) {
163184
url := githubReleasesURL

cmd/wfctl/update_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,127 @@ func TestDownloadWithTimeout_Success(t *testing.T) {
276276
}
277277
}
278278

279+
func TestIsNewerVersion(t *testing.T) {
280+
tests := []struct {
281+
latest string
282+
current string
283+
want bool
284+
}{
285+
// Newer available
286+
{"v0.3.43", "v0.3.42", true},
287+
{"0.3.43", "0.3.42", true},
288+
{"v1.0.0", "v0.9.9", true},
289+
// Same version
290+
{"v0.3.42", "v0.3.42", false},
291+
// Older version reported as "latest" (the bug scenario)
292+
{"v0.3.41", "v0.3.42", false},
293+
{"v0.2.0", "v1.0.0", false},
294+
// Invalid semver
295+
{"not-a-version", "v1.0.0", false},
296+
{"v1.0.0", "not-a-version", false},
297+
{"", "v1.0.0", false},
298+
}
299+
for _, tt := range tests {
300+
t.Run(fmt.Sprintf("latest=%s current=%s", tt.latest, tt.current), func(t *testing.T) {
301+
got := isNewerVersion(tt.latest, tt.current)
302+
if got != tt.want {
303+
t.Errorf("isNewerVersion(%q, %q) = %v, want %v", tt.latest, tt.current, got, tt.want)
304+
}
305+
})
306+
}
307+
}
308+
309+
func TestCheckForUpdateNotice_OlderReleaseSuppressed(t *testing.T) {
310+
// Regression test: when running a newer version than the latest GitHub release,
311+
// no update notice should be printed.
312+
origVersion := version
313+
version = "v0.3.42"
314+
defer func() { version = origVersion }()
315+
316+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
317+
rel := githubRelease{
318+
TagName: "v0.3.41", // older than current
319+
HTMLURL: "https://github.com/GoCodeAlone/workflow/releases/tag/v0.3.41",
320+
Assets: []githubAsset{},
321+
}
322+
w.Header().Set("Content-Type", "application/json")
323+
_ = json.NewEncoder(w).Encode(rel)
324+
}))
325+
defer srv.Close()
326+
327+
githubReleasesURLOverride = srv.URL
328+
defer func() { githubReleasesURLOverride = "" }()
329+
330+
// Capture stderr to ensure no update notice is printed.
331+
origStderr := os.Stderr
332+
r, w, _ := os.Pipe()
333+
os.Stderr = w
334+
335+
done := checkForUpdateNotice()
336+
<-done
337+
338+
w.Close()
339+
var buf [512]byte
340+
n, _ := r.Read(buf[:])
341+
os.Stderr = origStderr
342+
343+
output := string(buf[:n])
344+
if output != "" {
345+
t.Errorf("expected no update notice for older release, got: %q", output)
346+
}
347+
}
348+
349+
func TestRunUpdate_CheckOnly_OlderRelease(t *testing.T) {
350+
// When current version is newer than the GitHub release, --check should
351+
// report "up to date" rather than showing a spurious update notice.
352+
origVersion := version
353+
version = "v0.3.42"
354+
defer func() { version = origVersion }()
355+
356+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
357+
rel := githubRelease{
358+
TagName: "v0.3.41",
359+
HTMLURL: "https://example.com",
360+
Assets: []githubAsset{},
361+
}
362+
w.Header().Set("Content-Type", "application/json")
363+
_ = json.NewEncoder(w).Encode(rel)
364+
}))
365+
defer srv.Close()
366+
367+
githubReleasesURLOverride = srv.URL
368+
defer func() { githubReleasesURLOverride = "" }()
369+
370+
if err := runUpdate([]string{"--check"}); err != nil {
371+
t.Fatalf("unexpected error: %v", err)
372+
}
373+
}
374+
375+
func TestRunUpdate_OlderRelease_NoDownload(t *testing.T) {
376+
// When the current version is newer, runUpdate should not attempt to download.
377+
origVersion := version
378+
version = "v0.3.42"
379+
defer func() { version = origVersion }()
380+
381+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
382+
rel := githubRelease{
383+
TagName: "v0.3.41",
384+
HTMLURL: "https://example.com",
385+
Assets: []githubAsset{},
386+
}
387+
w.Header().Set("Content-Type", "application/json")
388+
_ = json.NewEncoder(w).Encode(rel)
389+
}))
390+
defer srv.Close()
391+
392+
githubReleasesURLOverride = srv.URL
393+
defer func() { githubReleasesURLOverride = "" }()
394+
395+
if err := runUpdate([]string{}); err != nil {
396+
t.Fatalf("unexpected error: %v", err)
397+
}
398+
}
399+
279400
func TestDownloadWithTimeout_HTTPError(t *testing.T) {
280401
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
281402
http.Error(w, "gone", http.StatusGone)

0 commit comments

Comments
 (0)