Skip to content

Commit 5c8a14e

Browse files
committed
fix: replace gh CLI dependency with direct GitHub API and HTTP calls
The install script, upgrade command, and version check all previously shelled out to the gh CLI. Since the repo is public, no authentication is needed. This replaces every gh invocation with: - net/http calls to the GitHub releases API (for resolving latest tag) - Direct HTTP downloads from GitHub release asset URLs - curl in the install script (with grep/sed to parse JSON, no jq needed)
1 parent f69d639 commit 5c8a14e

3 files changed

Lines changed: 92 additions & 61 deletions

File tree

internal/commands/upgrade.go

Lines changed: 54 additions & 34 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
@@ -96,15 +93,15 @@ func runUpgrade(cmd *cobra.Command, opts *upgradeOptions) error {
9693
}
9794
defer os.RemoveAll(tmpDir)
9895

99-
if err := ghDownload(tag, asset, tmpDir); err != nil {
96+
if err := httpDownload(tag, asset, tmpDir); err != nil {
10097
return fmt.Errorf("download asset: %w", err)
10198
}
10299

103100
assetPath := filepath.Join(tmpDir, asset)
104101

105102
if !opts.skipChecksum {
106103
fmt.Fprintln(w, "Verifying checksum...")
107-
if err := ghDownload(tag, "checksums.txt", tmpDir); err != nil {
104+
if err := httpDownload(tag, "checksums.txt", tmpDir); err != nil {
108105
return fmt.Errorf("download checksums: %w", err)
109106
}
110107
if err := verifyChecksum(assetPath, filepath.Join(tmpDir, "checksums.txt"), asset); err != nil {
@@ -122,17 +119,6 @@ func runUpgrade(cmd *cobra.Command, opts *upgradeOptions) error {
122119
return nil
123120
}
124121

125-
// requireGh checks that the gh CLI is installed and authenticated.
126-
func requireGh() error {
127-
if _, err := exec.LookPath("gh"); err != nil {
128-
return fmt.Errorf("gh CLI is required; install from https://cli.github.com/")
129-
}
130-
if err := exec.Command("gh", "auth", "status").Run(); err != nil {
131-
return fmt.Errorf("gh is not authenticated; run: gh auth login")
132-
}
133-
return nil
134-
}
135-
136122
func detectPlatform() (string, string, error) {
137123
var osName string
138124
switch runtime.GOOS {
@@ -159,32 +145,66 @@ func detectPlatform() (string, string, error) {
159145
return osName, archName, nil
160146
}
161147

162-
// resolveLatestTag queries gh for the latest non-prerelease release tag.
148+
// resolveLatestTag queries the GitHub API for the latest release tag.
163149
func resolveLatestTag() (string, error) {
164-
out, err := exec.Command("gh", "release", "view",
165-
"--repo", upgradeRepo,
166-
"--json", "tagName",
167-
"-q", ".tagName",
168-
).Output()
150+
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", upgradeRepo)
151+
req, err := http.NewRequest(http.MethodGet, url, nil)
169152
if err != nil {
170-
return "", fmt.Errorf("no release found in %s", upgradeRepo)
153+
return "", err
154+
}
155+
req.Header.Set("User-Agent", "wherobots-cli")
156+
157+
resp, err := http.DefaultClient.Do(req)
158+
if err != nil {
159+
return "", fmt.Errorf("GitHub API request failed: %w", err)
171160
}
172-
tag := strings.TrimSpace(string(out))
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)
173174
if tag == "" {
174175
return "", fmt.Errorf("no release found in %s", upgradeRepo)
175176
}
176177
return tag, nil
177178
}
178179

179-
func ghDownload(tag, pattern, dir string) error {
180-
out, err := exec.Command("gh", "release", "download", tag,
181-
"--repo", upgradeRepo,
182-
"--pattern", pattern,
183-
"--dir", dir,
184-
"--clobber",
185-
).CombinedOutput()
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)
186201
if err != nil {
187-
return fmt.Errorf("%s: %s", err, strings.TrimSpace(string(out)))
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)
188208
}
189209
return nil
190210
}

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: 12 additions & 16 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" ;;
@@ -112,19 +102,25 @@ trap cleanup EXIT
112102

113103
# "latest" is not a real tag — resolve it to the actual latest release tag.
114104
if [[ "$TAG" == "latest" ]]; then
115-
TAG="$(gh release view --repo "$REPO" --json tagName -q .tagName 2>/dev/null)" || true
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
116110
if [[ -z "$TAG" ]]; then
117111
echo "No release found in $REPO" >&2
118112
exit 1
119113
fi
120114
fi
121115

116+
DOWNLOAD_BASE="https://github.com/${REPO}/releases/download/${TAG}"
117+
122118
echo "Downloading ${ASSET} from ${REPO}@${TAG}..."
123-
gh release download "$TAG" --repo "$REPO" --pattern "$ASSET" --dir "$TMP_DIR" --clobber
119+
curl -fsSL -o "$TMP_DIR/$ASSET" "${DOWNLOAD_BASE}/${ASSET}"
124120

125121
if [[ "$SKIP_CHECKSUM" -eq 0 ]]; then
126122
echo "Verifying checksum..."
127-
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"
128124
EXPECTED="$(awk -v file="$ASSET" '$2 == file { print $1 }' "$TMP_DIR/checksums.txt" | head -n1)"
129125
if [[ -z "$EXPECTED" ]]; then
130126
echo "Could not find checksum entry for $ASSET" >&2

0 commit comments

Comments
 (0)