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/charmbracelet/crush/internal/version"
13)
14
15const (
16	githubAPIURL = "https://api.github.com/repos/charmbracelet/crush/releases/latest"
17	userAgent    = "crush/1.0"
18)
19
20// Info contains information about an available update.
21type Info struct {
22	CurrentVersion string
23	LatestVersion  string
24	ReleaseURL     string
25}
26
27func (i Info) Available() bool { return i.CurrentVersion != i.LatestVersion }
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		LatestVersion:  version.Version,
34	}
35
36	if info.CurrentVersion == "devel" || info.CurrentVersion == "unknown" {
37		return info, nil
38	}
39
40	release, err := fetchLatestRelease(ctx)
41	if err != nil {
42		return info, fmt.Errorf("failed to fetch latest release: %w", err)
43	}
44
45	info.LatestVersion = strings.TrimPrefix(release.TagName, "v")
46	info.ReleaseURL = release.HTMLURL
47	return info, nil
48}
49
50// githubRelease represents a GitHub release.
51type githubRelease struct {
52	TagName string `json:"tag_name"`
53	HTMLURL string `json:"html_url"`
54}
55
56// fetchLatestRelease fetches the latest release information from GitHub.
57func fetchLatestRelease(ctx context.Context) (*githubRelease, error) {
58	client := &http.Client{
59		Timeout: 30 * time.Second,
60	}
61
62	req, err := http.NewRequestWithContext(ctx, "GET", githubAPIURL, nil)
63	if err != nil {
64		return nil, err
65	}
66	req.Header.Set("User-Agent", userAgent)
67	req.Header.Set("Accept", "application/vnd.github.v3+json")
68
69	resp, err := client.Do(req)
70	if err != nil {
71		return nil, err
72	}
73	defer resp.Body.Close()
74
75	if resp.StatusCode != http.StatusOK {
76		body, _ := io.ReadAll(resp.Body)
77		return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
78	}
79
80	var release githubRelease
81	if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
82		return nil, err
83	}
84
85	return &release, nil
86}