in cli/azd/main.go [172:303]
func fetchLatestVersion(version chan<- semver.Version) {
defer close(version)
// Allow the user to skip the update check if they wish, by setting AZD_SKIP_UPDATE_CHECK to
// a truthy value.
if value, has := os.LookupEnv("AZD_SKIP_UPDATE_CHECK"); has {
if setting, err := strconv.ParseBool(value); err == nil && setting {
log.Print("skipping update check since AZD_SKIP_UPDATE_CHECK is true")
return
} else if err != nil {
log.Printf("could not parse value for AZD_SKIP_UPDATE_CHECK a boolean "+
"(it was: %s), proceeding with update check", value)
}
}
// To avoid fetching the latest version of the CLI on every invocation, we cache the result for a period
// of time, in the user's home directory.
configDir, err := config.GetUserConfigDir()
if err != nil {
log.Printf("could not determine config directory: %v, skipping update check", err)
return
}
cacheFilePath := filepath.Join(configDir, updateCheckCacheFileName)
cacheFile, err := os.ReadFile(cacheFilePath)
if err != nil && !errors.Is(err, fs.ErrNotExist) {
log.Printf("error reading update cache file: %v, skipping update check", err)
return
}
// If we were able to read the update file, try to interpret it and use the cached
// value if it is still valid. Note the `err == nil` guard here ensures we don't run
// this logic when the cache file did not exist (since err will be a form of fs.ErrNotExist)
var cachedLatestVersion *semver.Version
if err == nil {
var cache updateCacheFile
if err := json.Unmarshal(cacheFile, &cache); err == nil {
parsedVersion, parseVersionErr := semver.Parse(cache.Version)
parsedExpiresOn, parseExpiresOnErr := time.Parse(time.RFC3339, cache.ExpiresOn)
if parseVersionErr == nil && parseExpiresOnErr == nil {
if time.Now().UTC().Before(parsedExpiresOn) {
log.Printf("using cached latest version: %s (expires on: %s)", cache.Version, cache.ExpiresOn)
cachedLatestVersion = &parsedVersion
} else {
log.Printf("ignoring cached latest version, it is out of date")
}
} else {
if parseVersionErr != nil {
log.Printf("failed to parse cached version '%s' as a semver: %v,"+
" ignoring cached value", cache.Version, parseVersionErr)
}
if parseExpiresOnErr != nil {
log.Printf(
"failed to parse cached version expiration time '%s' as a RFC3339"+
" timestamp: %v, ignoring cached value",
cache.ExpiresOn,
parseExpiresOnErr)
}
}
} else {
log.Printf("could not unmarshal cache file: %v, ignoring cache", err)
}
}
// If we don't have a cached version we can use, fetch one (and cache it)
if cachedLatestVersion == nil {
log.Print("fetching latest version information for update check")
req, err := http.NewRequest(http.MethodGet, "https://aka.ms/azure-dev/versions/cli/latest", nil)
if err != nil {
log.Printf("failed to create request object: %v, skipping update check", err)
}
req.Header.Set("User-Agent", internal.UserAgent())
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Printf("failed to fetch latest version: %v, skipping update check", err)
return
}
body, err := readToEndAndClose(res.Body)
if err != nil {
log.Printf("failed to read response body: %v, skipping update check", err)
return
}
if res.StatusCode != http.StatusOK {
log.Printf(
"failed to refresh latest version, http status: %v, body: %v, skipping update check",
res.StatusCode,
body,
)
return
}
// Parse the body of the response as a semver, and if it's valid, cache it.
fetchedVersionText := strings.TrimSpace(body)
fetchedVersion, err := semver.Parse(fetchedVersionText)
if err != nil {
log.Printf("failed to parse latest version '%s' as a semver: %v, skipping update check", fetchedVersionText, err)
return
}
cachedLatestVersion = &fetchedVersion
// Write the value back to the cache. Note that on these logging paths for errors we do not return
// eagerly, since we have not yet sent the latest versions across the channel (and we don't want to do that until
// we've updated the cache since reader on the other end of the channel will exit the process after it receives this
// value and finishes
// the up to date check, possibly while this go-routine is still running)
if err := os.MkdirAll(filepath.Dir(cacheFilePath), osutil.PermissionFile); err != nil {
log.Printf("failed to create cache folder '%s': %v", filepath.Dir(cacheFilePath), err)
} else {
cacheObject := updateCacheFile{
Version: fetchedVersionText,
ExpiresOn: time.Now().UTC().Add(24 * time.Hour).Format(time.RFC3339),
}
// The marshal call can not fail, so we ignore the error.
cacheContents, _ := json.Marshal(cacheObject)
if err := os.WriteFile(cacheFilePath, cacheContents, osutil.PermissionDirectory); err != nil {
log.Printf("failed to write update cache file: %v", err)
} else {
log.Printf("updated cache file to version %s (expires on: %s)", cacheObject.Version, cacheObject.ExpiresOn)
}
}
}
// Publish our value, the defer above will close the channel.
version <- *cachedLatestVersion
}