func fetchLatestVersion()

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
}