upgrade_v1.go

  1package cli
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"net/http"
  7	"os"
  8	"os/exec"
  9	"regexp"
 10	"runtime"
 11	"strings"
 12
 13	"github.com/floatpane/matcha/internal/httpclient"
 14)
 15
 16var v1RCRegex = regexp.MustCompile(`^v?1\.0\.0-rc\d+$`)
 17
 18// RunUpgradeV1 handles `matcha upgrade-v1` and upgrades a pre-v1 install (or
 19// release candidate) to the v1 release.
 20func RunUpgradeV1(args []string) error {
 21	_ = args
 22
 23	fmt.Println("Checking for v1 release...")
 24
 25	rel, tag, err := fetchV1Release()
 26	if err != nil {
 27		return err
 28	}
 29
 30	fmt.Printf("Target version: v%s\n", tag)
 31
 32	switch runtime.GOOS {
 33	case goosDarwin:
 34		if tryHomebrewV1Upgrade(true) {
 35			return nil
 36		}
 37	case goosLinux:
 38		if trySnapV1Refresh() {
 39			return nil
 40		}
 41		if tryHomebrewV1Upgrade(false) {
 42			return nil
 43		}
 44	}
 45
 46	// Windows or fallbacks: download the binary directly.
 47	osName := runtime.GOOS
 48	arch := runtime.GOARCH
 49	assetName, assetURL, err := FindAsset(rel, osName, arch)
 50	if err != nil {
 51		return err
 52	}
 53	return UpgradeBinaryFromAsset(assetURL, assetName, "v"+tag, "matcha upgrade-v1")
 54}
 55
 56func fetchV1Release() (*Release, string, error) {
 57	client := httpclient.NewWithRedirectCap(httpclient.UpdateCheckTimeout, 5)
 58
 59	const apiTag = "https://api.github.com/repos/floatpane/matcha/releases/tags/v1.0.0"
 60	resp, err := client.Get(apiTag) //nolint:noctx
 61	if err == nil {
 62		if resp.StatusCode == http.StatusOK {
 63			defer resp.Body.Close() //nolint:errcheck
 64			var rel Release
 65			if err := json.NewDecoder(resp.Body).Decode(&rel); err == nil && !rel.Prerelease {
 66				tag := strings.TrimPrefix(rel.TagName, "v")
 67				return &rel, tag, nil
 68			}
 69		} else {
 70			if err := resp.Body.Close(); err != nil {
 71				fmt.Printf("warning: non-fatal response body close error: %v\n", err)
 72			}
 73		}
 74	}
 75
 76	const apiList = "https://api.github.com/repos/floatpane/matcha/releases"
 77	resp, err = client.Get(apiList) //nolint:noctx
 78	if err != nil {
 79		return nil, "", fmt.Errorf("could not query releases: %w", err)
 80	}
 81	defer resp.Body.Close() //nolint:errcheck
 82
 83	var rels []Release
 84	if err := json.NewDecoder(resp.Body).Decode(&rels); err != nil {
 85		return nil, "", fmt.Errorf("could not parse releases: %w", err)
 86	}
 87
 88	for i := range rels {
 89		tag := strings.TrimPrefix(rels[i].TagName, "v")
 90		if strings.HasPrefix(tag, "1.0.") && !strings.Contains(tag, "-") && !rels[i].Prerelease {
 91			return &rels[i], tag, nil
 92		}
 93	}
 94	for i := range rels {
 95		tag := strings.TrimPrefix(rels[i].TagName, "v")
 96		if v1RCRegex.MatchString(tag) {
 97			return &rels[i], tag, nil
 98		}
 99	}
100	return nil, "", fmt.Errorf("no v1 release found")
101}
102
103func tryHomebrewV1Upgrade(cask bool) bool {
104	if _, err := exec.LookPath("brew"); err != nil {
105		return false
106	}
107
108	formula := "floatpane/matcha/matcha@v1"
109	var installArgs, upgradeArgs []string
110	if cask {
111		installArgs = []string{"install", "--cask", formula}
112		upgradeArgs = []string{"upgrade", "--cask", formula}
113		fmt.Println("Attempting to upgrade via Homebrew cask to v1.")
114	} else {
115		installArgs = []string{"install", formula}
116		upgradeArgs = []string{"upgrade", formula}
117		fmt.Println("Attempting to upgrade via Homebrew to v1.")
118	}
119
120	cmd := exec.Command("brew", installArgs...) //nolint:noctx
121	cmd.Stdout = os.Stdout
122	cmd.Stderr = os.Stderr
123	if err := cmd.Run(); err == nil {
124		fmt.Println("Successfully upgraded via Homebrew.")
125		return true
126	}
127
128	cmd = exec.Command("brew", upgradeArgs...) //nolint:noctx
129	cmd.Stdout = os.Stdout
130	cmd.Stderr = os.Stderr
131	if err := cmd.Run(); err == nil {
132		fmt.Println("Successfully upgraded via Homebrew.")
133		return true
134	}
135
136	fmt.Println("Homebrew v1 upgrade failed.")
137	return false
138}
139
140func trySnapV1Refresh() bool {
141	if _, err := exec.LookPath("snap"); err != nil {
142		return false
143	}
144
145	cmdCheck := exec.Command("snap", "list", "matcha") //nolint:noctx
146	if err := cmdCheck.Run(); err != nil {
147		return false
148	}
149
150	fmt.Println("Detected Snap package — attempting to refresh to candidate v1.")
151	cmd := exec.Command("snap", "refresh", "matcha", "--candidate") //nolint:noctx
152	cmd.Stdout = os.Stdout
153	cmd.Stderr = os.Stderr
154	if err := cmd.Run(); err == nil {
155		fmt.Println("Successfully refreshed snap to candidate v1.")
156		return true
157	}
158
159	fmt.Println("Snap candidate refresh failed.")
160	return false
161}