@@ -5,26 +5,30 @@ import (
55 "encoding/json"
66 "fmt"
77 "net/http"
8+ "os"
89 "runtime"
9- "sync"
1010 "time"
1111
1212 "github.com/pkg/errors"
1313 "golang.org/x/mod/semver"
1414
15+ "github.com/zeropsio/zcli/src/constants"
1516 "github.com/zeropsio/zcli/src/httpClient"
1617 "github.com/zeropsio/zcli/src/printer"
1718)
1819
19- const apiUrl = "https://api.app-prg1.zerops.io/api/rest/public/zcli/version"
20+ const defaultApiUrl = "https://api.app-prg1.zerops.io/api/rest/public/zcli/version"
2021
21- var version = "local"
22+ // apiURL returns the version API endpoint, honoring an env override so tests
23+ // (and mirrors) can point it elsewhere without rebuilding.
24+ func apiURL () string {
25+ if u := os .Getenv (constants .VersionApiUrlEnvVar ); u != "" {
26+ return u
27+ }
28+ return defaultApiUrl
29+ }
2230
23- var (
24- fetchOnce sync.Once
25- latestResponse * apiResponse
26- errFetch error
27- )
31+ var version = "local"
2832
2933func GetCurrent () string {
3034 return version
@@ -125,36 +129,35 @@ func isUpdateAvailable(current, latest string) bool {
125129 return semver .Compare (current , latest ) < 0
126130}
127131
132+ // fetch returns the latest-release response, preferring the on-disk cache.
133+ // The cache also dedupes within a single invocation: the first call writes it
134+ // and any later call (e.g. GetLatestUrl after GetLatest) reads it back, so
135+ // there's no need for in-process memoization.
128136func fetch (ctx context.Context ) (* apiResponse , error ) {
129- fetchOnce .Do (func () {
130- if entry , err := loadCacheEntry (); err == nil && entry != nil && entry .Fresh () {
131- latestResponse = entry .Response
132- return
133- }
134- resp , err := fetchFromNetwork (ctx )
135- if err != nil {
136- // Stale cache beats no answer at all.
137- if entry , cacheErr := loadCacheEntry (); cacheErr == nil && entry != nil {
138- latestResponse = entry .Response
139- return
140- }
141- errFetch = err
142- return
137+ if entry , err := loadCacheEntry (); err == nil && entry != nil && entry .Fresh () {
138+ return entry .Response , nil
139+ }
140+ resp , err := fetchFromNetwork (ctx )
141+ if err != nil {
142+ // Stale cache beats no answer at all.
143+ if entry , cacheErr := loadCacheEntry (); cacheErr == nil && entry != nil {
144+ return entry .Response , nil
143145 }
144- latestResponse = resp
145- _ = writeCacheEntry ( resp )
146- } )
147- return latestResponse , errFetch
146+ return nil , err
147+ }
148+ _ = writeCacheEntry ( resp )
149+ return resp , nil
148150}
149151
150152func fetchFromNetwork (ctx context.Context ) (* apiResponse , error ) {
153+ url := apiURL ()
151154 client := httpClient .New (ctx , httpClient.Config {HttpTimeout : time .Second * 5 })
152- resp , err := client .Get (ctx , apiUrl )
155+ resp , err := client .Get (ctx , url )
153156 if err != nil {
154- return nil , errors .Wrapf (err , "version api request to %s failed" , apiUrl )
157+ return nil , errors .Wrapf (err , "version api request to %s failed" , url )
155158 }
156159 if resp .StatusCode != http .StatusOK {
157- return nil , errors .Errorf ("version api %s returned status %d" , apiUrl , resp .StatusCode )
160+ return nil , errors .Errorf ("version api %s returned status %d" , url , resp .StatusCode )
158161 }
159162 out := & apiResponse {}
160163 if err := json .Unmarshal (resp .Body , out ); err != nil {
0 commit comments