@@ -14,6 +14,8 @@ import (
1414 "regexp"
1515 "runtime"
1616 "strings"
17+
18+ "tailscale.com/util/cmpver"
1719)
1820
1921const androidGitHubRepoURL = "https://api.github.com/repos/anasfanani/tailscale-magisk-build/releases"
@@ -31,17 +33,20 @@ func (up *Updater) updateAndroid() error {
3133 repoURL := androidGitHubRepoURL
3234 if up .Version != "" {
3335 repoURL = fmt .Sprintf (repoURL + "/tags/v%s-android" , up .Version )
36+ } else if up .Track == UnstableTrack {
37+ // For unstable track, fetch all releases to get the latest (including pre-releases)
38+ repoURL = androidGitHubRepoURL
3439 } else {
35- repoURL = fmt . Sprintf ( repoURL + "/latest" )
40+ repoURL = repoURL + "/latest"
3641 }
3742
3843 release , ver , err := fetchAndroidRelease (repoURL , up .Track )
3944 if err != nil {
4045 return err
4146 }
4247
43- // Confirm the update with the user
44- if ! up . confirm ( ver ) {
48+ // Confirm the update with the user (allow downgrades when specific version is requested)
49+ if ! confirmAndroidUpdate ( up , ver ) {
4550 return nil
4651 }
4752
@@ -69,6 +74,33 @@ func (up *Updater) updateAndroid() error {
6974 return nil
7075}
7176
77+ // confirmAndroidUpdate confirms the update with the user, allowing downgrades when a specific version is requested
78+ func confirmAndroidUpdate (up * Updater , ver string ) bool {
79+ // Allow downgrades when a specific version is requested
80+ if up .Version != "" {
81+ if up .Confirm != nil {
82+ return up .Confirm (ver )
83+ }
84+ return true
85+ }
86+
87+ // Only check version when we're not switching tracks.
88+ if up .Track == "" || up .Track == CurrentTrack {
89+ switch c := cmpver .Compare (up .currentVersion , ver ); {
90+ case c == 0 :
91+ up .Logf ("already running %v version %v; no update needed" , up .Track , ver )
92+ return false
93+ case c > 0 :
94+ up .Logf ("installed %v version %v is newer than the latest available version %v; no update needed" , up .Track , up .currentVersion , ver )
95+ return false
96+ }
97+ }
98+ if up .Confirm != nil {
99+ return up .Confirm (ver )
100+ }
101+ return true
102+ }
103+
72104// androidDirectories returns the download and extract directories for Android updates
73105// downloadDir: uses TempDir, fallback to current working directory
74106// extractDir: uses the directory of the current executable
@@ -135,8 +167,35 @@ func fetchAndroidRelease(repoURL, track string) (release struct {
135167 return release , "" , fmt .Errorf ("unexpected status code: %d" , resp .StatusCode )
136168 }
137169
138- if err := json .NewDecoder (resp .Body ).Decode (& release ); err != nil {
139- return release , "" , fmt .Errorf ("failed to decode release metadata: %w" , err )
170+ // Check if we're fetching all releases (for unstable track)
171+ if strings .HasSuffix (repoURL , "/releases" ) {
172+ // Fetch all releases and find the latest one (including pre-releases)
173+ var releases []struct {
174+ TagName string `json:"tag_name"`
175+ Assets []struct {
176+ Name string `json:"name"`
177+ URL string `json:"browser_download_url"`
178+ } `json:"assets"`
179+ Prerelease bool `json:"prerelease"`
180+ }
181+
182+ if err := json .NewDecoder (resp .Body ).Decode (& releases ); err != nil {
183+ return release , "" , fmt .Errorf ("failed to decode releases metadata: %w" , err )
184+ }
185+
186+ if len (releases ) == 0 {
187+ return release , "" , fmt .Errorf ("no releases found" )
188+ }
189+
190+ // Get the first release (latest)
191+ release .TagName = releases [0 ].TagName
192+ release .Assets = releases [0 ].Assets
193+ release .Prerelease = releases [0 ].Prerelease
194+ } else {
195+ // Single release (latest or specific tag)
196+ if err := json .NewDecoder (resp .Body ).Decode (& release ); err != nil {
197+ return release , "" , fmt .Errorf ("failed to decode release metadata: %w" , err )
198+ }
140199 }
141200
142201 // If fetching latest and it's a pre-release, skip it for stable track
0 commit comments