@@ -19,6 +19,8 @@ package update
1919import (
2020 "errors"
2121 "fmt"
22+ "strconv"
23+ "strings"
2224 "time"
2325
2426 semver "github.com/Masterminds/semver/v3"
@@ -134,18 +136,200 @@ func isPatch(wantVersion *semver.Version, currentVersion *semver.Version) bool {
134136 return currentVersion .Major () == wantVersion .Major () && currentVersion .Minor () == wantVersion .Minor ()
135137}
136138
137- func isForwardOneMajorVersion (wantVersion * semver.Version , currentVersion * semver.Version ) bool {
139+ // getReleaseType returns if a release is Innovative or Regular.
140+ func getReleaseType (major , minor int ) ReleaseType {
141+ // Before 25.1, we define them explicitly
142+ switch fmt .Sprintf ("%d.%d" , major , minor ) {
143+ case "24.1" , "24.3" :
144+ return Regular
145+ case "24.2" :
146+ return Innovative
147+ }
148+
149+ // Post 25.1: Odd releases (1,3) are Innovative, Even (2,4) are Regular
150+ if minor % 2 == 1 {
151+ return Innovative
152+ }
153+ return Regular
154+ }
155+
156+ // generateReleases generates a list of release versions from a 24.1 to the specified year.
157+ // The releases follow a "YY.Q" format, where YY represents the last two digits of the year, and Q represents the quarter.
158+ //
159+ // Parameters:
160+ // - upToYear: The last two digits of the year up to which releases should be generated.
161+ //
162+ // Returns:
163+ // - A slice of strings containing release versions from "24.1" onward, up to the specified year, with four
164+ // releases per year (except 2024).
165+ func generateReleases (upToYear int ) []string {
166+ var releases = []string {"24.1" , "24.2" , "24.3" }
167+
168+ for year := 25 ; year <= upToYear ; year ++ {
169+ for quarter := 1 ; quarter <= 4 ; quarter ++ {
170+ releases = append (releases , fmt .Sprintf ("%d.%d" , year , quarter ))
171+ }
172+ }
173+
174+ return releases
175+ }
176+
177+ // getNextReleases returns a list of possible upgradable release versions following the given currentVersion.
178+ // It iterates through the generated release versions for the current year and collects the next releases
179+ // after finding the currentVersion. The function stops collecting releases once it encounters the next regular release.
180+ //
181+ // Parameters:
182+ // - currentVersion: The current release version in the format "YY.Q" (e.g., "24.1").
183+ //
184+ // Returns:
185+ // - A slice of strings containing the possible upgradable release versions, stopping at the next regular release.
186+ func getNextReleases (currentVersion string ) []string {
187+ var nextReleases []string
188+ var found bool
189+
190+ releases := generateReleases (time .Now ().Year () % 100 )
191+ for _ , release := range releases {
192+ year , _ := strconv .Atoi (strings .Split (release , "." )[0 ])
193+ quarter , _ := strconv .Atoi (strings .Split (release , "." )[1 ])
194+
195+ if found {
196+ nextReleases = append (nextReleases , release )
197+ if getReleaseType (year , quarter ) == Regular {
198+ break // Stop at the next regular release
199+ }
200+ }
201+ if release == currentVersion {
202+ found = true
203+ }
204+ }
205+
206+ return nextReleases
207+ }
208+
209+ // getPreviousReleases returns a list of possible rollback release versions preceding the given currentVersion.
210+ // It iterates through the generated release versions in reverse order and collects previous releases
211+ // until it encounters the last regular release before the currentVersion.
212+ //
213+ // Parameters:
214+ // - currentVersion: The current release version in the format "YY.Q" (e.g., "24.1").
215+ //
216+ // Returns:
217+ // - A slice of strings containing possible rollback release versions, stopping at the last regular release before the currentVersion.
218+ func getPreviousReleases (currentVersion string ) []string {
219+ var prevReleases []string
220+ var found bool
221+
222+ releases := generateReleases (time .Now ().Year () % 100 )
223+ for i := len (releases ) - 1 ; i >= 0 ; i -- {
224+ release := releases [i ]
225+ year , _ := strconv .Atoi (strings .Split (release , "." )[0 ])
226+ quarter , _ := strconv .Atoi (strings .Split (release , "." )[1 ])
227+ if found {
228+ prevReleases = append (prevReleases , release )
229+ if getReleaseType (year , quarter ) == Regular {
230+ break
231+ }
232+ }
233+ if release == currentVersion {
234+ found = true
235+ }
236+ }
237+
238+ return prevReleases
239+ }
240+
241+ // isMajorUpgradeAllowed determines whether an upgrade from the current version to the desired version is allowed
242+ // based on the release cycle and upgrade policies.
243+ //
244+ // Upgrade Rules:
245+ // 1. For versions prior to 2024:
246+ // - A major upgrade is allowed if
247+ // - The major version remains the same and the minor version increments by 1 (e.g., 19.1 -> 19.2).
248+ // - The major version increments by 1 and the minor version resets to 1. (e.g., 19.2 -> 20.1).
249+ //
250+ // 2. For version of 2024:
251+ // - There are 3 releases (24.1, 24.2, 24.3) where 24.2 being innovative release
252+ //
253+ // 3. For versions from 2025 onwards:
254+ // - Releases follow a quarterly cycle, with some releases designated as innovative.
255+ // - Users are allowed to skip upgrading to an innovative release.
256+ // - The upgrade is permitted if the desired version matches one of the next valid releases.
257+ //
258+ // Parameters:
259+ // - wantVersion: The target version to which an upgrade is requested.
260+ // - currentVersion: The currently installed version.
261+ //
262+ // Returns:
263+ // - true if the upgrade is allowed based on the release rules; false otherwise.
264+ func isMajorUpgradeAllowed (wantVersion * semver.Version , currentVersion * semver.Version ) bool {
138265 // Two cases:
139266 // 19.1 to 19.2 -> same year
140267 // 19.2 to 20.1 -> next year
268+
269+ // Since 2025, we have adopted a quarterly release cycle, with two of the four annual releases designated
270+ // as innovative releases. Users have the option to skip upgrading to an innovative release.
271+ // For 2024, we just had 3 releases overall, with 24.2 as the innovation release.
272+ if currentVersion .Major () >= 24 {
273+ // Four Cases:
274+ // 24.1 to 24.2 -> Same year without skipping innovative release
275+ // 24.1 to 24.3 -> Same year with skipping innovative release
276+ // 24.3 to 25.1 -> Next year without skipping innovative release
277+ // 24.3 to 25.2 -> Next year with skipping innovative release
278+ nextPossibleRelease := getNextReleases (fmt .Sprintf ("%d.%d" , currentVersion .Major (), currentVersion .Minor ()))
279+ for _ , version := range nextPossibleRelease {
280+ if version == fmt .Sprintf ("%d.%d" , wantVersion .Major (), wantVersion .Minor ()) {
281+ return true
282+ }
283+ }
284+
285+ return false
286+ }
287+
141288 return (currentVersion .Major () == wantVersion .Major () && currentVersion .Minor ()+ 1 == wantVersion .Minor ()) ||
142289 (currentVersion .Major ()+ 1 == wantVersion .Major () && currentVersion .Minor ()- 1 == wantVersion .Minor ())
143290}
144291
145- func isBackOneMajorVersion (wantVersion * semver.Version , currentVersion * semver.Version ) bool {
292+ // isMajorRollbackAllowed determines whether rolling back from the current version to the desired version is allowed
293+ // based on the release cycle and rollback policies.
294+ //
295+ // Rollback Rules:
296+ // 1. For versions prior to 2024:
297+ // - A rollback is allowed if:
298+ // - The major version remains the same and the minor version decrements by 1 (e.g., 19.2 -> 19.1).
299+ // - The major version decrements by 1 and the minor version is the last release. (e.g., 20.1 -> 19.2).
300+ //
301+ // 2. For versions from 2024 onwards:
302+ // - Users can skip rolling back to an innovative release.
303+ // - The rollback is permitted if the desired version matches one of the valid previous releases.
304+ //
305+ // Parameters:
306+ // - wantVersion: The target version to which a rollback is requested.
307+ // - currentVersion: The currently installed version.
308+ //
309+ // Returns:
310+ // - true if the rollback is allowed based on the rollback policies; false otherwise.
311+ func isMajorRollbackAllowed (wantVersion * semver.Version , currentVersion * semver.Version ) bool {
146312 // Two cases:
147313 // 19.2 to 19.1 -> same year
148314 // 20.1 to 19.2 -> previous year
315+
316+ // Since 2024, users have the option to skip rollback to an innovative release.
317+ if wantVersion .Major () >= 24 {
318+ // Four cases:
319+ // 24.2 -> 24.1 -> Same year without skipping innovative release
320+ // 24.3 -> 24.1 -> Same year with skipping innovative release
321+ // 25.1 -> 24.3 -> Previous year without skipping innovative release
322+ // 25.2 -> 24.3 -> Previous year with skipping innovative release
323+ rollbackReleases := getPreviousReleases (fmt .Sprintf ("%d.%d" , currentVersion .Major (), currentVersion .Minor ()))
324+ for _ , version := range rollbackReleases {
325+ if version == fmt .Sprintf ("%d.%d" , wantVersion .Major (), wantVersion .Minor ()) {
326+ return true
327+ }
328+ }
329+
330+ return false
331+ }
332+
149333 return (currentVersion .Major () == wantVersion .Major () && currentVersion .Minor () == wantVersion .Minor ()+ 1 ) ||
150334 (currentVersion .Major () == wantVersion .Major ()+ 1 && currentVersion .Minor () == wantVersion .Minor ()- 1 )
151335}
0 commit comments