Repository: JanDeDobbeleer/oh-my-posh
Pull request #5976 (2024-12-01 19:46:11)

feat(upgrade): select source
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [x] The commit message follows the [conventional commits][cc] guidelines.
- [ ] Tests for the changes have been added (for bug fixes / features).
- [ ] Docs have been added/updated (for bug fixes / features).

### Description

TODO: add JSON schema and docs

<!---

Tips:

If you're not comfortable with working with Git,
we're working a guide (https://ohmyposh.dev/docs/contributing/git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/cli/notice.go b/src/cli/notice.go
index 58640b44..9bd6f99d 100644
--- a/src/cli/notice.go
+++ b/src/cli/notice.go
@@ -2,9 +2,10 @@ package cli
 
 import (
 	"fmt"
+	"os"
 
+	"github.com/jandedobbeleer/oh-my-posh/src/config"
 	"github.com/jandedobbeleer/oh-my-posh/src/runtime"
-	"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
 	"github.com/spf13/cobra"
 )
 
@@ -23,7 +24,12 @@ var noticeCmd = &cobra.Command{
 		env.Init(flags)
 		defer env.Close()
 
-		if notice, hasNotice := upgrade.Notice(env, false); hasNotice {
+		sh := os.Getenv("POSH_SHELL")
+		configFile := config.Path(configFlag)
+		cfg := config.Load(configFile, sh, false)
+		cfg.Upgrade.Cache = env.Cache()
+
+		if notice, hasNotice := cfg.Upgrade.Notice(); hasNotice {
 			fmt.Println(notice)
 		}
 	},
diff --git a/src/cli/upgrade.go b/src/cli/upgrade.go
index abeb6343..25676b37 100644
--- a/src/cli/upgrade.go
+++ b/src/cli/upgrade.go
@@ -7,6 +7,7 @@ import (
 	"slices"
 
 	"github.com/jandedobbeleer/oh-my-posh/src/build"
+	"github.com/jandedobbeleer/oh-my-posh/src/config"
 	"github.com/jandedobbeleer/oh-my-posh/src/runtime"
 	"github.com/jandedobbeleer/oh-my-posh/src/terminal"
 	"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
@@ -32,36 +33,42 @@ var upgradeCmd = &cobra.Command{
 			return
 		}
 
+		sh := os.Getenv("POSH_SHELL")
+
 		env := &runtime.Terminal{}
 		env.Init(nil)
 		defer env.Close()
 
-		terminal.Init(env.Shell())
+		terminal.Init(sh)
 		fmt.Print(terminal.StartProgress())
 
-		latest, err := upgrade.Latest(env)
+		configFile := config.Path(configFlag)
+		cfg := config.Load(configFile, sh, false)
+		cfg.Upgrade.Cache = env.Cache()
+
+		latest, err := cfg.Upgrade.Latest()
 		if err != nil {
 			fmt.Printf("\n❌ %s\n\n%s", err, terminal.StopProgress())
 			os.Exit(1)
 			return
 		}
 
+		cfg.Upgrade.Version = fmt.Sprintf("v%s", latest)
+
 		if force {
-			executeUpgrade(latest)
+			executeUpgrade(cfg.Upgrade)
 			return
 		}
 
-		version := fmt.Sprintf("v%s", build.Version)
-
-		if upgrade.IsMajorUpgrade(version, latest) {
+		if upgrade.IsMajorUpgrade(build.Version, latest) {
 			message := terminal.StopProgress()
-			message += fmt.Sprintf("\n🚨 major upgrade available: %s -> %s, use oh-my-posh upgrade --force to upgrade\n\n", version, latest)
+			message += fmt.Sprintf("\n🚨 major upgrade available: v%s -> v%s, use oh-my-posh upgrade --force to upgrade\n\n", build.Version, latest)
 			fmt.Print(message)
 			return
 		}
 
-		if version != latest {
-			executeUpgrade(latest)
+		if build.Version != latest {
+			executeUpgrade(cfg.Upgrade)
 			return
 		}
 
@@ -69,8 +76,8 @@ var upgradeCmd = &cobra.Command{
 	},
 }
 
-func executeUpgrade(latest string) {
-	err := upgrade.Run(latest)
+func executeUpgrade(cfg *upgrade.Config) {
+	err := upgrade.Run(cfg)
 	fmt.Print(terminal.StopProgress())
 	if err == nil {
 		return
diff --git a/src/config/config.go b/src/config/config.go
index e01baaaa..24ae767e 100644
--- a/src/config/config.go
+++ b/src/config/config.go
@@ -7,6 +7,7 @@ import (
 	"github.com/jandedobbeleer/oh-my-posh/src/shell"
 	"github.com/jandedobbeleer/oh-my-posh/src/template"
 	"github.com/jandedobbeleer/oh-my-posh/src/terminal"
+	"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
 )
 
 const (
@@ -30,26 +31,27 @@ type Config struct {
 	SecondaryPrompt         *Segment        `json:"secondary_prompt,omitempty" toml:"secondary_prompt,omitempty"`
 	TransientPrompt         *Segment        `json:"transient_prompt,omitempty" toml:"transient_prompt,omitempty"`
 	ErrorLine               *Segment        `json:"error_line,omitempty" toml:"error_line,omitempty"`
-	ConsoleTitleTemplate    string          `json:"console_title_template,omitempty" toml:"console_title_template,omitempty"`
-	Format                  string          `json:"-" toml:"-"`
+	TerminalBackground      color.Ansi      `json:"terminal_background,omitempty" toml:"terminal_background,omitempty"`
 	origin                  string
 	PWD                     string                 `json:"pwd,omitempty" toml:"pwd,omitempty"`
 	AccentColor             color.Ansi             `json:"accent_color,omitempty" toml:"accent_color,omitempty"`
 	Output                  string                 `json:"-" toml:"-"`
-	TerminalBackground      color.Ansi             `json:"terminal_background,omitempty" toml:"terminal_background,omitempty"`
+	ConsoleTitleTemplate    string                 `json:"console_title_template,omitempty" toml:"console_title_template,omitempty"`
+	Format                  string                 `json:"-" toml:"-"`
+	Upgrade                 *upgrade.Config        `json:"upgrade,omitempty" toml:"upgrade,omitempty"`
 	Cycle                   color.Cycle            `json:"cycle,omitempty" toml:"cycle,omitempty"`
 	ITermFeatures           terminal.ITermFeatures `json:"iterm_features,omitempty" toml:"iterm_features,omitempty"`
 	Blocks                  []*Block               `json:"blocks,omitempty" toml:"blocks,omitempty"`
 	Tooltips                []*Segment             `json:"tooltips,omitempty" toml:"tooltips,omitempty"`
 	Version                 int                    `json:"version" toml:"version"`
-	UpgradeNotice           bool                   `json:"upgrade_notice,omitempty" toml:"upgrade_notice,omitempty"`
-	AutoUpgrade             bool                   `json:"auto_upgrade,omitempty" toml:"auto_upgrade,omitempty"`
+	AutoUpgrade             bool                   `json:"-" toml:"-"`
 	ShellIntegration        bool                   `json:"shell_integration,omitempty" toml:"shell_integration,omitempty"`
 	MigrateGlyphs           bool                   `json:"-" toml:"-"`
 	PatchPwshBleed          bool                   `json:"patch_pwsh_bleed,omitempty" toml:"patch_pwsh_bleed,omitempty"`
 	EnableCursorPositioning bool                   `json:"enable_cursor_positioning,omitempty" toml:"enable_cursor_positioning,omitempty"`
 	updated                 bool
 	FinalSpace              bool `json:"final_space,omitempty" toml:"final_space,omitempty"`
+	UpgradeNotice           bool `json:"-" toml:"-"`
 }
 
 func (cfg *Config) MakeColors(env runtime.Environment) color.String {
@@ -98,12 +100,12 @@ func (cfg *Config) Features(env runtime.Environment) shell.Features {
 		feats = append(feats, shell.FTCSMarks)
 	}
 
-	autoUpgrade := cfg.AutoUpgrade
+	autoUpgrade := cfg.Upgrade.Auto
 	if _, OK := env.Cache().Get(AUTOUPGRADE); OK {
 		autoUpgrade = true
 	}
 
-	upgradeNotice := cfg.UpgradeNotice
+	upgradeNotice := cfg.Upgrade.DisplayNotice
 	if _, OK := env.Cache().Get(UPGRADENOTICE); OK {
 		upgradeNotice = true
 	}
diff --git a/src/config/load.go b/src/config/load.go
index edbe066e..84dadb91 100644
--- a/src/config/load.go
+++ b/src/config/load.go
@@ -15,6 +15,7 @@ import (
 	"github.com/jandedobbeleer/oh-my-posh/src/log"
 	"github.com/jandedobbeleer/oh-my-posh/src/runtime/path"
 	"github.com/jandedobbeleer/oh-my-posh/src/shell"
+	"github.com/jandedobbeleer/oh-my-posh/src/upgrade"
 
 	json "github.com/goccy/go-json"
 	yaml "github.com/goccy/go-yaml"
@@ -32,6 +33,19 @@ func Load(configFile, sh string, migrate bool) *Config {
 		cfg.BackupAndMigrate()
 	}
 
+	if cfg.Upgrade == nil {
+		cfg.Upgrade = &upgrade.Config{
+			Source:        upgrade.CDN,
+			DisplayNotice: cfg.UpgradeNotice,
+			Auto:          cfg.AutoUpgrade,
+			Interval:      cache.ONEWEEK,
+		}
+	}
+
+	if cfg.Upgrade.Interval.IsEmpty() {
+		cfg.Upgrade.Interval = cache.ONEWEEK
+	}
+
 	if !cfg.ShellIntegration {
 		return cfg
 	}
diff --git a/src/segments/upgrade.go b/src/segments/upgrade.go
index 53030bac..9207e243 100644
--- a/src/segments/upgrade.go
+++ b/src/segments/upgrade.go
@@ -68,23 +68,29 @@ func (u *Upgrade) cachedLatest(current string) (*UpgradeCache, error) {
 }
 
 func (u *Upgrade) checkUpdate(current string) (*UpgradeCache, error) {
-	tag, err := upgrade.Latest(u.env)
+	duration := u.props.GetString(properties.CacheDuration, string(cache.ONEWEEK))
+	source := u.props.GetString(Source, string(upgrade.CDN))
+
+	cfg := &upgrade.Config{
+		Source:   upgrade.Source(source),
+		Interval: cache.Duration(duration),
+	}
+
+	latest, err := cfg.Latest()
 	if err != nil {
 		return nil, err
 	}
 
-	latest := tag[1:]
 	cacheData := &UpgradeCache{
 		Latest:  latest,
 		Current: current,
 	}
+
 	cacheJSON, err := json.Marshal(cacheData)
 	if err != nil {
 		return nil, err
 	}
 
-	// update cache
-	duration := u.props.GetString(properties.CacheDuration, string(cache.ONEWEEK))
 	u.env.Cache().Set(UPGRADECACHEKEY, string(cacheJSON), cache.Duration(duration))
 
 	return cacheData, nil
diff --git a/src/upgrade/cli.go b/src/upgrade/cli.go
index a4d16d3d..9b3db13d 100644
--- a/src/upgrade/cli.go
+++ b/src/upgrade/cli.go
@@ -39,40 +39,40 @@ type stateMsg state
 
 type model struct {
 	error   error
+	config  *Config
 	message string
-	tag     string
 	spinner spinner.Model
 	state   state
 }
 
-func initialModel(tag string) *model {
+func initialModel(cfg *Config) *model {
 	s := spinner.New()
 	s.Spinner = spinner.Dot
 	s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("170"))
-	return &model{spinner: s, tag: tag}
+	return &model{spinner: s, config: cfg}
 }
 
 func (m *model) Init() tea.Cmd {
-	defer func() {
-		go func() {
-			if err := install(m.tag); err != nil {
-				m.error = err
-				program.Send(resultMsg(fmt.Sprintf("❌ upgrade failed: %v", err)))
-				return
-			}
+	go m.start()
 
-			message := "🚀 Upgrade successful"
+	return m.spinner.Tick
+}
 
-			current := fmt.Sprintf("v%s", build.Version)
-			if current != m.tag {
-				message += ", restart your shell to take full advantage of the new functionality"
-			}
+func (m *model) start() {
+	if err := install(m.config); err != nil {
+		m.error = err
+		program.Send(resultMsg(fmt.Sprintf("❌ upgrade failed: %v", err)))
+		return
+	}
 
-			program.Send(resultMsg(message))
-		}()
-	}()
+	message := "🚀 Upgrade successful"
 
-	return m.spinner.Tick
+	current := fmt.Sprintf("v%s", build.Version)
+	if current != m.config.Version {
+		message += ", restart your shell to take full advantage of the new functionality"
+	}
+
+	program.Send(resultMsg(message))
 }
 
 func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
@@ -113,7 +113,7 @@ func (m *model) View() string {
 		message = "Validating current installation"
 	case downloading:
 		m.spinner.Spinner = spinner.Globe
-		message = "Downloading latest version"
+		message = fmt.Sprintf("Downloading latest version from %s", m.config.Source.String())
 	case verifying:
 		m.spinner.Spinner = spinner.Moon
 		message = "Verifying download"
@@ -125,19 +125,19 @@ func (m *model) View() string {
 	return title + textStyle.Render(fmt.Sprintf("%s %s", m.spinner.View(), message))
 }
 
-func Run(latest string) error {
+func Run(cfg *Config) error {
 	titleStyle := lipgloss.NewStyle().Margin(1, 0, 1, 0)
 	title = "📦 Upgrading Oh My Posh"
 
-	current := build.Version
+	current := fmt.Sprintf("v%s", build.Version)
 	if len(current) == 0 {
 		current = "dev"
 	}
 
-	title = fmt.Sprintf("%s from %s to %s", title, current, latest)
+	title = fmt.Sprintf("%s from %s to %s", title, current, cfg.Version)
 	title = titleStyle.Render(title)
 
-	program = tea.NewProgram(initialModel(latest))
+	program = tea.NewProgram(initialModel(cfg))
 	resultModel, _ := program.Run()
 
 	programModel, OK := resultModel.(*model)
diff --git a/src/upgrade/config.go b/src/upgrade/config.go
new file mode 100644
index 00000000..bc4025ae
--- /dev/null
+++ b/src/upgrade/config.go
@@ -0,0 +1,100 @@
+package upgrade
+
+import (
+	"context"
+	"fmt"
+	"io"
+	httplib "net/http"
+	"strings"
+
+	"github.com/jandedobbeleer/oh-my-posh/src/cache"
+	"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
+)
+
+type Config struct {
+	Cache         cache.Cache    `json:"-" toml:"-"`
+	Source        Source         `json:"source" toml:"source"`
+	Interval      cache.Duration `json:"interval" toml:"interval"`
+	Version       string         `json:"-" toml:"-"`
+	Auto          bool           `json:"auto" toml:"auto"`
+	DisplayNotice bool           `json:"notice" toml:"notice"`
+	Force         bool           `json:"-" toml:"-"`
+}
+
+type Source string
+
+const (
+	GitHub Source = "github"
+	CDN    Source = "cdn"
+)
+
+func (s Source) String() string {
+	switch s {
+	case GitHub:
+		return "github.com"
+	case CDN:
+		return "cdn.ohmyposh.dev"
+	default:
+		return "Unknown"
+	}
+}
+
+func (cfg *Config) Latest() (string, error) {
+	cfg.Version = "latest"
+	v, err := cfg.DownloadAsset("version.txt")
+	version := strings.TrimSpace(string(v))
+	return strings.TrimPrefix(version, "v"), err
+}
+
+func (cfg *Config) DownloadAsset(asset string) ([]byte, error) {
+	if len(cfg.Source) == 0 {
+		cfg.Source = GitHub
+	}
+
+	switch cfg.Source {
+	case GitHub:
+		var url string
+
+		switch cfg.Version {
+		case "latest":
+			url = fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/%s", asset)
+		default:
+			url = fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/%s/%s", cfg.Version, asset)
+		}
+
+		return cfg.Download(url)
+	case CDN:
+		fallthrough
+	default:
+		url := fmt.Sprintf("https://cdn.ohmyposh.dev/releases/%s/%s", cfg.Version, asset)
+		return cfg.Download(url)
+	}
+}
+
+func (cfg *Config) Download(url string) ([]byte, error) {
+	req, err := httplib.NewRequestWithContext(context.Background(), "GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Add("User-Agent", "oh-my-posh")
+	req.Header.Add("Cache-Control", "max-age=0")
+
+	resp, err := http.HTTPClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.StatusCode != httplib.StatusOK {
+		return nil, fmt.Errorf("failed to download asset: %s", url)
+	}
+
+	defer resp.Body.Close()
+
+	data, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	return data, nil
+}
diff --git a/src/upgrade/github.go b/src/upgrade/github.go
deleted file mode 100644
index 6efc865e..00000000
--- a/src/upgrade/github.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package upgrade
-
-import (
-	"context"
-	"fmt"
-	"io"
-	httplib "net/http"
-
-	"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
-)
-
-func downloadReleaseAsset(tag, asset string) ([]byte, error) {
-	url := fmt.Sprintf("https://github.com/JanDeDobbeleer/oh-my-posh/releases/download/%s/%s", tag, asset)
-
-	req, err := httplib.NewRequestWithContext(context.Background(), "GET", url, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Header.Add("User-Agent", "oh-my-posh")
-
-	resp, err := http.HTTPClient.Do(req)
-	if err != nil {
-		return nil, err
-	}
-
-	if resp.StatusCode != httplib.StatusOK {
-		return nil, fmt.Errorf("failed to download asset: %s", url)
-	}
-
-	defer resp.Body.Close()
-
-	data, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, err
-	}
-
-	return data, nil
-}
diff --git a/src/upgrade/install.go b/src/upgrade/install.go
index d4f0c24c..88d6bcdf 100644
--- a/src/upgrade/install.go
+++ b/src/upgrade/install.go
@@ -9,7 +9,7 @@ import (
 	"path/filepath"
 )
 
-func install(tag string) error {
+func install(cfg *Config) error {
 	setState(validating)
 
 	executable, err := os.Executable()
@@ -28,7 +28,7 @@ func install(tag string) error {
 
 	setState(downloading)
 
-	data, err := downloadAndVerify(tag)
+	data, err := downloadAndVerify(cfg)
 	if err != nil {
 		return err
 	}
@@ -71,7 +71,5 @@ func install(tag string) error {
 		_ = hideFile(oldPath)
 	}
 
-	updateRegistry(tag, executable)
-
 	return nil
 }
diff --git a/src/upgrade/install_noop.go b/src/upgrade/install_noop.go
index 785e25c9..ec035bed 100644
--- a/src/upgrade/install_noop.go
+++ b/src/upgrade/install_noop.go
@@ -5,5 +5,3 @@ package upgrade
 func hideFile(_ string) error {
 	return nil
 }
-
-func updateRegistry(_, _ string) {}
diff --git a/src/upgrade/install_windows.go b/src/upgrade/install_windows.go
index 958665bf..c10f3f89 100644
--- a/src/upgrade/install_windows.go
+++ b/src/upgrade/install_windows.go
@@ -1,14 +1,8 @@
 package upgrade
 
 import (
-	"errors"
-	"path/filepath"
-	"strconv"
-	"strings"
 	"syscall"
 	"unsafe"
-
-	"golang.org/x/sys/windows/registry"
 )
 
 func hideFile(path string) error {
@@ -28,89 +22,3 @@ func hideFile(path string) error {
 
 	return nil
 }
-
-func updateRegistry(version, executable string) {
-	// needs to be the parent directory of the executable's bin directory
-	// with a trailing backslash to match the registry key
-	// in case this wasn't installed with the installer, nothing will match
-	// and we don't set the registry keys
-	installLocation := filepath.Dir(executable)
-	installLocation = filepath.Dir(installLocation)
-	installLocation += `\`
-
-	key, err := getRegistryKey(installLocation)
-	if err != nil {
-		key.Close()
-		return
-	}
-
-	version = strings.TrimLeft(version, "v")
-
-	_ = key.SetStringValue("DisplayVersion", version)
-	_ = key.SetStringValue("DisplayName", "Oh My Posh")
-
-	splitted := strings.Split(version, ".")
-	if len(splitted) < 3 {
-		key.Close()
-		return
-	}
-
-	if u64, err := strconv.ParseUint(splitted[0], 10, 32); err == nil {
-		major := uint32(u64)
-		_ = key.SetDWordValue("MajorVersion", major)
-		_ = key.SetDWordValue("VersionMajor", major)
-	}
-
-	if u64, err := strconv.ParseUint(splitted[1], 10, 32); err == nil {
-		minor := uint32(u64)
-		_ = key.SetDWordValue("MinorVersion", minor)
-		_ = key.SetDWordValue("VersionMinor", minor)
-	}
-
-	key.Close()
-}
-
-// getRegistryKey tries all known registry paths to find the one we need to adjust (if any)
-func getRegistryKey(installLocation string) (registry.Key, error) {
-	knownRegistryPaths := []struct {
-		Path string
-		Key  registry.Key
-	}{
-		{`Software\Microsoft\Windows\CurrentVersion\Uninstall`, registry.CURRENT_USER},
-		{`Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`, registry.CURRENT_USER},
-		{`Software\Microsoft\Windows\CurrentVersion\Uninstall`, registry.LOCAL_MACHINE},
-		{`Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`, registry.LOCAL_MACHINE},
-	}
-
-	for _, path := range knownRegistryPaths {
-		key, ok := tryRegistryKey(path.Key, path.Path, installLocation)
-		if ok {
-			return key, nil
-		}
-	}
-
-	return registry.CURRENT_USER, errors.New("could not find registry key")
-}
-
-// tryRegistryKey tries to open the registry key for the given path
-// and checks if the install location matches with the current executable's location
-func tryRegistryKey(key registry.Key, path, installLocation string) (registry.Key, bool) {
-	path += `\Oh My Posh_is1`
-
-	readKey, err := registry.OpenKey(key, path, registry.READ)
-	if err != nil {
-		return key, false
-	}
-
-	location, _, err := readKey.GetStringValue("InstallLocation")
-	if err != nil {
-		return key, false
-	}
-
-	if location != installLocation {
-		return key, false
-	}
-
-	key, err = registry.OpenKey(key, path, registry.WRITE)
-	return key, err == nil
-}
diff --git a/src/upgrade/notice.go b/src/upgrade/notice.go
index cdf13ab5..32880290 100644
--- a/src/upgrade/notice.go
+++ b/src/upgrade/notice.go
@@ -1,100 +1,60 @@
 package upgrade
 
 import (
-	"encoding/json"
 	"fmt"
-	"time"
+	"os"
 
 	"github.com/jandedobbeleer/oh-my-posh/src/build"
-	"github.com/jandedobbeleer/oh-my-posh/src/cache"
-	"github.com/jandedobbeleer/oh-my-posh/src/runtime"
+	"github.com/jandedobbeleer/oh-my-posh/src/log"
 	"github.com/jandedobbeleer/oh-my-posh/src/runtime/http"
 )
 
-type release struct {
-	CreatedAt       time.Time `json:"created_at"`
-	PublishedAt     time.Time `json:"published_at"`
-	NodeID          string    `json:"node_id"`
-	TagName         string    `json:"tag_name"`
-	TarballURL      string    `json:"tarball_url"`
-	ZipballURL      string    `json:"zipball_url"`
-	DiscussionURL   string    `json:"discussion_url"`
-	HTMLURL         string    `json:"html_url"`
-	URL             string    `json:"url"`
-	UploadURL       string    `json:"upload_url"`
-	TargetCommitish string    `json:"target_commitish"`
-	Name            string    `json:"name"`
-	Body            string    `json:"body"`
-	AssetsURL       string    `json:"assets_url"`
-	ID              int       `json:"id"`
-	Prerelease      bool      `json:"prerelease"`
-	Draft           bool      `json:"draft"`
-}
-
 const (
-	RELEASEURL = "https://api.github.com/repos/jandedobbeleer/oh-my-posh/releases/latest"
-	CACHEKEY   = "upgrade_check"
+	CACHEKEY = "upgrade_check"
 
 	upgradeNotice = `
-A new release of Oh My Posh is available: %s → %s
+A new release of Oh My Posh is available: v%s → v%s
 To upgrade, run: 'oh-my-posh upgrade%s'
 
 To enable automated upgrades, run: 'oh-my-posh enable upgrade'.
 `
 )
 
-func Latest(env runtime.Environment) (string, error) {
-	body, err := env.HTTPRequest(RELEASEURL, nil, 5000)
-	if err != nil {
-		return "", err
-	}
-
-	var release release
-	// this can't fail
-	_ = json.Unmarshal(body, &release)
-
-	if len(release.TagName) == 0 {
-		return "", fmt.Errorf("failed to get latest release")
-	}
-
-	return release.TagName, nil
-}
-
 // Returns the upgrade notice if a new version is available
 // that should be displayed to the user.
 //
 // The upgrade check is only performed every other week.
-func Notice(env runtime.Environment, force bool) (string, bool) {
-	// do not check when last validation was < 1 week ago
-	if _, OK := env.Cache().Get(CACHEKEY); OK && !force {
+func (cfg *Config) Notice() (string, bool) {
+	// never validate when we install using the Windows Store
+	if os.Getenv("POSH_INSTALLER") == "ws" {
+		log.Debug("skipping upgrade check because we are using the Windows Store")
 		return "", false
 	}
 
-	if !http.IsConnected() {
+	// do not check when last validation was < 1 week ago
+	if _, OK := cfg.Cache.Get(CACHEKEY); OK && !cfg.Force {
 		return "", false
 	}
 
-	// never validate when we install using the Windows Store
-	if env.Getenv("POSH_INSTALLER") == "ws" {
+	if !http.IsConnected() {
 		return "", false
 	}
 
-	latest, err := Latest(env)
+	latest, err := cfg.Latest()
 	if err != nil {
 		return "", false
 	}
 
-	env.Cache().Set(CACHEKEY, latest, cache.ONEWEEK)
+	cfg.Cache.Set(CACHEKEY, latest, cfg.Interval)
 
-	version := fmt.Sprintf("v%s", build.Version)
-	if latest == version {
+	if latest == build.Version {
 		return "", false
 	}
 
 	var forceUpdate string
-	if IsMajorUpgrade(version, latest) {
+	if IsMajorUpgrade(build.Version, latest) {
 		forceUpdate = " --force"
 	}
 
-	return fmt.Sprintf(upgradeNotice, version, latest, forceUpdate), true
+	return fmt.Sprintf(upgradeNotice, build.Version, latest, forceUpdate), true
 }
diff --git a/src/upgrade/verify.go b/src/upgrade/verify.go
index 2af7955b..1292fd95 100644
--- a/src/upgrade/verify.go
+++ b/src/upgrade/verify.go
@@ -32,7 +32,7 @@ import (
 //go:embed public_key.pem
 var publicKey []byte
 
-func downloadAndVerify(tag string) ([]byte, error) {
+func downloadAndVerify(cfg *Config) ([]byte, error) {
 	extension := ""
 	if stdruntime.GOOS == runtime.WINDOWS {
 		extension = ".exe"
@@ -40,14 +40,14 @@ func downloadAndVerify(tag string) ([]byte, error) {
 
 	asset := fmt.Sprintf("posh-%s-%s%s", stdruntime.GOOS, stdruntime.GOARCH, extension)
 
-	data, err := downloadReleaseAsset(tag, asset)
+	data, err := cfg.DownloadAsset(asset)
 	if err != nil {
 		return nil, err
 	}
 
 	setState(verifying)
 
-	err = verify(tag, asset, data)
+	err = verify(cfg, asset, data)
 	if err != nil {
 		return nil, err
 	}
@@ -55,13 +55,13 @@ func downloadAndVerify(tag string) ([]byte, error) {
 	return data, nil
 }
 
-func verify(tag, asset string, binary []byte) error {
-	checksums, err := downloadReleaseAsset(tag, "checksums.txt")
+func verify(cfg *Config, asset string, binary []byte) error {
+	checksums, err := cfg.DownloadAsset("checksums.txt")
 	if err != nil {
 		return err
 	}
 
-	signature, err := downloadReleaseAsset(tag, "checksums.txt.sig")
+	signature, err := cfg.DownloadAsset("checksums.txt.sig")
 	if err != nil {
 		return err
 	}
diff --git a/themes/schema.json b/themes/schema.json
index a4e7967b..1318b394 100644
--- a/themes/schema.json
+++ b/themes/schema.json
@@ -1281,8 +1281,22 @@
                     "description": "The extensions to look for when determining if a folder is a Fortran workspace",
                     "default": [
                       "fpm.toml",
-                      "*.f", "*.for", "*.fpp", "*.f77", "*.f90", "*.f95", "*.f03", "*.f08",
-                      "*.F", "*.FOR", "*.FPP", "*.F77", "*.F90", "*.F95", "*.F03", "*.F08"
+                      "*.f",
+                      "*.for",
+                      "*.fpp",
+                      "*.f77",
+                      "*.f90",
+                      "*.f95",
+                      "*.f03",
+                      "*.f08",
+                      "*.F",
+                      "*.FOR",
+                      "*.FPP",
+                      "*.F77",
+                      "*.F90",
+                      "*.F95",
+                      "*.F03",
+                      "*.F08"
                     ],
                     "items": {
                       "type": "string"
@@ -5036,17 +5050,36 @@
       "description": "https://ohmyposh.dev/docs/configuration/general#general-settings",
       "default": ""
     },
-    "upgrade_notice": {
-      "type": "boolean",
+    "upgrade": {
+      "type": "object",
       "title": "Enable Upgrade Notice",
       "description": "https://ohmyposh.dev/docs/configuration/general#general-settings",
-      "default": false
-    },
-    "auto_upgrade": {
-      "type": "boolean",
-      "title": "Enable automatic upgrades for Oh My Posh (supports Windows/macOS only)",
-      "description": "https://ohmyposh.dev/docs/configuration/general#general-settings",
-      "default": false
+      "default": {
+        "source": "cdn",
+        "auto": false,
+        "notice": false
+      },
+      "properties": {
+        "interval": {
+          "$ref": "#/definitions/cache_duration"
+        },
+        "source": {
+          "type": "string",
+          "enum": [
+            "cdn",
+            "github"
+          ],
+          "default": "cdn"
+        },
+        "auto": {
+          "type": "boolean",
+          "default": false
+        },
+        "notice": {
+          "type": "boolean",
+          "default": false
+        }
+      }
     },
     "patch_pwsh_bleed": {
       "type": "boolean",
diff --git a/website/docs/configuration/general.mdx b/website/docs/configuration/general.mdx
index 57224e31..b449213f 100644
--- a/website/docs/configuration/general.mdx
+++ b/website/docs/configuration/general.mdx
@@ -136,8 +136,7 @@ For example, the following is a valid `--config` flag:
 | `shell_integration`         | `boolean`        | `false` | enable shell integration using FinalTerm's OSC sequences. Works in bash, cmd (Clink v1.14.25+), fish, powershell and zsh                                                                                                                                                     |
 | `enable_cursor_positioning` | `boolean`        | `false` | enable fetching the cursor position in bash and zsh to allow automatic hiding of leading newlines when at the top of the shell                                                                                                                                               |
 | `patch_pwsh_bleed`          | `boolean`        | `false` | patch a PowerShell bug where the background colors bleed into the next line at the end of the buffer (can be removed when [this][pwsh-bleed] is merged)                                                                                                                      |
-| `upgrade_notice`            | `boolean`        | `false` | enable the notice that a new upgrade is available when `auto_upgrade` is disabled                                                                                                                                                                                            |
-| `auto_upgrade`              | `boolean`        | `false` | enable [automatic upgrades][upgrade] for Oh My Posh (supports Windows, macOS and Linux)                                                                                                                                                                                                 |
+| `upgrade`                   | `Upgrade`        |         | enable auto upgrade or the upgrade notice. See [Upgrade]                                                                                                                                                                                            |
 | `iterm_features`            | `[]string`       | `false` | enable iTerm2 specific features:<ul><li>`prompt_mark`: add the `iterm2_prompt_mark` [function][iterm2-si] for supported shells</li><li>`current_dir`: expose the current directory for iTerm2</li><li>`remote_host`: expose the current remote and user for iTerm2</li></ul> |
 
 ### JSON Schema Validation
@@ -194,4 +193,4 @@ Converters won't catch this change, so you will need to adjust manually.
 [templates]: /docs/configuration/templates#config-variables
 [pwsh-bleed]: https://github.com/PowerShell/PowerShell/pull/19019
 [iterm2-si]: https://iterm2.com/documentation-shell-integration.html
-[upgrade]: /docs/installation/upgrade#automatic
+[Upgrade]: /docs/installation/upgrade
diff --git a/website/docs/installation/upgrade.mdx b/website/docs/installation/upgrade.mdx
index 4608a7b3..751c442f 100644
--- a/website/docs/installation/upgrade.mdx
+++ b/website/docs/installation/upgrade.mdx
@@ -6,8 +6,34 @@ sidebar_label: ♻️ Upgrade
 
 import Tabs from "@theme/Tabs";
 import TabItem from "@theme/TabItem";
+import Config from "@site/src/components/Config.js";
+
+## Configuration
+
+Oh My Posh can display the availability of an update, or auto update itself when
+enabled by adding the following to your configuration.
+
+<Config
+  data={{
+    upgrade: {
+      notice: true,
+      interval: "168h",
+      auto: false,
+      source: "cdn",
+    },
+  }}
+/>
+
+| Name       |   Type    | Default | Description                                                                                                                                                                                                                |
+| ---------- | :-------: | :-----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `notice`   | `boolean` | `false` | enbale displaying the upgrade notice on shell start, only checks based on `interval`                                                                                                                                       |
+| `auto`     | `boolean` | `false` | automatically update Oh My Posh when an update is found, only checks based on `interval`                                                                                                                                   |
+| `interval` | `string`  |  `24h`  | the duration for which not to check for an update. The duration is a string in the format `1h2m3s` and is parsed using the [time.ParseDuration] function from the Go standard library                                      |
+| `source`   | `string`  |  `cdn`  | where to fetch the information from. Accepted values are `cdn` (`https://cdn.ohmyposh.dev/releases/latest/version.txt`) and `github` (`https://github.com/JanDeDobbeleer/oh-my-posh/releases/latest/download/version.txt`) |
 
-## Manual
+## Upgrade
+
+### Manual
 
 While you can always follow the upgrade steps listed under the installation section,
 you can also use the `upgrade` command to update Oh My Posh to the latest version.
@@ -16,7 +42,7 @@ you can also use the `upgrade` command to update Oh My Posh to the latest versio
 oh-my-posh upgrade
 ```
 
-## Automated
+### Automated
 
 <Tabs
   defaultValue="cli"
@@ -35,11 +61,13 @@ oh-my-posh enable upgrade
 </TabItem>
 <TabItem value="config">
 
-import Config from "@site/src/components/Config.js";
-
 <Config
   data={{
-    "auto_upgrade": true
+    upgrade: {
+      interval: "168h",
+      auto: true,
+      source: "cdn",
+    },
   }}
 />
 


Test patch:
diff --git a/src/segments/upgrade_test.go b/src/segments/upgrade_test.go
index 5dcc2f10..10c264e3 100644
--- a/src/segments/upgrade_test.go
+++ b/src/segments/upgrade_test.go
@@ -1,7 +1,6 @@
 package segments
 
 import (
-	"errors"
 	"fmt"
 	"testing"
 
@@ -16,8 +15,10 @@ import (
 )
 
 func TestUpgrade(t *testing.T) {
+	ugc := &upgrade.Config{}
+	latest, _ := ugc.Latest()
+
 	cases := []struct {
-		Error           error
 		Case            string
 		CurrentVersion  string
 		LatestVersion   string
@@ -33,33 +34,28 @@ func TestUpgrade(t *testing.T) {
 		},
 		{
 			Case:           "On latest",
-			CurrentVersion: "1.0.1",
-			LatestVersion:  "1.0.1",
-		},
-		{
-			Case:  "Error on update check",
-			Error: errors.New("error"),
+			CurrentVersion: latest,
 		},
 		{
 			Case:            "On previous, from cache",
 			HasCache:        true,
 			CurrentVersion:  "1.0.2",
-			LatestVersion:   "1.0.3",
+			LatestVersion:   latest,
 			CachedVersion:   "1.0.2",
 			ExpectedEnabled: true,
 		},
 		{
 			Case:           "On latest, version changed",
 			HasCache:       true,
-			CurrentVersion: "1.0.2",
-			LatestVersion:  "1.0.2",
+			CurrentVersion: latest,
+			LatestVersion:  latest,
 			CachedVersion:  "1.0.1",
 		},
 		{
 			Case:            "On previous, version changed",
 			HasCache:        true,
 			CurrentVersion:  "1.0.2",
-			LatestVersion:   "1.0.3",
+			LatestVersion:   latest,
 			CachedVersion:   "1.0.1",
 			ExpectedEnabled: true,
 		},
@@ -73,15 +69,13 @@ func TestUpgrade(t *testing.T) {
 		if len(tc.CachedVersion) == 0 {
 			tc.CachedVersion = tc.CurrentVersion
 		}
+
 		cacheData := fmt.Sprintf(`{"latest":"%s", "current": "%s"}`, tc.LatestVersion, tc.CachedVersion)
 		cache.On("Get", UPGRADECACHEKEY).Return(cacheData, tc.HasCache)
 		cache.On("Set", testify_.Anything, testify_.Anything, testify_.Anything)
 
 		build.Version = tc.CurrentVersion
 
-		json := fmt.Sprintf(`{"tag_name":"v%s"}`, tc.LatestVersion)
-		env.On("HTTPRequest", upgrade.RELEASEURL).Return([]byte(json), tc.Error)
-
 		ug := &Upgrade{}
 		ug.Init(properties.Map{}, env)
 
diff --git a/src/upgrade/notice_test.go b/src/upgrade/notice_test.go
index ec95d736..6fa5099d 100644
--- a/src/upgrade/notice_test.go
+++ b/src/upgrade/notice_test.go
@@ -1,52 +1,47 @@
 package upgrade
 
 import (
-	"fmt"
+	"os"
 	"testing"
 
 	"github.com/jandedobbeleer/oh-my-posh/src/build"
-	cache "github.com/jandedobbeleer/oh-my-posh/src/cache/mock"
-	"github.com/jandedobbeleer/oh-my-posh/src/runtime"
-	"github.com/jandedobbeleer/oh-my-posh/src/runtime/mock"
+	cache_ "github.com/jandedobbeleer/oh-my-posh/src/cache/mock"
 	"github.com/stretchr/testify/assert"
-
-	testify "github.com/stretchr/testify/mock"
+	testify_ "github.com/stretchr/testify/mock"
 )
 
 func TestCanUpgrade(t *testing.T) {
+	ugc := &Config{}
+	latest, _ := ugc.Latest()
+
 	cases := []struct {
-		Error          error
 		Case           string
 		CurrentVersion string
-		LatestVersion  string
-		GOOS           string
 		Installer      string
 		Expected       bool
 		Cache          bool
 	}{
-		{Case: "Up to date", CurrentVersion: "3.0.0", LatestVersion: "v3.0.0"},
-		{Case: "Outdated Windows", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: runtime.WINDOWS},
-		{Case: "Outdated Linux", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: runtime.LINUX},
-		{Case: "Outdated Darwin", Expected: true, CurrentVersion: "3.0.0", LatestVersion: "v3.0.1", GOOS: runtime.DARWIN},
+		{Case: "Up to date", CurrentVersion: latest},
+		{Case: "Outdated Linux", Expected: true, CurrentVersion: "3.0.0"},
+		{Case: "Outdated Darwin", Expected: true, CurrentVersion: "3.0.0"},
 		{Case: "Cached", Cache: true},
-		{Case: "Error", Error: fmt.Errorf("error")},
 		{Case: "Windows Store", Installer: "ws"},
 	}
 
 	for _, tc := range cases {
-		env := new(mock.Environment)
 		build.Version = tc.CurrentVersion
-		c := &cache.Cache{}
+		c := &cache_.Cache{}
 		c.On("Get", CACHEKEY).Return("", tc.Cache)
-		c.On("Set", testify.Anything, testify.Anything, testify.Anything)
-		env.On("Cache").Return(c)
-		env.On("GOOS").Return(tc.GOOS)
-		env.On("Getenv", "POSH_INSTALLER").Return(tc.Installer)
-
-		json := fmt.Sprintf(`{"tag_name":"%s"}`, tc.LatestVersion)
-		env.On("HTTPRequest", RELEASEURL).Return([]byte(json), tc.Error)
-		// ignore the notice
-		_, canUpgrade := Notice(env, false)
+		c.On("Set", testify_.Anything, testify_.Anything, testify_.Anything)
+		ugc.Cache = c
+
+		if len(tc.Installer) > 0 {
+			os.Setenv("POSH_INSTALLER", tc.Installer)
+		}
+
+		_, canUpgrade := ugc.Notice()
 		assert.Equal(t, tc.Expected, canUpgrade, tc.Case)
+
+		os.Setenv("POSH_INSTALLER", "")
 	}
 }