Skip to content

Commit 61e13ba

Browse files
authored
Merge pull request #18 from wherobots/fix/resolve-latest-release-tag
fix: resolve latest to actual release tag before downloading
2 parents a0d738d + 5c8a14e commit 61e13ba

3 files changed

Lines changed: 118 additions & 52 deletions

File tree

internal/commands/upgrade.go

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package commands
33
import (
44
"crypto/sha256"
55
"encoding/hex"
6+
"encoding/json"
67
"fmt"
78
"io"
9+
"net/http"
810
"os"
911
"os/exec"
1012
"path/filepath"
@@ -66,11 +68,6 @@ func runUpgrade(cmd *cobra.Command, opts *upgradeOptions) error {
6668
installDir = filepath.Dir(exe)
6769
}
6870

69-
// Ensure gh is available and authenticated.
70-
if err := requireGh(); err != nil {
71-
return err
72-
}
73-
7471
osName, archName, err := detectPlatform()
7572
if err != nil {
7673
return err
@@ -79,6 +76,15 @@ func runUpgrade(cmd *cobra.Command, opts *upgradeOptions) error {
7976
asset := fmt.Sprintf("%s_%s_%s", upgradeBinary, osName, archName)
8077
tag := opts.tag
8178

79+
// "latest" is not a real tag — resolve it to the actual latest release tag.
80+
if tag == "latest" {
81+
resolved, err := resolveLatestTag()
82+
if err != nil {
83+
return fmt.Errorf("resolve latest release: %w", err)
84+
}
85+
tag = resolved
86+
}
87+
8288
fmt.Fprintf(w, "Downloading %s from %s@%s...\n", asset, upgradeRepo, tag)
8389

8490
tmpDir, err := os.MkdirTemp("", "wherobots-upgrade-*")
@@ -87,15 +93,15 @@ func runUpgrade(cmd *cobra.Command, opts *upgradeOptions) error {
8793
}
8894
defer os.RemoveAll(tmpDir)
8995

90-
if err := ghDownload(tag, asset, tmpDir); err != nil {
96+
if err := httpDownload(tag, asset, tmpDir); err != nil {
9197
return fmt.Errorf("download asset: %w", err)
9298
}
9399

94100
assetPath := filepath.Join(tmpDir, asset)
95101

96102
if !opts.skipChecksum {
97103
fmt.Fprintln(w, "Verifying checksum...")
98-
if err := ghDownload(tag, "checksums.txt", tmpDir); err != nil {
104+
if err := httpDownload(tag, "checksums.txt", tmpDir); err != nil {
99105
return fmt.Errorf("download checksums: %w", err)
100106
}
101107
if err := verifyChecksum(assetPath, filepath.Join(tmpDir, "checksums.txt"), asset); err != nil {
@@ -113,17 +119,6 @@ func runUpgrade(cmd *cobra.Command, opts *upgradeOptions) error {
113119
return nil
114120
}
115121

116-
// requireGh checks that the gh CLI is installed and authenticated.
117-
func requireGh() error {
118-
if _, err := exec.LookPath("gh"); err != nil {
119-
return fmt.Errorf("gh CLI is required; install from https://cli.github.com/")
120-
}
121-
if err := exec.Command("gh", "auth", "status").Run(); err != nil {
122-
return fmt.Errorf("gh is not authenticated; run: gh auth login")
123-
}
124-
return nil
125-
}
126-
127122
func detectPlatform() (string, string, error) {
128123
var osName string
129124
switch runtime.GOOS {
@@ -150,15 +145,66 @@ func detectPlatform() (string, string, error) {
150145
return osName, archName, nil
151146
}
152147

153-
func ghDownload(tag, pattern, dir string) error {
154-
out, err := exec.Command("gh", "release", "download", tag,
155-
"--repo", upgradeRepo,
156-
"--pattern", pattern,
157-
"--dir", dir,
158-
"--clobber",
159-
).CombinedOutput()
148+
// resolveLatestTag queries the GitHub API for the latest release tag.
149+
func resolveLatestTag() (string, error) {
150+
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", upgradeRepo)
151+
req, err := http.NewRequest(http.MethodGet, url, nil)
152+
if err != nil {
153+
return "", err
154+
}
155+
req.Header.Set("User-Agent", "wherobots-cli")
156+
157+
resp, err := http.DefaultClient.Do(req)
160158
if err != nil {
161-
return fmt.Errorf("%s: %s", err, strings.TrimSpace(string(out)))
159+
return "", fmt.Errorf("GitHub API request failed: %w", err)
160+
}
161+
defer resp.Body.Close()
162+
163+
if resp.StatusCode != http.StatusOK {
164+
return "", fmt.Errorf("no release found in %s (HTTP %d)", upgradeRepo, resp.StatusCode)
165+
}
166+
167+
var release struct {
168+
TagName string `json:"tag_name"`
169+
}
170+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
171+
return "", fmt.Errorf("failed to parse GitHub API response: %w", err)
172+
}
173+
tag := strings.TrimSpace(release.TagName)
174+
if tag == "" {
175+
return "", fmt.Errorf("no release found in %s", upgradeRepo)
176+
}
177+
return tag, nil
178+
}
179+
180+
// httpDownload downloads a release asset from GitHub to the given directory.
181+
func httpDownload(tag, filename, dir string) error {
182+
url := fmt.Sprintf("https://github.com/%s/releases/download/%s/%s", upgradeRepo, tag, filename)
183+
req, err := http.NewRequest(http.MethodGet, url, nil)
184+
if err != nil {
185+
return err
186+
}
187+
req.Header.Set("User-Agent", "wherobots-cli")
188+
189+
resp, err := http.DefaultClient.Do(req)
190+
if err != nil {
191+
return fmt.Errorf("download request failed: %w", err)
192+
}
193+
defer resp.Body.Close()
194+
195+
if resp.StatusCode != http.StatusOK {
196+
return fmt.Errorf("failed to download %s (HTTP %d)", filename, resp.StatusCode)
197+
}
198+
199+
outPath := filepath.Join(dir, filename)
200+
f, err := os.Create(outPath)
201+
if err != nil {
202+
return fmt.Errorf("create file %s: %w", outPath, err)
203+
}
204+
defer f.Close()
205+
206+
if _, err := io.Copy(f, resp.Body); err != nil {
207+
return fmt.Errorf("write file %s: %w", outPath, err)
162208
}
163209
return nil
164210
}

internal/version/check.go

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ package version
33

44
import (
55
"context"
6+
"encoding/json"
67
"fmt"
7-
"os/exec"
8+
"net/http"
89
"strconv"
910
"strings"
1011
"time"
@@ -23,7 +24,7 @@ type Result struct {
2324
Outdated bool // true when current < latest
2425
}
2526

26-
// CheckInBackground spawns a goroutine that queries the GitHub CLI for the
27+
// CheckInBackground spawns a goroutine that queries the GitHub API for the
2728
// latest release tag. Call Collect on the returned channel after the main
2829
// command has finished to retrieve the result (if any).
2930
//
@@ -46,7 +47,7 @@ func CheckInBackground(ctx context.Context, currentVersion string) <-chan *Resul
4647

4748
latest, err := fetchLatestTag(checkCtx)
4849
if err != nil || latest == "" {
49-
return // silently skip; don't annoy users when gh is unavailable
50+
return // silently skip; don't annoy users when the check fails
5051
}
5152

5253
if !isNewer(currentVersion, latest) {
@@ -91,18 +92,32 @@ func isDevVersion(v string) bool {
9192
return v == "" || v == "dev" || v == "latest-prerelease" || strings.HasPrefix(v, "dev-")
9293
}
9394

94-
// fetchLatestTag shells out to: gh release view --repo wherobots/wbc-cli --json tagName -q .tagName
95+
// fetchLatestTag queries the GitHub API for the latest release tag.
9596
func fetchLatestTag(ctx context.Context) (string, error) {
96-
cmd := exec.CommandContext(ctx, "gh", "release", "view",
97-
"--repo", repo,
98-
"--json", "tagName",
99-
"-q", ".tagName",
100-
)
101-
out, err := cmd.Output()
97+
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", repo)
98+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
99+
if err != nil {
100+
return "", err
101+
}
102+
req.Header.Set("User-Agent", "wherobots-cli")
103+
104+
resp, err := http.DefaultClient.Do(req)
102105
if err != nil {
103106
return "", err
104107
}
105-
return strings.TrimSpace(string(out)), nil
108+
defer resp.Body.Close()
109+
110+
if resp.StatusCode != http.StatusOK {
111+
return "", fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
112+
}
113+
114+
var release struct {
115+
TagName string `json:"tag_name"`
116+
}
117+
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
118+
return "", err
119+
}
120+
return strings.TrimSpace(release.TagName), nil
106121
}
107122

108123
// isNewer returns true when latest represents a strictly newer semver than current.

scripts/install-release.sh

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ usage() {
1212
Install wherobots CLI from a GitHub release.
1313
1414
Requirements:
15-
- gh CLI installed and authenticated with access to the repository.
15+
- curl
1616
1717
Usage:
1818
./scripts/install-release.sh [options]
@@ -67,8 +67,8 @@ while (($# > 0)); do
6767
esac
6868
done
6969

70-
if ! command -v gh >/dev/null 2>&1; then
71-
echo "gh CLI is required. Install from https://cli.github.com/" >&2
70+
if ! command -v curl >/dev/null 2>&1; then
71+
echo "curl is required." >&2
7272
exit 1
7373
fi
7474

@@ -77,16 +77,6 @@ if ! command -v install >/dev/null 2>&1; then
7777
exit 1
7878
fi
7979

80-
if ! gh auth status >/dev/null 2>&1; then
81-
echo "gh is not authenticated. Run: gh auth login" >&2
82-
exit 1
83-
fi
84-
85-
if ! gh repo view "$REPO" >/dev/null 2>&1; then
86-
echo "Unable to access repository $REPO with current gh credentials." >&2
87-
exit 1
88-
fi
89-
9080
case "$(uname -s)" in
9181
Linux) OS="linux" ;;
9282
Darwin) OS="darwin" ;;
@@ -110,12 +100,27 @@ TMP_DIR="$(mktemp -d)"
110100
cleanup() { rm -rf "$TMP_DIR"; }
111101
trap cleanup EXIT
112102

103+
# "latest" is not a real tag — resolve it to the actual latest release tag.
104+
if [[ "$TAG" == "latest" ]]; then
105+
API_RESPONSE="$(curl -fsSL -H "User-Agent: wherobots-cli" \
106+
"https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null)" || true
107+
# Parse tag_name from JSON without requiring jq — grep for the field and
108+
# strip surrounding quotes/whitespace with sed.
109+
TAG="$(printf '%s' "$API_RESPONSE" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')" || true
110+
if [[ -z "$TAG" ]]; then
111+
echo "No release found in $REPO" >&2
112+
exit 1
113+
fi
114+
fi
115+
116+
DOWNLOAD_BASE="https://github.com/${REPO}/releases/download/${TAG}"
117+
113118
echo "Downloading ${ASSET} from ${REPO}@${TAG}..."
114-
gh release download "$TAG" --repo "$REPO" --pattern "$ASSET" --dir "$TMP_DIR" --clobber
119+
curl -fsSL -o "$TMP_DIR/$ASSET" "${DOWNLOAD_BASE}/${ASSET}"
115120

116121
if [[ "$SKIP_CHECKSUM" -eq 0 ]]; then
117122
echo "Verifying checksum..."
118-
gh release download "$TAG" --repo "$REPO" --pattern "checksums.txt" --dir "$TMP_DIR" --clobber
123+
curl -fsSL -o "$TMP_DIR/checksums.txt" "${DOWNLOAD_BASE}/checksums.txt"
119124
EXPECTED="$(awk -v file="$ASSET" '$2 == file { print $1 }' "$TMP_DIR/checksums.txt" | head -n1)"
120125
if [[ -z "$EXPECTED" ]]; then
121126
echo "Could not find checksum entry for $ASSET" >&2

0 commit comments

Comments
 (0)