update.go

 1package update
 2
 3import (
 4	"context"
 5	"encoding/json"
 6	"fmt"
 7	"io"
 8	"net/http"
 9	"strings"
10	"time"
11
12	"github.com/Masterminds/semver/v3"
13	"github.com/charmbracelet/crush/internal/version"
14)
15
16const (
17	githubAPIURL = "https://api.github.com/repos/charmbracelet/crush/releases/latest"
18	userAgent    = "crush/1.0"
19)
20
21// Info contains information about an available update.
22type Info struct {
23	CurrentVersion string
24	LatestVersion  string
25	ReleaseURL     string
26	Available      bool
27}
28
29// Check checks if a new version is available.
30func Check(ctx context.Context) (*Info, error) {
31	info := &Info{
32		CurrentVersion: version.Version,
33	}
34
35	cv, err := semver.NewVersion(version.Version)
36	if err != nil {
37		// its devel, unknown, etc
38		return info, nil
39	}
40
41	release, err := fetchLatestRelease(ctx)
42	if err != nil {
43		return nil, fmt.Errorf("failed to fetch latest release: %w", err)
44	}
45
46	lv, err := semver.NewVersion(release.TagName)
47	if err != nil {
48		return nil, fmt.Errorf("failed to parse latest version: %w", err)
49	}
50
51	info.LatestVersion = strings.TrimPrefix(release.TagName, "v")
52	info.ReleaseURL = release.HTMLURL
53	info.Available = lv.GreaterThan(cv)
54
55	return info, nil
56}
57
58// githubRelease represents a GitHub release.
59type githubRelease struct {
60	TagName string `json:"tag_name"`
61	HTMLURL string `json:"html_url"`
62}
63
64// fetchLatestRelease fetches the latest release information from GitHub.
65func fetchLatestRelease(ctx context.Context) (*githubRelease, error) {
66	client := &http.Client{
67		Timeout: 30 * time.Second,
68	}
69
70	req, err := http.NewRequestWithContext(ctx, "GET", githubAPIURL, nil)
71	if err != nil {
72		return nil, err
73	}
74	req.Header.Set("User-Agent", userAgent)
75	req.Header.Set("Accept", "application/vnd.github.v3+json")
76
77	resp, err := client.Do(req)
78	if err != nil {
79		return nil, err
80	}
81	defer resp.Body.Close()
82
83	if resp.StatusCode != http.StatusOK {
84		body, _ := io.ReadAll(resp.Body)
85		return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
86	}
87
88	var release githubRelease
89	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
90		return nil, err
91	}
92
93	return &release, nil
94}