PageRenderTime 31ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/eng/versioning/set_versions.py

http://github.com/WindowsAzure/azure-sdk-for-java
Python | 443 lines | 270 code | 52 blank | 121 comment | 108 complexity | bf6bac209e11baac03f3306260f76b3b MD5 | raw file
Possible License(s): MIT
  1. # Copyright (c) Microsoft Corporation. All rights reserved.
  2. # Licensed under the MIT License.
  3. # Python version 3.4 or higher is required to run this script.
  4. # Nobody outside of the EngSys team should actually be using this script which is primarily
  5. # used for version verification and manipulation in the pipeline automation.
  6. # Use case: Append the build qualifier onto the existing version in such a way that the
  7. # resulting version string is still in semver format. This will be utilized by the build
  8. # system to modify the version string to produce nightly DevOps builds.
  9. #
  10. # It's worth noting that this only manipulates the version in the appropriate version_*.txt file.
  11. # which has the format <groupId>:<artifactId>;<dependencyVersion>;<currentVersion>.
  12. # Selecting dependency for the update type will only update the dependency version. Similarly,
  13. # selecting current will only update the current. Selecting both will update both the dependency
  14. # and the current versions.
  15. #
  16. #
  17. # python eng/versioning/set_versions.py --ut [library|external_dependency|all] --bt [client|data|management] --bq <BuildQualifierString> --ar <artifactId>
  18. #
  19. # Use case: increment the version of a given artifact in the approprate version_[client|data|management].txt file
  20. #
  21. # python eng/versioning/set_versions.py --bt [client|data|management] --increment-version --artifact-id <artifactId>
  22. # For example: To increment the version of azure-core
  23. # python eng/versioning/set_versions.py --bt client --iv --ar azure-core
  24. #
  25. # Use case: verify the version of a given artifact in the approprate version_[client|data|management].txt file
  26. #
  27. # python eng/versioning/set_versions.py --bt [client|data|management] --verify-version --artifact-id <artifactId>
  28. # For example: To verify the version of azure-core
  29. # python eng/versioning/set_versions.py --bt client --vv --ar azure-core
  30. #
  31. # The script must be run at the root of azure-sdk-for-java.
  32. import argparse
  33. from datetime import timedelta
  34. import os
  35. import re
  36. import sys
  37. import time
  38. from utils import BuildType
  39. from utils import CodeModule
  40. from utils import UpdateType
  41. from utils import version_regex_str_with_names_anchored
  42. from utils import prerelease_data_version_regex
  43. from utils import prerelease_version_regex_with_name
  44. # some things that should not be updated for devops builds, in the case where everything is being updated in one call
  45. items_we_should_not_update = ['com.azure:azure-sdk-all', 'com.azure:azure-sdk-parent', 'com.azure:azure-client-sdk-parent', 'azure-data-sdk-parent']
  46. # The regex string we want should be the anchored one since the entire string is what's being matched
  47. version_regex_named = re.compile(version_regex_str_with_names_anchored)
  48. prerelease_regex_named = re.compile(prerelease_version_regex_with_name)
  49. prerelease_data_regex = re.compile(prerelease_data_version_regex)
  50. # Update packages (excluding unreleased dependencies and packages which already
  51. # have a dev version set) to use a "zero dev version" (e.g. alpha.20201225.0).
  52. # This ensures that packages built in pipelines who have unreleased dependencies
  53. # that are built in other pipelines can successfully fall back to a source build
  54. # of the unreleased dependency package in the monorepo if the unreleased
  55. # dependency has not been published to the dev feed yet. This makes it so
  56. # nightly pipelines do not have to coordinate to publish packages to the dev
  57. # feed in dependency order.
  58. # This function assumes that the version file has already been updated with dev
  59. # versions for the appropriate target packages.
  60. def set_dev_zero_version(build_type, build_qualifier):
  61. version_file = os.path.normpath('eng/versioning/version_' + build_type.name + '.txt')
  62. print('version_file=' + version_file)
  63. # Assuming a build qualifier of the form: "alpha.20200204.123"
  64. # Converts "alpha.20200204.123" -> "alpha.20200204.0"
  65. zero_qualifier = build_qualifier[:build_qualifier.rfind('.') + 1] + '0'
  66. newlines = []
  67. with open(version_file, encoding='utf8') as f:
  68. for raw_line in f:
  69. stripped_line = raw_line.strip()
  70. if not stripped_line or stripped_line.startswith('#'):
  71. newlines.append(raw_line)
  72. continue
  73. module = CodeModule(stripped_line)
  74. if module.name in items_we_should_not_update:
  75. newlines.append(module.string_for_version_file())
  76. continue
  77. if module.name.startswith('unreleased_') or module.name.startswith('beta_'):
  78. newlines.append(module.string_for_version_file())
  79. continue
  80. if hasattr(module, 'current'):
  81. if 'alpha' in module.current:
  82. newlines.append(module.string_for_version_file())
  83. continue
  84. set_both = module.current == module.dependency
  85. if '-' in module.current:
  86. # if the module is 1.2.3-beta.x, strip off everything after the '-' and add the qualifier
  87. module.current = module.current[:module.current.rfind('-') + 1] + zero_qualifier
  88. else:
  89. # if the module is a GA version 1.2.3, add '-' and the qualifier
  90. module.current += '-' + zero_qualifier
  91. # The resulting version must be a valid SemVer
  92. match = version_regex_named.match(module.current)
  93. if not match:
  94. raise ValueError('{}\'s current version + build qualifier {} is not a valid semver version'.format(module.name, module.current + build_qualifier))
  95. if set_both:
  96. module.dependency = module.current
  97. print(f'updating {module.name} to use dependency={module.dependency}, current={module.current}')
  98. newlines.append(module.string_for_version_file())
  99. with open(version_file, 'w', encoding='utf-8') as f:
  100. for line in newlines:
  101. f.write(line)
  102. def update_versions_file_for_nightly_devops(build_type, build_qualifier, artifact_id, group_id):
  103. version_file = os.path.normpath('eng/versioning/version_' + build_type.name + '.txt')
  104. print('version_file=' + version_file)
  105. library_to_update = group_id + ':' + artifact_id
  106. print('adding build_qualifier({}) to {}'.format(build_qualifier, library_to_update))
  107. version_map = {}
  108. newlines = []
  109. artifact_found = False
  110. with open(version_file, encoding='utf-8') as f:
  111. for raw_line in f:
  112. stripped_line = raw_line.strip()
  113. if not stripped_line or stripped_line.startswith('#'):
  114. newlines.append(raw_line)
  115. else:
  116. module = CodeModule(stripped_line)
  117. # basically we don't want to change any of the parent versions here, only
  118. # library versions
  119. if module.name in items_we_should_not_update:
  120. newlines.append(module.string_for_version_file())
  121. continue
  122. if library_to_update == module.name:
  123. artifact_found = True
  124. if hasattr(module, 'current'):
  125. set_both = False
  126. # In the case where the current and dependency are both equal then both
  127. # need to be updated. In theory, this should only really happen when a
  128. # new library has been added and has not yet been released but can also
  129. # happen if a library has just been released and the devops build kicks
  130. # of before the human submits the PR to increase the current version. Being
  131. # that it's a devops build and both versions are being updated this should
  132. # be OK.
  133. if module.current == module.dependency:
  134. set_both = True
  135. if '-' in module.current:
  136. # if the module is 1.2.3-beta.x, strip off everything after the '-' and add the qualifier
  137. module.current = module.current[:module.current.rfind('-') + 1] + build_qualifier
  138. else:
  139. module.current += '-' + build_qualifier
  140. match = version_regex_named.match(module.current)
  141. if not match:
  142. raise ValueError('{}\'s current version + build qualifier {} is not a valid semver version'.format(module.name, module.current + build_qualifier))
  143. if set_both:
  144. module.dependency = module.current
  145. # If the library is not the update target and is unreleased, use
  146. # a version range based on the build qualifier as the dependency
  147. # version so that packages from this build that are released to
  148. # the dev feed will depend on a version that should be found in
  149. # the dev feed.
  150. # This script runs once for each artifact so it makes no
  151. # changes in the case where a dependency version has already
  152. # been modified.
  153. elif (module.name.startswith('unreleased_') or module.name.startswith('beta_')) and not module.dependency.startswith('['):
  154. # Assuming a build qualifier of the form: "alpha.20200204.1"
  155. # Converts "alpha.20200204.1" -> "alpha"
  156. unreleased_build_identifier = build_qualifier[:build_qualifier.find('.')]
  157. # Converts "alpha.20200204.1" -> "20200204.1"
  158. unreleased_build_date = build_qualifier[build_qualifier.find('.')+1:][:8]
  159. if '-' in module.dependency:
  160. # if the module is 1.2.3-beta.x, strip off everything after the '-'
  161. module.dependency = module.dependency[:module.dependency.rfind('-')]
  162. # The final unreleased dependency version needs to be of the form
  163. # [1.0.0-alpha.YYYYMMDD,1.0.0-alpha.99999999] this will keep it in alpha version of the same major/minor/patch range to avoid potential future breaks
  164. module.dependency = '[{0}-{1}.{2},{0}-{1}.{3}]'.format(module.dependency, unreleased_build_identifier, unreleased_build_date, "99999999")
  165. print(f'updating unreleased/beta dependency {module.name} to use dependency version range: "{module.dependency}"')
  166. version_map[module.name] = module
  167. newlines.append(module.string_for_version_file())
  168. if not artifact_found:
  169. raise ValueError('library_to_update ({}) was not found in version file {}'.format(library_to_update, version_file))
  170. with open(version_file, 'w', encoding='utf-8') as f:
  171. for line in newlines:
  172. f.write(line)
  173. # Prep the appropriate version file for source testing. What this really means is set the
  174. # all of the dependency versions to the current versions. This will effectively cause maven
  175. # to use the built version of the libraries for build and testing. The purpose of this is to
  176. # ensure current version compatibility amongst the various libraries for a given built type
  177. def prep_version_file_for_source_testing(build_type):
  178. version_file = os.path.normpath('eng/versioning/version_' + build_type.name + '.txt')
  179. print('version_file=' + version_file)
  180. file_changed = False
  181. # The version map is needed to get the 'current' version of any beta dependencies
  182. # in order to update the beta_ version in the From Source runs
  183. version_map = {}
  184. newlines = []
  185. with open(version_file, encoding='utf-8') as f:
  186. for raw_line in f:
  187. stripped_line = raw_line.strip()
  188. if not stripped_line or stripped_line.startswith('#'):
  189. newlines.append(raw_line)
  190. else:
  191. module = CodeModule(stripped_line)
  192. if hasattr(module, 'current') and not module.current == module.dependency:
  193. module.dependency = module.current
  194. file_changed = True
  195. # In order to ensure that the From Source runs are effectively testing everything
  196. # together using the latest source built libraries, ensure that the beta_ dependency's
  197. # version is set
  198. elif module.name.startswith('beta_'):
  199. tempName = module.name[len('beta_'):]
  200. if tempName in version_map:
  201. # beta_ tags only have a dependency version, set that to
  202. # the current version of the non-beta dependency
  203. module.dependency = version_map[tempName].current
  204. file_changed = True
  205. else:
  206. # if the beta_ dependency doesn't have a non-beta entry in the version file then this is an error
  207. raise ValueError('prep_version_file_for_source_testing: beta library ({}) does not have a non-beta entry {} in version file {}'.format(module.name, tempName, version_file))
  208. version_map[module.name] = module
  209. newlines.append(module.string_for_version_file())
  210. with open(version_file, 'w', encoding='utf-8') as f:
  211. for line in newlines:
  212. f.write(line)
  213. return file_changed
  214. # given a build type, artifact id and group id, set the dependency version to the
  215. # current version and increment the current version
  216. def increment_or_set_library_version(build_type, artifact_id, group_id, new_version=None):
  217. version_file = os.path.normpath('eng/versioning/version_' + build_type.name + '.txt')
  218. print('version_file=' + version_file)
  219. library_to_update = group_id + ':' + artifact_id
  220. artifact_found = False
  221. newlines = []
  222. with open(version_file, encoding='utf-8') as f:
  223. for raw_line in f:
  224. stripped_line = raw_line.strip()
  225. if not stripped_line or stripped_line.startswith('#'):
  226. newlines.append(raw_line)
  227. else:
  228. module = CodeModule(stripped_line)
  229. # Tick up the version here. If the version is already a pre-release
  230. # version then just increment the revision. Otherwise increment the
  231. # minor version, zero the patch and add "-beta.1" to the end
  232. # https://github.com/Azure/azure-sdk/blob/main/docs/policies/releases.md#java
  233. if module.name == library_to_update and hasattr(module, 'current'):
  234. artifact_found = True
  235. if new_version is None:
  236. vmatch = version_regex_named.match(module.current)
  237. if (vmatch.group('prerelease') is not None):
  238. prever = prerelease_regex_named.match(vmatch.group('prerelease'))
  239. # This is the case where, somehow, the versioning verification has failed and
  240. # the prerelease verification doesn't match "beta.X"
  241. if prever is None:
  242. # if the build_type isn't data then error
  243. if build_type.name.lower() != 'data':
  244. raise ValueError('library_to_update ({}:{}) has an invalid prerelease version ({}) which should be of the format beta.X'.format(library_to_update, module.current, vmatch.group('prerelease')))
  245. else:
  246. # verify that prerelease is "beta"
  247. if prerelease_data_regex.match(vmatch.group('prerelease')) is None:
  248. raise ValueError('library_to_update ({}:{}) has an invalid prerelease version ({}) which should be of the format (beta) or (beta.X)'.format(library_to_update, module.current, vmatch.group('prerelease')))
  249. # in the case there the prerelease version is just "beta", increment the minor and set the patch to 0
  250. minor = int(vmatch.group('minor'))
  251. minor += 1
  252. new_version = '{}.{}.{}-beta'.format(vmatch.group('major'), minor, 0)
  253. else:
  254. rev = int(prever.group('revision'))
  255. rev += 1
  256. new_version = '{}.{}.{}-beta.{}'.format(vmatch.group('major'), vmatch.group('minor'), vmatch.group('patch'), str(rev))
  257. else:
  258. minor = int(vmatch.group('minor'))
  259. minor += 1
  260. new_version = '{}.{}.{}-beta.1'.format(vmatch.group('major'), minor, 0)
  261. # The dependency version only needs to be updated it if is different from the current version.
  262. # This would be the case where a library hasn't been released yet and has been released (either GA or preview)
  263. if (module.dependency != module.current):
  264. vDepMatch = version_regex_named.match(module.dependency)
  265. # If the dependency version is a beta then just set it to whatever the current
  266. # version is
  267. if (vDepMatch.group('prerelease') is not None):
  268. print('library_to_update {}, previous dependency version={}, new dependency version={}'.format(library_to_update, module.dependency, module.current))
  269. module.dependency = module.current
  270. # else, the dependency version isn't a pre-release version
  271. else:
  272. # if the dependency version isn't a beta and the current version is, don't
  273. # update the dependency version
  274. if (vmatch.group('prerelease') is not None):
  275. print('library_to_update {}, has a GA dependency version {} and a beta current version {}. The dependency version will be kept at the GA version. '.format(library_to_update, module.dependency, module.current))
  276. else:
  277. print('library_to_update {}, has both GA dependency {} and current {} versions. The dependency will be updated to {}. '.format(library_to_update, module.dependency, module.current, module.current))
  278. module.dependency = module.current
  279. print('library_to_update {}, previous current version={}, new current version={}'.format(library_to_update, module.current, new_version))
  280. module.current = new_version
  281. newlines.append(module.string_for_version_file())
  282. if not artifact_found:
  283. raise ValueError('library_to_update ({}) was not found in version file {}'.format(library_to_update, version_file))
  284. with open(version_file, 'w', encoding='utf-8') as f:
  285. for line in newlines:
  286. f.write(line)
  287. # Verify that the current version of an artifact matches our versioning scheme. This is meant to be called
  288. # as part of the release pipeline for a given artifact to verify that we don't accidentally release a version
  289. # that doesn't match our versioning scheme
  290. def verify_current_version_of_artifact(build_type, artifact_id, group_id):
  291. version_file = os.path.normpath('eng/versioning/version_' + build_type.name + '.txt')
  292. print('version_file=' + version_file)
  293. library_to_update = group_id + ':' + artifact_id
  294. artifact_found = False
  295. with open(version_file, encoding='utf-8') as f:
  296. for raw_line in f:
  297. stripped_line = raw_line.strip()
  298. if not stripped_line or stripped_line.startswith('#'):
  299. continue
  300. else:
  301. module = CodeModule(stripped_line)
  302. # verify the current version of the artifact matches our version schema which is one
  303. # of the following:
  304. # <major>.<minor>.<patch/hotfix>
  305. # <major>.<minor>.<patch/hotfix>-beta.<prerelease>
  306. if module.name == library_to_update and hasattr(module, 'current'):
  307. artifact_found = True
  308. vmatch = version_regex_named.match(module.current)
  309. temp_ver = '{}.{}.{}'.format(vmatch.group('major'), vmatch.group('minor'), vmatch.group('patch'))
  310. # we should never have buildmetadata in our versioning scheme
  311. if vmatch.group('buildmetadata') is not None:
  312. raise ValueError('library ({}) version ({}) in version file ({}) is not a correct version to release. buildmetadata is set and should never be {}'.format(library_to_update, module.current, version_file, vmatch.group('buildmetadata')))
  313. # reconstruct the version from the semver pieces and it should match exactly the current
  314. # version in the module
  315. # If there's a pre-release version it should be beta.X
  316. if vmatch.group('prerelease') is not None:
  317. prerel = vmatch.group('prerelease')
  318. # this regex is looking for beta.X
  319. if prerelease_regex_named.match(prerel) is None:
  320. # if the build_type isn't data then error
  321. if build_type.name.lower() != 'data':
  322. raise ValueError('library ({}) version ({}) in version file ({}) is not a correct version to release. The accepted prerelease tag is (beta.X) and the current prerelease tag is ({})'.format(library_to_update, module.current, version_file, prerel))
  323. else:
  324. # verify that the prerelease tag is "beta" which is the only allowable thing for data track aside from beta.X
  325. if prerelease_data_regex.match(prerel) is None:
  326. raise ValueError('library ({}) version ({}) in version file ({}) is not a correct version to release. The accepted prerelease tags for data track are (beta) or (beta.X) and the current prerelease tag is ({})'.format(library_to_update, module.current, version_file, prerel))
  327. # at this point the version is <major>.<minor>.<patch>-beta
  328. temp_ver = '{}-{}'.format(temp_ver, str(prerel))
  329. else:
  330. prever = prerelease_regex_named.match(prerel)
  331. rev = int(prever.group('revision'))
  332. temp_ver = '{}-beta.{}'.format(temp_ver, str(rev))
  333. # last but not least, for sanity verify that the version constructed from the
  334. # semver pieces matches module's current version
  335. if module.current != temp_ver:
  336. raise ValueError('library ({}) version ({}) in version file ({}) does not match the version constructed from the semver pieces ({})'.format(library_to_update, module.current, version_file, temp_ver))
  337. print('The version {} for {} looks good!'.format(module.current, module.name))
  338. if not artifact_found:
  339. raise ValueError('library ({}) was not found in version file {}'.format(library_to_update, version_file))
  340. def main():
  341. parser = argparse.ArgumentParser(description='set version numbers in the appropriate version text file', add_help=False)
  342. required = parser.add_argument_group('required arguments')
  343. required.add_argument('--build-type', '--bt', type=BuildType, choices=list(BuildType), required=True)
  344. optional = parser.add_argument_group('optional arguments')
  345. optional.add_argument('--build-qualifier', '--bq', help='build qualifier to append onto the version string.')
  346. optional.add_argument('--artifact-id', '--ai', help='artifactId of the target library')
  347. optional.add_argument('--group-id', '--gi', help='groupId of the target library')
  348. optional.add_argument('--prep-source-testing', '--pst', action='store_true', help='prep the version file for source testing')
  349. optional.add_argument('--new-version', '--nv', help='set an new version.')
  350. optional.add_argument('--increment-version', '--iv', action='store_true', help='increment the version for a given group/artifact')
  351. optional.add_argument('--verify-version', '--vv', action='store_true', help='verify the version for a given group/artifact')
  352. optional.add_argument('--set-dev-zero-version', '--sdzv', action='store_true', help='Set a zero dev build version for packages that do not already have dev versions set (should be run after setting dev versions for other packages)')
  353. optional.add_argument('-h', '--help', action='help', default=argparse.SUPPRESS, help='show this help message and exit')
  354. args = parser.parse_args()
  355. if (args.build_type == BuildType.management):
  356. raise ValueError('{} is not currently supported.'.format(BuildType.management.name))
  357. start_time = time.time()
  358. file_changed = False
  359. if (args.prep_source_testing):
  360. file_changed = prep_version_file_for_source_testing(args.build_type)
  361. elif (args.increment_version):
  362. if not args.artifact_id or not args.group_id:
  363. raise ValueError('increment-version requires both the artifact-id and group-id arguments. artifact-id={}, group-id={}'.format(args.artifact_id, args.group_id))
  364. if args.new_version:
  365. raise ValueError('new-version should not be passed with increment-version')
  366. increment_or_set_library_version(args.build_type, args.artifact_id, args.group_id)
  367. elif (args.new_version):
  368. if not args.artifact_id or not args.group_id:
  369. raise ValueError('new-version requires both the artifact-id and group-id arguments. artifact-id={}, group-id={}'.format(args.artifact_id, args.group_id))
  370. increment_or_set_library_version(args.build_type, args.artifact_id, args.group_id, args.new_version)
  371. elif (args.verify_version):
  372. if not args.artifact_id or not args.group_id:
  373. raise ValueError('verify-version requires both the artifact-id and group-id arguments. artifact-id={}, group-id={}'.format(args.artifact_id, args.group_id))
  374. verify_current_version_of_artifact(args.build_type, args.artifact_id, args.group_id)
  375. elif (args.set_dev_zero_version):
  376. if not args.build_qualifier:
  377. raise ValueError('set-dev-zero-version requires build-qualifier')
  378. set_dev_zero_version(args.build_type, args.build_qualifier)
  379. else:
  380. if not args.artifact_id or not args.group_id or not args.build_qualifier:
  381. raise ValueError('update-version requires the artifact-id, group-id and build-qualifier arguments. artifact-id={}, group-id={}, build-qualifier={}'.format(args.artifact_id, args.group_id, args.build_qualifier))
  382. update_versions_file_for_nightly_devops(args.build_type, args.build_qualifier, args.artifact_id, args.group_id)
  383. elapsed_time = time.time() - start_time
  384. print('elapsed_time={}'.format(elapsed_time))
  385. print('Total time for replacement: {}'.format(str(timedelta(seconds=elapsed_time))))
  386. # if the file changed flag is set, which only happens through a call to prep_version_file_for_source_testing,
  387. # then exit with a unique code that allows us to know that something changed.
  388. if (file_changed):
  389. print('##vso[task.setvariable variable=ShouldRunSourceTests]true')
  390. if __name__ == '__main__':
  391. main()