update.go

  1package update
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"fmt"
  7	"io"
  8	"net/http"
  9	"os"
 10	"strings"
 11	"time"
 12
 13	"github.com/Masterminds/semver/v3"
 14	"github.com/charmbracelet/crush/internal/version"
 15)
 16
 17const (
 18	githubAPIURL = "https://api.github.com/repos/charmbracelet/crush/releases/latest"
 19	userAgent    = "crush/1.0"
 20)
 21
 22// Release represents a GitHub release.
 23type Release struct {
 24	TagName string `json:"tag_name"`
 25	HTMLURL string `json:"html_url"`
 26}
 27
 28// UpdateInfo contains information about an available update.
 29type UpdateInfo struct {
 30	CurrentVersion string
 31	LatestVersion  string
 32	ReleaseURL     string
 33	Available      bool
 34}
 35
 36// CheckForUpdate checks if a new version is available.
 37func CheckForUpdate(ctx context.Context) (*UpdateInfo, error) {
 38	info := &UpdateInfo{
 39		CurrentVersion: version.Version,
 40	}
 41
 42	cv, err := semver.NewVersion(version.Version)
 43	if err != nil {
 44		// its devel, unknown, etc
 45		return info, nil
 46	}
 47
 48	release, err := fetchLatestRelease(ctx)
 49	if err != nil {
 50		return nil, fmt.Errorf("failed to fetch latest release: %w", err)
 51	}
 52
 53	lv, err := semver.NewVersion(release.TagName)
 54	if err != nil {
 55		return nil, fmt.Errorf("failed to parse latest version: %w", err)
 56	}
 57
 58	info.LatestVersion = strings.TrimPrefix(release.TagName, "v")
 59	info.ReleaseURL = release.HTMLURL
 60	info.Available = lv.GreaterThan(cv)
 61
 62	return info, nil
 63}
 64
 65// fetchLatestRelease fetches the latest release information from GitHub.
 66func fetchLatestRelease(ctx context.Context) (*Release, error) {
 67	client := &http.Client{
 68		Timeout: 30 * time.Second,
 69	}
 70
 71	req, err := http.NewRequestWithContext(ctx, "GET", githubAPIURL, nil)
 72	if err != nil {
 73		return nil, err
 74	}
 75	req.Header.Set("User-Agent", userAgent)
 76	req.Header.Set("Accept", "application/vnd.github.v3+json")
 77
 78	resp, err := client.Do(req)
 79	if err != nil {
 80		return nil, err
 81	}
 82	defer resp.Body.Close()
 83
 84	if resp.StatusCode != http.StatusOK {
 85		body, _ := io.ReadAll(resp.Body)
 86		return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
 87	}
 88
 89	var release Release
 90	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
 91		return nil, err
 92	}
 93
 94	return &release, nil
 95}
 96
 97// CheckForUpdateAsync performs an update check in the background and returns immediately.
 98// If an update is available, it returns the update info through the channel.
 99func CheckForUpdateAsync(ctx context.Context, dataDir string) <-chan *UpdateInfo {
100	ch := make(chan *UpdateInfo, 1)
101
102	go func() {
103		defer close(ch)
104
105		// Perform the check.
106		info, err := CheckForUpdate(ctx)
107		if err != nil {
108			// Log error but don't fail.
109			fmt.Fprintf(os.Stderr, "Failed to check for updates: %v\n", err)
110			return
111		}
112
113		// Send update info if available.
114		if info.Available {
115			ch <- info
116		}
117	}()
118
119	return ch
120}