@@ -20,7 +20,6 @@ import (
2020 "context"
2121 "crypto/sha256"
2222 "encoding/hex"
23- "encoding/json"
2423 "errors"
2524 "fmt"
2625 "io"
@@ -34,49 +33,65 @@ import (
3433)
3534
3635const (
37- repo = "kyc-rip/cli"
38- releaseAPI = "https://api.github.com/repos/" + repo + "/releases/latest"
39- checkCacheFile = "kyc-cli/lastcheck"
40- checkInterval = 24 * time .Hour
36+ repo = "kyc-rip/cli"
37+ // releaseLatestURL is the HTML endpoint that 302-redirects to the
38+ // latest non-draft, non-prerelease tag. We use it instead of
39+ // api.github.com/.../releases/latest because the API endpoint is
40+ // rate-limited to 60 unauthenticated requests per IP per hour —
41+ // users behind shared NATs (Cloudflare WARP, mobile carriers,
42+ // office networks) blow through that quickly and the auto-update
43+ // nudge starts failing visibly.
44+ releaseLatestURL = "https://github.com/" + repo + "/releases/latest"
45+ checkCacheFile = "kyc-cli/lastcheck"
46+ checkInterval = 24 * time .Hour
4147)
4248
43- // LatestRelease is the slice of the GitHub releases API we use.
44- type LatestRelease struct {
45- TagName string `json:"tag_name"`
46- HTMLURL string `json:"html_url"`
47- Prerelease bool `json:"prerelease"`
48- Draft bool `json:"draft"`
49- }
50-
51- // CheckLatest queries GitHub for the most recent (non-draft, non-pre)
52- // release. Returns ("", nil) if there's nothing newer than `current`.
53- // Returns the tag (e.g. "v0.1.6") if an upgrade is available.
49+ // CheckLatest resolves the latest non-draft, non-prerelease release tag
50+ // via the HTML redirect endpoint (no API rate limit). Returns ("", nil)
51+ // if `current` is already up to date.
5452func CheckLatest (ctx context.Context , current string ) (string , error ) {
55- req , err := http .NewRequestWithContext (ctx , http .MethodGet , releaseAPI , nil )
53+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , releaseLatestURL , nil )
5654 if err != nil {
5755 return "" , err
5856 }
59- req .Header .Set ("Accept" , "application/vnd.github+json" )
6057 req .Header .Set ("User-Agent" , "kyc-cli-self-update" )
61- resp , err := (& http.Client {Timeout : 4 * time .Second }).Do (req )
58+ // Disable automatic redirect following — we want the Location header
59+ // from the first 302 so we can read the tag without downloading the
60+ // full HTML release page (small, but pointless).
61+ client := & http.Client {
62+ Timeout : 4 * time .Second ,
63+ CheckRedirect : func (* http.Request , []* http.Request ) error {
64+ return http .ErrUseLastResponse
65+ },
66+ }
67+ resp , err := client .Do (req )
6268 if err != nil {
6369 return "" , err
6470 }
6571 defer resp .Body .Close ()
66- if resp .StatusCode != 200 {
72+ if resp .StatusCode != http . StatusFound && resp . StatusCode != http . StatusMovedPermanently {
6773 return "" , fmt .Errorf ("github releases: %s" , resp .Status )
6874 }
69- var rel LatestRelease
70- if err := json .NewDecoder (resp .Body ).Decode (& rel ); err != nil {
71- return "" , err
75+ loc := resp .Header .Get ("Location" )
76+ if loc == "" {
77+ return "" , errors .New ("github releases: missing Location header" )
78+ }
79+ // Location looks like ".../releases/tag/v0.1.34" — pull the trailing
80+ // path segment as the tag. If GitHub ever returns the bare
81+ // "/releases" path (no releases yet), `tag` will end up "releases"
82+ // and semverNewer will reject it.
83+ idx := strings .LastIndex (loc , "/" )
84+ if idx < 0 || idx == len (loc )- 1 {
85+ return "" , fmt .Errorf ("github releases: bad redirect %q" , loc )
7286 }
73- if rel .Draft || rel .Prerelease || rel .TagName == "" {
87+ tag := loc [idx + 1 :]
88+ if ! strings .HasPrefix (tag , "v" ) {
7489 return "" , nil
7590 }
76- if ! semverNewer (rel . TagName , current ) {
91+ if ! semverNewer (tag , current ) {
7792 return "" , nil
7893 }
79- return rel . TagName , nil
94+ return tag , nil
8095}
8196
8297// CheckLatestThrottled is CheckLatest with a 24h-on-disk cache so we
0 commit comments