appservices_version_bump.py (6769B)
1 #!/usr/bin/env python3 2 3 # This Source Code Form is subject to the terms of the Mozilla Public 4 # License, v. 2.0. If a copy of the MPL was not distributed with this 5 # file, You can obtain one at http://mozilla.org/MPL/2.0/ 6 7 # Helper script for mach vendor / updatebot to update the application-services 8 # version pin in ApplicationServices.kt to the latest available 9 # application-services nightly build. 10 # 11 # This script was adapted from https://github.com/mozilla-mobile/relbot/ 12 13 import datetime 14 import logging 15 import os 16 import re 17 from urllib.parse import quote_plus 18 19 import requests 20 from mozbuild.vendor.host_base import BaseHost 21 22 log = logging.getLogger(__name__) 23 logging.basicConfig( 24 format="%(asctime)s - %(name)s.%(funcName)s:%(lineno)s - %(levelname)s - %(message)s", # noqa E501 25 level=logging.INFO, 26 ) 27 28 29 def get_contents(path): 30 with open(path) as f: 31 return f.read() 32 33 34 def write_contents(path, new_content): 35 with open(path, "w") as f: 36 f.write(new_content) 37 38 39 def get_app_services_version_path(): 40 """Return the file path to dependencies file""" 41 p = "ApplicationServices.kt" 42 if os.path.exists(p): 43 return p 44 return "mobile/android/android-components/plugins/dependencies/src/main/java/ApplicationServices.kt" 45 46 47 def taskcluster_indexed_artifact_url(index_name, artifact_path): 48 artifact_path = quote_plus(artifact_path) 49 return ( 50 "https://firefox-ci-tc.services.mozilla.com/" 51 f"api/index/v1/task/{index_name}/artifacts/{artifact_path}" 52 ) 53 54 55 def validate_as_version(v): 56 """Validate that v is in an expected format for an app-services version. Returns v or raises an exception.""" 57 58 match = re.match(r"(^\d+)\.\d+$", v) 59 if match: 60 # Application-services switched to following the 2-component the 61 # Firefox version number in v114 62 if int(match.group(1)) >= 114: 63 return v 64 raise Exception(f"Invalid version format {v}") 65 66 67 def validate_as_channel(c): 68 """Validate that c is a valid app-services channel.""" 69 if c in ("staging", "nightly_staging"): 70 # These are channels are valid, but only used for preview builds. We don't have 71 # any way of auto-updating them 72 raise Exception(f"Can't update app-services channel {c}") 73 if c not in ("release", "nightly"): 74 raise Exception(f"Invalid app-services channel {c}") 75 return c 76 77 78 def get_current_as_version(): 79 """Return the current nightly app-services version""" 80 regex = re.compile(r'val VERSION = "([\d\.]+)"', re.MULTILINE) 81 82 path = get_app_services_version_path() 83 src = get_contents(path) 84 match = regex.search(src) 85 if match: 86 return validate_as_version(match[1]) 87 raise Exception( 88 f"Could not find application-services version in {os.path.basename(path)}" 89 ) 90 91 92 def match_as_channel(src): 93 """ 94 Find the ApplicationServicesChannel channel in the contents of the given 95 ApplicationServices.kt file. 96 """ 97 match = re.compile( 98 r"val CHANNEL = ApplicationServicesChannel." 99 r"(NIGHTLY|NIGHTLY_STAGING|STAGING|RELEASE)", 100 re.MULTILINE, 101 ).search(src) 102 if match: 103 return validate_as_channel(match[1].lower()) 104 raise Exception("Could not match the channel in ApplicationServices.kt") 105 106 107 def get_current_as_channel(): 108 """Return the current app-services channel""" 109 content = get_contents(get_app_services_version_path()) 110 return match_as_channel(content) 111 112 113 def get_as_nightly_json(version="latest"): 114 r = requests.get( 115 taskcluster_indexed_artifact_url( 116 f"project.application-services.v2.nightly.{version}", 117 "public/build/nightly.json", 118 ) 119 ) 120 r.raise_for_status() 121 return r.json() 122 123 124 def compare_as_versions(a, b): 125 # Tricky cmp()-style function for application services versions. Note that 126 # this works with both 2-component versions and 3-component ones, Since 127 # python compares tuples element by element. 128 a = tuple(int(x) for x in validate_as_version(a).split(".")) 129 b = tuple(int(x) for x in validate_as_version(b).split(".")) 130 return (a > b) - (a < b) 131 132 133 def update_as_version(old_as_version, new_as_version): 134 """Update the VERSION in ApplicationServices.kt""" 135 path = get_app_services_version_path() 136 current_version_string = f'val VERSION = "{old_as_version}"' 137 new_version_string = f'val VERSION = "{new_as_version}"' 138 log.info(f"Updating app-services version in {path}") 139 140 content = get_contents(path) 141 new_content = content.replace(current_version_string, new_version_string) 142 if content == new_content: 143 raise Exception( 144 "Update to ApplicationServices.kt resulted in no changes: " 145 "maybe the file was already up to date?" 146 ) 147 148 write_contents(path, new_content) 149 150 151 def update_application_services(revision): 152 """Find the app-services nightly build version corresponding to revision; 153 if it is newer than the current version in ApplicationServices.kt, then 154 update ApplicationServices.kt with the newer version number.""" 155 as_channel = get_current_as_channel() 156 log.info(f"Current app-services channel is {as_channel}") 157 if as_channel != "nightly": 158 raise NotImplementedError( 159 "Only the app-services nightly channel is currently supported" 160 ) 161 162 current_as_version = get_current_as_version() 163 log.info( 164 f"Current app-services {as_channel.capitalize()} version is {current_as_version}" 165 ) 166 167 json = get_as_nightly_json(f"revision.{revision}") 168 target_as_version = json["version"] 169 log.info( 170 f"Target app-services {as_channel.capitalize()} version is {target_as_version}" 171 ) 172 173 if compare_as_versions(current_as_version, target_as_version) >= 0: 174 log.warning( 175 f"No newer app-services {as_channel.capitalize()} release found. Exiting." 176 ) 177 return 178 179 dry_run = os.getenv("DRY_RUN") == "True" 180 if dry_run: 181 log.warning("Dry-run so not continuing.") 182 return 183 184 update_as_version( 185 current_as_version, 186 target_as_version, 187 ) 188 189 190 class ASHost(BaseHost): 191 def upstream_tag(self, revision): 192 if revision == "HEAD": 193 index = "latest" 194 else: 195 index = f"revision.{revision}" 196 json = get_as_nightly_json(index) 197 timestamp = json["version"].rsplit(".", 1)[1] 198 return ( 199 json["commit"], 200 datetime.datetime.strptime(timestamp, "%Y%m%d%H%M%S").isoformat(), 201 ) 202 203 204 def main(): 205 import sys 206 207 if len(sys.argv) != 2: 208 print(f"Usage: {sys.argv[0]} <commit hash>", file=sys.stderr) 209 sys.exit(1) 210 211 update_application_services(sys.argv[1]) 212 213 214 if __name__ == "__main__": 215 main()