Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions cmd/wfctl/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"runtime"
"strings"
"time"

"golang.org/x/mod/semver"
)

const (
Expand Down Expand Up @@ -67,7 +69,7 @@ Options:
current := strings.TrimPrefix(version, "v")

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

if latest == current && current != "dev" {
if current != "dev" && !isNewerVersion(latest, current) {
fmt.Printf("wfctl %s is already the latest version.\n", version)
return nil
}
Expand Down Expand Up @@ -151,13 +153,32 @@ func checkForUpdateNotice() <-chan struct{} {
}
latest := strings.TrimPrefix(rel.TagName, "v")
current := strings.TrimPrefix(version, "v")
if latest != "" && latest != current {
if isNewerVersion(latest, current) {
fmt.Fprintf(os.Stderr, "\n⚡ wfctl %s is available (you have %s). Run 'wfctl update' to upgrade.\n\n", rel.TagName, version)
}
}()
return done
}

// isNewerVersion reports whether latestVer is strictly greater than currentVer
// using semantic versioning. Both arguments may optionally include a "v" prefix.
// Returns false if either version string is not valid semver.
func isNewerVersion(latestVer, currentVer string) bool {
// golang.org/x/mod/semver requires the "v" prefix.
lv := latestVer
if !strings.HasPrefix(lv, "v") {
lv = "v" + lv
}
cv := currentVer
if !strings.HasPrefix(cv, "v") {
cv = "v" + cv
}
if !semver.IsValid(lv) || !semver.IsValid(cv) {
return false
}
return semver.Compare(lv, cv) > 0
}

// fetchLatestRelease queries the GitHub releases API for the latest release.
func fetchLatestRelease() (*githubRelease, error) {
url := githubReleasesURL
Expand Down
127 changes: 127 additions & 0 deletions cmd/wfctl/update_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,133 @@ func TestDownloadWithTimeout_Success(t *testing.T) {
}
}

func TestIsNewerVersion(t *testing.T) {
tests := []struct {
latest string
current string
want bool
}{
// Newer available
{"v0.3.43", "v0.3.42", true},
{"0.3.43", "0.3.42", true},
{"v1.0.0", "v0.9.9", true},
// Same version
{"v0.3.42", "v0.3.42", false},
// Older version reported as "latest" (the bug scenario)
{"v0.3.41", "v0.3.42", false},
{"v0.2.0", "v1.0.0", false},
// Invalid semver
{"not-a-version", "v1.0.0", false},
{"v1.0.0", "not-a-version", false},
{"", "v1.0.0", false},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("latest=%s current=%s", tt.latest, tt.current), func(t *testing.T) {
got := isNewerVersion(tt.latest, tt.current)
if got != tt.want {
t.Errorf("isNewerVersion(%q, %q) = %v, want %v", tt.latest, tt.current, got, tt.want)
}
})
}
}

func TestCheckForUpdateNotice_OlderReleaseSuppressed(t *testing.T) {
// Regression test: when running a newer version than the latest GitHub release,
// no update notice should be printed.
origVersion := version
version = "v0.3.42"
defer func() { version = origVersion }()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
rel := githubRelease{
TagName: "v0.3.41", // older than current
HTMLURL: "https://github.com/GoCodeAlone/workflow/releases/tag/v0.3.41",
Assets: []githubAsset{},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(rel)
}))
defer srv.Close()

githubReleasesURLOverride = srv.URL
defer func() { githubReleasesURLOverride = "" }()

// Capture stderr to ensure no update notice is printed.
origStderr := os.Stderr
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe: %v", err)
}
os.Stderr = w
t.Cleanup(func() {
os.Stderr = origStderr
r.Close()
})

done := checkForUpdateNotice()
<-done

w.Close()
var buf [512]byte
n, _ := r.Read(buf[:])

output := string(buf[:n])
if output != "" {
t.Errorf("expected no update notice for older release, got: %q", output)
}
}

func TestRunUpdate_CheckOnly_OlderRelease(t *testing.T) {
// When current version is newer than the GitHub release, --check should
// report "up to date" rather than showing a spurious update notice.
origVersion := version
version = "v0.3.42"
defer func() { version = origVersion }()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
rel := githubRelease{
TagName: "v0.3.41",
HTMLURL: "https://example.com",
Assets: []githubAsset{},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(rel)
}))
defer srv.Close()

githubReleasesURLOverride = srv.URL
defer func() { githubReleasesURLOverride = "" }()

if err := runUpdate([]string{"--check"}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

func TestRunUpdate_OlderRelease_NoDownload(t *testing.T) {
// When the current version is newer, runUpdate should not attempt to download.
origVersion := version
version = "v0.3.42"
defer func() { version = origVersion }()

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
rel := githubRelease{
TagName: "v0.3.41",
HTMLURL: "https://example.com",
Assets: []githubAsset{},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(rel)
}))
defer srv.Close()

githubReleasesURLOverride = srv.URL
defer func() { githubReleasesURLOverride = "" }()

if err := runUpdate([]string{}); err != nil {
t.Fatalf("unexpected error: %v", err)
}
}

func TestDownloadWithTimeout_HTTPError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "gone", http.StatusGone)
Expand Down
Loading