Add new release channel to tie to RomM version.

This commit is contained in:
Brandon T. Kowalski
2026-01-19 17:59:39 -05:00
parent 712b54d6c2
commit bfdbfcff9a
12 changed files with 168 additions and 16 deletions
+1
View File
@@ -1,4 +1,5 @@
ENVIRONMENT=DEV
# GROUT_VERSION=
# Window Size overrides (defaults to full)
# WINDOW_WIDTH=1024
+8 -2
View File
@@ -146,9 +146,9 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
})
}
// Start auto-update check on first platform menu view
autoUpdateOnce.Do(func() {
autoUpdate = update.NewAutoUpdate(currentCFW, config.ReleaseChannel)
host, _ := gaba.Get[romm.Host](ctx)
autoUpdate = update.NewAutoUpdate(currentCFW, config.ReleaseChannel, &host)
ui.AddStatusBarIcon(autoUpdate.Icon())
autoUpdate.Start()
})
@@ -661,6 +661,10 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
nav.AdvancedSettingsPos.Index = result.Value.LastSelectedIndex
nav.AdvancedSettingsPos.VisibleStartIndex = result.Value.LastVisibleStartIndex
if result.ExitCode == gaba.ExitCodeSuccess && autoUpdate != nil {
autoUpdate.Recheck(config.ReleaseChannel)
}
return result.Value, result.ExitCode
}).
On(gaba.ExitCodeSuccess, settings).
@@ -958,11 +962,13 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
gaba.AddState(fsm, updateCheck, func(ctx *gaba.Context) (ui.UpdateOutput, gaba.ExitCode) {
currentCFW, _ := gaba.Get[cfw.CFW](ctx)
host, _ := gaba.Get[romm.Host](ctx)
screen := ui.NewUpdateScreen()
result, err := screen.Draw(ui.UpdateInput{
CFW: currentCFW,
ReleaseChannel: config.ReleaseChannel,
Host: &host,
})
if err != nil {
+4 -3
View File
@@ -17,8 +17,9 @@ import (
type ReleaseChannel string
const (
ReleaseChannelStable ReleaseChannel = "stable"
ReleaseChannelBeta ReleaseChannel = "beta"
ReleaseChannelMatchRomM ReleaseChannel = "match_romm"
ReleaseChannelStable ReleaseChannel = "stable"
ReleaseChannelBeta ReleaseChannel = "beta"
)
var kidModeEnabled atomic.Bool
@@ -138,7 +139,7 @@ func SaveConfig(config *Config) error {
}
if config.ReleaseChannel == "" {
config.ReleaseChannel = ReleaseChannelStable
config.ReleaseChannel = ReleaseChannelMatchRomM
}
gaba.SetRawLogLevel(config.LogLevel)
+13
View File
@@ -0,0 +1,13 @@
package romm
type HeartbeatResponse struct {
System struct {
Version string `json:"VERSION"`
} `json:"SYSTEM"`
}
func (c *Client) GetHeartbeat() (HeartbeatResponse, error) {
var heartbeat HeartbeatResponse
err := c.doRequest("GET", endpointHeartbeat, nil, nil, &heartbeat)
return heartbeat, err
}
+1
View File
@@ -143,6 +143,7 @@ func (s *AdvancedSettingsScreen) buildMenuItems(config *internal.Config) []gaba.
{
Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_release_channel", Other: "Release Channel"}, nil)},
Options: []gaba.Option{
{DisplayName: i18n.Localize(&goi18n.Message{ID: "release_match_romm", Other: "Match RomM"}, nil), Value: internal.ReleaseChannelMatchRomM},
{DisplayName: i18n.Localize(&goi18n.Message{ID: "release_stable", Other: "Stable"}, nil), Value: internal.ReleaseChannelStable},
{DisplayName: i18n.Localize(&goi18n.Message{ID: "release_beta", Other: "Beta"}, nil), Value: internal.ReleaseChannelBeta},
},
+4 -2
View File
@@ -258,10 +258,12 @@ func logLevelToIndex(level string) int {
func releaseChannelToIndex(releaseChannel internal.ReleaseChannel) int {
switch releaseChannel {
case internal.ReleaseChannelStable:
case internal.ReleaseChannelMatchRomM:
return 0
case internal.ReleaseChannelBeta:
case internal.ReleaseChannelStable:
return 1
case internal.ReleaseChannelBeta:
return 2
default:
return 0
}
+3 -1
View File
@@ -6,6 +6,7 @@ import (
"grout/cfw"
"grout/internal"
"grout/internal/stringutil"
"grout/romm"
"grout/update"
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
@@ -18,6 +19,7 @@ import (
type UpdateInput struct {
CFW cfw.CFW
ReleaseChannel internal.ReleaseChannel
Host *romm.Host
}
type UpdateOutput struct {
@@ -43,7 +45,7 @@ func (s *UpdateScreen) Draw(input UpdateInput) (ScreenResult[UpdateOutput], erro
ShowThemeBackground: true,
},
func() (interface{}, error) {
updateInfo, checkErr = update.CheckForUpdate(input.CFW, input.ReleaseChannel)
updateInfo, checkErr = update.CheckForUpdate(input.CFW, input.ReleaseChannel, input.Host)
return nil, checkErr
},
)
+20 -2
View File
@@ -3,6 +3,7 @@ package update
import (
"grout/cfw"
"grout/internal"
"grout/romm"
"sync/atomic"
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
@@ -13,6 +14,7 @@ const updateIcon = "\U000F06B0"
type AutoUpdate struct {
cfwType cfw.CFW
releaseChannel internal.ReleaseChannel
host *romm.Host
icon *gaba.DynamicStatusBarIcon
running atomic.Bool
updateAvailable atomic.Bool
@@ -20,10 +22,11 @@ type AutoUpdate struct {
updateInfo *Info
}
func NewAutoUpdate(c cfw.CFW, r internal.ReleaseChannel) *AutoUpdate {
func NewAutoUpdate(c cfw.CFW, r internal.ReleaseChannel, host *romm.Host) *AutoUpdate {
return &AutoUpdate{
cfwType: c,
releaseChannel: r,
host: host,
icon: gaba.NewDynamicStatusBarIcon(""), // Start empty, will show icon if update available
done: make(chan struct{}),
}
@@ -53,6 +56,21 @@ func (a *AutoUpdate) UpdateInfo() *Info {
return a.updateInfo
}
// Recheck updates the release channel and re-runs the update check.
// This should be called when the user changes the release channel in settings.
func (a *AutoUpdate) Recheck(releaseChannel internal.ReleaseChannel) {
if a.running.Load() {
return // Already running, skip
}
a.releaseChannel = releaseChannel
a.updateAvailable.Store(false)
a.updateInfo = nil
a.icon.SetText("") // Clear the icon
a.Start()
}
func (a *AutoUpdate) run() {
logger := gaba.GetLogger()
defer func() {
@@ -62,7 +80,7 @@ func (a *AutoUpdate) run() {
logger.Debug("AutoUpdate: Checking for updates in background")
info, err := CheckForUpdate(a.cfwType, a.releaseChannel)
info, err := CheckForUpdate(a.cfwType, a.releaseChannel, a.host)
if err != nil {
logger.Debug("AutoUpdate: Failed to check for updates", "error", err)
return
+73
View File
@@ -95,3 +95,76 @@ func (r *GitHubRelease) FindAsset(name string) *GitHubAsset {
}
return nil
}
// FetchReleaseForRomMVersion fetches the latest Grout release that matches
// the first 3 semver components (major.minor.patch) of the given RomM version.
// For example, if rommVersion is "4.6.0-alpha.3", this will find Grout releases
// like "4.6.0", "4.6.0.1", "4.6.0-beta.1", etc.
func FetchReleaseForRomMVersion(rommVersion string) (*GitHubRelease, error) {
rommVer, err := ParseVersion(rommVersion)
if err != nil {
return nil, fmt.Errorf("failed to parse RomM version: %w", err)
}
url := fmt.Sprintf("%s/repos/%s/%s/releases", githubAPIURL, repoOwner, repoName)
client := &http.Client{
Timeout: defaultTimeout,
}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "Grout-Updater")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch release: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return nil, fmt.Errorf("no releases found")
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
var releases []GitHubRelease
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
if len(releases) == 0 {
return nil, fmt.Errorf("no releases found")
}
// Find the latest release that matches the RomM version's major.minor.patch
for _, release := range releases {
if release.Draft {
continue
}
releaseVer, err := ParseVersion(release.TagName)
if err != nil {
gaba.GetLogger().Debug("skipping release with unparseable version", "tag", release.TagName, "error", err)
continue
}
// Check if major.minor.patch match
if releaseVer.Major == rommVer.Major &&
releaseVer.Minor == rommVer.Minor &&
releaseVer.Patch == rommVer.Patch {
gaba.GetLogger().Debug("found matching release for RomM version",
"rommVersion", rommVersion, "release", release.TagName)
return &release, nil
}
}
return nil, fmt.Errorf("no Grout release found matching RomM version %d.%d.%d",
rommVer.Major, rommVer.Minor, rommVer.Patch)
}
+33 -4
View File
@@ -5,12 +5,14 @@ import (
"grout/cfw"
"grout/internal"
"grout/internal/constants"
"grout/romm"
"grout/version"
"io"
"net/http"
"os"
"path/filepath"
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
"go.uber.org/atomic"
)
@@ -32,7 +34,10 @@ func GetAssetName(c cfw.CFW) string {
}
}
func CheckForUpdate(c cfw.CFW, releaseChannel internal.ReleaseChannel) (*Info, error) {
// CheckForUpdate checks for available updates based on the release channel.
// For ReleaseChannelMatchRomM, the host parameter is required to fetch the RomM version.
// For other channels, the host parameter is optional and ignored.
func CheckForUpdate(c cfw.CFW, releaseChannel internal.ReleaseChannel, host *romm.Host) (*Info, error) {
currentVersion := version.Get().Version
if currentVersion == "dev" {
@@ -42,9 +47,33 @@ func CheckForUpdate(c cfw.CFW, releaseChannel internal.ReleaseChannel) (*Info, e
}, nil
}
release, err := FetchLatestRelease(releaseChannel)
if err != nil {
return nil, fmt.Errorf("failed to check for updates: %w", err)
var release *GitHubRelease
var err error
if releaseChannel == internal.ReleaseChannelMatchRomM {
if host == nil {
return nil, fmt.Errorf("host is required for Match RomM release channel")
}
// Fetch RomM version from heartbeat
client := romm.NewClientFromHost(*host)
heartbeat, err := client.GetHeartbeat()
if err != nil {
return nil, fmt.Errorf("failed to get RomM version: %w", err)
}
gaba.GetLogger().Debug("fetched RomM version for update check", "version", heartbeat.System.Version)
// Find a Grout release matching the RomM version
release, err = FetchReleaseForRomMVersion(heartbeat.System.Version)
if err != nil {
return nil, fmt.Errorf("failed to find matching release: %w", err)
}
} else {
release, err = FetchLatestRelease(releaseChannel)
if err != nil {
return nil, fmt.Errorf("failed to check for updates: %w", err)
}
}
info := &Info{
+1 -1
View File
@@ -58,7 +58,7 @@ func TestCompareVersions(t *testing.T) {
{"2.0.0", "1.0.0", 1, "major version older"},
{"1.0.0", "1.0.0", 0, "same version"},
// Beta vs full release - THE KEY FIX
// Beta vs full release
{"v1.2.0-beta.1", "v1.2.0", -1, "beta should recognize full release as newer"},
{"v1.2.0", "v1.2.0-beta.1", 1, "full release should be newer than beta"},
{"1.2.0-beta.1", "1.2.0", -1, "beta should recognize full release as newer (no v prefix)"},
+7 -1
View File
@@ -1,5 +1,7 @@
package version
import "os"
var (
Version = "dev"
GitCommit = "unknown"
@@ -13,8 +15,12 @@ type BuildInfo struct {
}
func Get() BuildInfo {
v := Version
if override := os.Getenv("GROUT_VERSION"); override != "" {
v = override
}
return BuildInfo{
Version: Version,
Version: v,
GitCommit: GitCommit,
BuildDate: BuildDate,
}