[infra] Promote beta to stable when stable is ahead of beta.

Don't promote older releases to latest if promoted out of order.

Fixes: b/227744513
Change-Id: I954c8620897e59ed3caa459e0bd22e446e38d02d
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/283622
Commit-Queue: Jonas Termansen <sortie@google.com>
Reviewed-by: Alexander Thomas <athom@google.com>
diff --git a/tools/promote.py b/tools/promote.py
index 2f214c8..e784dff 100755
--- a/tools/promote.py
+++ b/tools/promote.py
@@ -6,6 +6,7 @@
 
 # Dart SDK promote tools.
 
+import json
 import optparse
 import os
 import sys
@@ -14,6 +15,7 @@
 import bots.bot_utils as bot_utils
 
 from os.path import join
+from utils import Version
 
 DART_PATH = os.path.abspath(os.path.join(__file__, '..', '..'))
 DRY_RUN = False
@@ -93,6 +95,18 @@
         _PromoteDartArchiveBuild(options.channel, source, options.revision)
 
 
+def GetLatestRelease(channel):
+    release_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.RELEASE)
+    version_object = release_namer.version_filepath('latest')
+    global DRY_RUN
+    was_dry = DRY_RUN
+    DRY_RUN = False
+    (stdout, _, _) = Gsutil(['cat', version_object])
+    DRY_RUN = was_dry
+    version = json.loads(stdout)['version']
+    return Version(version=version)
+
+
 def UpdateDocs():
     try:
         print('Updating docs')
@@ -114,7 +128,6 @@
     release_namer = bot_utils.GCSNamer(channel, bot_utils.ReleaseType.RELEASE)
 
     def promote(to_revision):
-
         def safety_check_on_gs_path(gs_path, revision, channel):
             if not (revision != None and len(channel) > 0 and
                     ('%s' % revision) in gs_path and channel in gs_path):
@@ -182,7 +195,13 @@
         Gsutil(no_cache + ['cp', from_loc, to_loc])
 
     promote(revision)
-    promote('latest')
+    # Promote to latest unless it's an older version.
+    if GetLatestRelease(channel) <= Version(version=revision):
+        promote('latest')
+    # Promote beta to stable if stable becomes ahead of beta.
+    if channel == 'stable' and \
+       GetLatestRelease('beta') <= Version(version=revision):
+        _PromoteDartArchiveBuild('beta', 'stable', revision)
 
 
 def Gsutil(cmd, throw_on_error=True):
diff --git a/tools/utils.py b/tools/utils.py
index aab6417..ce932b3 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -9,6 +9,7 @@
 
 import contextlib
 import datetime
+from functools import total_ordering
 import glob
 import imp
 import json
@@ -28,6 +29,8 @@
 except:
     pass
 
+SEMANTIC_VERSION_PATTERN = r'^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
+
 
 # To eliminate clashing with older archived builds on bleeding edge we add
 # a base number bigger the largest svn revision (this also gives us an easy
@@ -120,16 +123,88 @@
                            os.path.join(repo_path, 'tools', 'minidump.py'))
 
 
+@total_ordering
 class Version(object):
 
-    def __init__(self, channel, major, minor, patch, prerelease,
-                 prerelease_patch):
+    def __init__(self,
+                 channel=None,
+                 major=None,
+                 minor=None,
+                 patch=None,
+                 prerelease=None,
+                 prerelease_patch=None,
+                 version=None):
         self.channel = channel
         self.major = major
         self.minor = minor
         self.patch = patch
         self.prerelease = prerelease
         self.prerelease_patch = prerelease_patch
+        if version:
+            self.set_version(version)
+
+    def set_version(self, version):
+        match = re.match(SEMANTIC_VERSION_PATTERN, version)
+        assert match, '%s must be a valid version' % version
+        self.channel = 'stable'
+        self.major = match['major']
+        self.minor = match['minor']
+        self.patch = match['patch']
+        self.prerelease = '0'
+        self.prerelease_patch = '0'
+        if match['prerelease']:
+            subversions = match['prerelease'].split('.')
+            self.prerelease = subversions[0]
+            self.prerelease_patch = subversions[1]
+            self.channel = subversions[2]
+
+    def __str__(self):
+        result = '%s.%s.%s' % (self.major, self.minor, self.patch)
+        if self.channel != 'stable':
+            result += '-%s.%s.%s' % (self.prerelease, self.prerelease_patch,
+                                     self.channel)
+        return result
+
+    def __eq__(self, other):
+        return self.channel == other.channel and \
+               self.major == other.major and \
+               self.minor == other.minor and \
+               self.patch == other.patch and \
+               self.prerelease == other.prerelease and \
+               self.prerelease_patch == other.prerelease_patch
+
+    def __lt__(self, other):
+        if int(self.major) < int(other.major):
+            return True
+        if int(self.major) > int(other.major):
+            return False
+        if int(self.minor) < int(other.minor):
+            return True
+        if int(self.minor) > int(other.minor):
+            return False
+        if int(self.patch) < int(other.patch):
+            return True
+        if int(self.patch) > int(other.patch):
+            return False
+        # The stable channel is ahead of the other channels on the same triplet.
+        if self.channel != 'stable' and other.channel == 'stable':
+            return True
+        if self.channel == 'stable' and other.channel != 'stable':
+            return False
+        # The be channel is ahead of the other channels on the same triplet.
+        if self.channel != 'be' and other.channel == 'be':
+            return True
+        if self.channel == 'be' and other.channel != 'be':
+            return False
+        if int(self.prerelease_patch) < int(other.prerelease_patch):
+            return True
+        if int(self.prerelease_patch) > int(other.prerelease_patch):
+            return False
+        if int(self.prerelease) < int(other.prerelease):
+            return True
+        if int(self.prerelease) > int(other.prerelease):
+            return False
+        return False
 
 
 # Try to guess the host operating system.