fix: handle pre releases

Carlos Alexandro Becker created

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

Change summary

internal/app/app.go            |  7 +++--
internal/update/update.go      | 41 +++++++++++++++++++++++++----------
internal/update/update_test.go | 41 +++++++++++++++++++++--------------
3 files changed, 58 insertions(+), 31 deletions(-)

Detailed changes

internal/app/app.go 🔗

@@ -23,6 +23,7 @@ import (
 	"github.com/charmbracelet/crush/internal/pubsub"
 	"github.com/charmbracelet/crush/internal/session"
 	"github.com/charmbracelet/crush/internal/update"
+	"github.com/charmbracelet/crush/internal/version"
 	"github.com/charmbracelet/x/ansi"
 )
 
@@ -352,12 +353,12 @@ func (app *App) Shutdown() {
 func (app *App) checkForUpdates(ctx context.Context) {
 	checkCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
 	defer cancel()
-	info, err := update.Check(checkCtx, update.Default)
+	info, err := update.Check(checkCtx, version.Version, update.Default)
 	if err != nil || !info.Available() {
 		return
 	}
 	app.events <- pubsub.UpdateAvailableMsg{
-		CurrentVersion: info.CurrentVersion,
-		LatestVersion:  info.LatestVersion,
+		CurrentVersion: info.Current,
+		LatestVersion:  info.Latest,
 	}
 }

internal/update/update.go 🔗

@@ -8,8 +8,6 @@ import (
 	"net/http"
 	"strings"
 	"time"
-
-	"github.com/charmbracelet/crush/internal/version"
 )
 
 const (
@@ -22,22 +20,41 @@ var Default Client = &github{}
 
 // Info contains information about an available update.
 type Info struct {
-	CurrentVersion string
-	LatestVersion  string
-	ReleaseURL     string
+	Current string
+	Latest  string
+	URL     string
 }
 
 // Available returns true if there's an update available.
-func (i Info) Available() bool { return i.CurrentVersion != i.LatestVersion }
+//
+// If both current and latest are stable versions, returns true if versions are
+// different.
+// If current is a pre-release and latest isn't, returns true.
+// If latest is a pre-release and current isn't, returns false.
+func (i Info) Available() bool {
+	cpr := strings.Contains(i.Current, "-")
+	lpr := strings.Contains(i.Latest, "-")
+	// current is pre release
+	if cpr {
+		// latest isn't a prerelease
+		if !lpr {
+			return true
+		}
+	}
+	if lpr && !cpr {
+		return false
+	}
+	return i.Current != i.Latest
+}
 
 // Check checks if a new version is available.
-func Check(ctx context.Context, client Client) (Info, error) {
+func Check(ctx context.Context, current string, client Client) (Info, error) {
 	info := Info{
-		CurrentVersion: version.Version,
-		LatestVersion:  version.Version,
+		Current: current,
+		Latest:  current,
 	}
 
-	if info.CurrentVersion == "devel" || info.CurrentVersion == "unknown" {
+	if info.Current == "devel" || info.Current == "unknown" {
 		return info, nil
 	}
 
@@ -46,8 +63,8 @@ func Check(ctx context.Context, client Client) (Info, error) {
 		return info, fmt.Errorf("failed to fetch latest release: %w", err)
 	}
 
-	info.LatestVersion = strings.TrimPrefix(release.TagName, "v")
-	info.ReleaseURL = release.HTMLURL
+	info.Latest = strings.TrimPrefix(release.TagName, "v")
+	info.URL = release.HTMLURL
 	return info, nil
 }
 

internal/update/update_test.go 🔗

@@ -4,41 +4,50 @@ import (
 	"context"
 	"testing"
 
-	"github.com/charmbracelet/crush/internal/version"
 	"github.com/stretchr/testify/require"
 )
 
 func TestCheckForUpdate_DevelopmentVersion(t *testing.T) {
-	originalVersion := version.Version
-	version.Version = "unknown"
-	t.Cleanup(func() {
-		version.Version = originalVersion
-	})
-
-	info, err := Check(t.Context(), testClient{})
+	info, err := Check(t.Context(), "unknown", testClient{"v0.11.0"})
 	require.NoError(t, err)
 	require.NotNil(t, info)
 	require.False(t, info.Available())
 }
 
 func TestCheckForUpdate_Old(t *testing.T) {
-	originalVersion := version.Version
-	version.Version = "0.10.0"
-	t.Cleanup(func() {
-		version.Version = originalVersion
-	})
-	info, err := Check(t.Context(), testClient{})
+	info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0"})
 	require.NoError(t, err)
 	require.NotNil(t, info)
 	require.True(t, info.Available())
 }
 
-type testClient struct{}
+func TestCheckForUpdate_Beta(t *testing.T) {
+	t.Run("current is stable", func(t *testing.T) {
+		info, err := Check(t.Context(), "v0.10.0", testClient{"v0.11.0-beta.1"})
+		require.NoError(t, err)
+		require.NotNil(t, info)
+		require.False(t, info.Available())
+	})
+	t.Run("current is also beta", func(t *testing.T) {
+		info, err := Check(t.Context(), "v0.11.0-beta.1", testClient{"v0.11.0-beta.2"})
+		require.NoError(t, err)
+		require.NotNil(t, info)
+		require.True(t, info.Available())
+	})
+	t.Run("current is beta, latest isn't", func(t *testing.T) {
+		info, err := Check(t.Context(), "v0.11.0-beta.1", testClient{"v0.11.0"})
+		require.NoError(t, err)
+		require.NotNil(t, info)
+		require.True(t, info.Available())
+	})
+}
+
+type testClient struct{ tag string }
 
 // Latest implements Client.
 func (t testClient) Latest(ctx context.Context) (*Release, error) {
 	return &Release{
-		TagName: "v0.11.0",
+		TagName: t.tag,
 		HTMLURL: "https://example.org",
 	}, nil
 }