create-release.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. #!/usr/bin/env python3
  2. #
  3. # Copyright 2018-2022 Elyra Authors
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. import argparse
  18. import json
  19. import os
  20. import shutil
  21. import subprocess
  22. import sys
  23. from types import SimpleNamespace
  24. config: SimpleNamespace
  25. VERSION_REG_EX = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<pre_release>[a-z]+)(?P<build>\d+))?"
  26. DEFAULT_GIT_URL = 'git@github.com:elyra-ai/pipeline-editor.git'
  27. DEFAULT_BUILD_DIR = 'build/release'
  28. class DependencyException(Exception):
  29. """Error if dependency is missing"""
  30. class MissingReleaseArtifactException(Exception):
  31. """Error if an artifact being released is not available"""
  32. class UpdateVersionException(Exception):
  33. """Error if the old version is invalid or cannot be found, or if there's a duplicate version"""
  34. def check_run(args, cwd=os.getcwd(), capture_output=True, env=None, shell=False) -> subprocess.CompletedProcess:
  35. try:
  36. return subprocess.run(args, cwd=cwd, capture_output=capture_output, check=True)
  37. except subprocess.CalledProcessError as ex:
  38. raise RuntimeError(f'Error executing process: {ex.stderr.decode("unicode_escape")}') from ex
  39. def check_output(args, cwd=os.getcwd(), env=None, shell=False) -> str:
  40. response = check_run(args, cwd, capture_output=True, env=env, shell=shell)
  41. return response.stdout.decode('utf-8').replace('\n', '')
  42. def dependency_exists(command) -> bool:
  43. """Returns true if a command exists on the system"""
  44. try:
  45. check_run(["which", command])
  46. except:
  47. return False
  48. return True
  49. def sed(file: str, pattern: str, replace: str) -> None:
  50. """Perform regex substitution on a given file"""
  51. try:
  52. check_run(["sed", "-i", "", "-e", f"s#{pattern}#{replace}#g", file], capture_output=False)
  53. except Exception as ex:
  54. raise RuntimeError(f'Error processing updated to file {file}: ') from ex
  55. def validate_dependencies() -> None:
  56. """Error if a dependency is missing or invalid"""
  57. if not dependency_exists("git"):
  58. raise DependencyException('Please install git https://git-scm.com/downloads')
  59. if not dependency_exists("node"):
  60. raise DependencyException('Please install node.js https://nodejs.org/')
  61. if not dependency_exists("yarn"):
  62. raise DependencyException("Please install yarn https://classic.yarnpkg.com/")
  63. if not dependency_exists("twine"):
  64. raise DependencyException("Please install twine https://twine.readthedocs.io/en/latest/#installation")
  65. def validate_environment() -> None:
  66. """Validate environment configurations are valid"""
  67. pass
  68. def update_version_to_release() -> None:
  69. global config
  70. new_version = config.new_version
  71. try:
  72. check_run(["lerna", "version", new_version, "--no-git-tag-version", "--no-push", "--yes"], cwd=config.source_dir)
  73. check_run(["yarn", "version", "--new-version", new_version, "--no-git-tag-version"], cwd=config.source_dir)
  74. except Exception as ex:
  75. raise UpdateVersionException from ex
  76. def _source(file: str) -> str:
  77. global config
  78. return os.path.join(config.source_dir, file)
  79. def checkout_code() -> None:
  80. global config
  81. print("-----------------------------------------------------------------")
  82. print("-------------------- Retrieving source code ---------------------")
  83. print("-----------------------------------------------------------------")
  84. print(f'Cloning repository: {config.git_url}')
  85. if os.path.exists(config.work_dir):
  86. print(f'Removing working directory: {config.work_dir}')
  87. shutil.rmtree(config.work_dir)
  88. print(f'Creating working directory: {config.work_dir}')
  89. os.makedirs(config.work_dir)
  90. print(f'Cloning : {config.git_url} to {config.work_dir}')
  91. check_run(['git', 'clone', config.git_url], cwd=config.work_dir)
  92. check_run(['git', 'config', 'user.name', config.git_user_name], cwd=config.source_dir)
  93. check_run(['git', 'config', 'user.email', config.git_user_email], cwd=config.source_dir)
  94. print('')
  95. def build_and_publish_npm_packages() -> None:
  96. global config
  97. print("-----------------------------------------------------------------")
  98. print("--------------------- Building NPM Packages ---------------------")
  99. print("-----------------------------------------------------------------")
  100. check_run(["make", "clean", "install"], cwd=config.source_dir, capture_output=False)
  101. print("-----------------------------------------------------------------")
  102. print("-------------------- Pushing npm packages -----------------------")
  103. print("-----------------------------------------------------------------")
  104. # publish npm packages
  105. print()
  106. print(f'publishing npm packages')
  107. check_run(['lerna', 'publish', '--yes', 'from-package', '--no-git-tag-version', '--no-verify-access', '--no-push'], cwd=config.source_dir)
  108. check_run(['make', 'lint'], cwd=config.source_dir)
  109. def publish_git_release() -> None:
  110. global config
  111. print("-----------------------------------------------------------------")
  112. print("--------------- Pushing Release and Tag to git ------------------")
  113. print("-----------------------------------------------------------------")
  114. # push release and tags to git
  115. print()
  116. print('Pushing release to git')
  117. check_run(['git', 'push'], cwd=config.source_dir)
  118. print('Pushing release tag to git')
  119. check_run(['git', 'push', '--tags'], cwd=config.source_dir)
  120. def release() -> None:
  121. """
  122. Prepare a release
  123. """
  124. global config
  125. print(f'Processing release from {config.old_version} to {config.new_version} ')
  126. print('')
  127. # clone repository
  128. checkout_code()
  129. # Update to new release version
  130. update_version_to_release()
  131. # commit release
  132. check_run(['git', 'commit', '-a', '-m', f'Release v{config.new_version}'], cwd=config.source_dir)
  133. # build and publish npm packages
  134. build_and_publish_npm_packages()
  135. # commit and tag release
  136. check_run(['git', 'commit', '-a', '--amend', '--no-edit'], cwd=config.source_dir)
  137. check_run(['git', 'tag', config.tag], cwd=config.source_dir)
  138. # publish git changes
  139. publish_git_release()
  140. def initialize_config(args=None) -> SimpleNamespace:
  141. if not args:
  142. raise ValueError("Invalid command line arguments")
  143. with open('package.json') as f:
  144. package_json = json.load(f)
  145. v = package_json['version']
  146. configuration = {
  147. 'git_url': DEFAULT_GIT_URL,
  148. 'git_hash': 'HEAD',
  149. 'git_user_name': check_output(['git', 'config', 'user.name']),
  150. 'git_user_email': check_output(['git', 'config', 'user.email']),
  151. 'base_dir': os.getcwd(),
  152. 'work_dir': os.path.join(os.getcwd(), DEFAULT_BUILD_DIR),
  153. 'source_dir': os.path.join(os.getcwd(), DEFAULT_BUILD_DIR, 'pipeline-editor'),
  154. 'old_version': v,
  155. 'new_version': args.version if not args.rc or not str.isdigit(args.rc) else f'{args.version}-rc.{args.rc}',
  156. 'rc': args.rc,
  157. 'tag': f'v{args.version}' if not args.rc or not str.isdigit(args.rc) else f'v{args.version}rc{args.rc}'
  158. }
  159. global config
  160. config = SimpleNamespace(**configuration)
  161. def print_config() -> None:
  162. global config
  163. print('')
  164. print("-----------------------------------------------------------------")
  165. print("--------------------- Release configuration ---------------------")
  166. print("-----------------------------------------------------------------")
  167. print(f'Git URL \t\t -> {config.git_url}')
  168. print(f'Git reference \t\t -> {config.git_hash}')
  169. print(f'Git user \t\t -> {config.git_user_name}')
  170. print(f'Git user email \t\t -> {config.git_user_email}')
  171. print(f'Work dir \t\t -> {config.work_dir}')
  172. print(f'Source dir \t\t -> {config.source_dir}')
  173. print(f'Current Version \t -> {config.old_version}')
  174. print(f'New Version \t\t -> {config.new_version}')
  175. if config.rc is not None:
  176. print(f'RC number \t\t -> {config.rc}')
  177. print(f'Release Tag \t\t -> {config.tag}')
  178. print("-----------------------------------------------------------------")
  179. print('')
  180. def print_help() -> str:
  181. return (
  182. """create-release.py --version VERSION
  183. DESCRIPTION
  184. Creates Pipeline-Editor release based on git commit hash or from HEAD.
  185. create-release.py --version 1.3.0 [--rc 0]
  186. This form will prepare a release, build its artifacts and publish
  187. Required software dependencies for building and publishing a release:
  188. - Git
  189. - Node
  190. - Twine
  191. - Yarn
  192. Required configurations for publishing a release:
  193. - GPG with signing key configured
  194. """
  195. )
  196. def main(args=None):
  197. """Perform necessary tasks to create and/or publish a new release"""
  198. parser = argparse.ArgumentParser(usage=print_help())
  199. parser.add_argument('--version', help='the new release version', type=str, required=True)
  200. parser.add_argument('--rc', help='the release candidate number', type=str, required=False, )
  201. args = parser.parse_args()
  202. global config
  203. try:
  204. # Validate all pre-requisites are available
  205. validate_dependencies()
  206. validate_environment()
  207. # Generate release config based on the provided arguments
  208. initialize_config(args)
  209. print_config()
  210. release()
  211. except Exception as ex:
  212. raise RuntimeError(f'Error performing release {args.version}') from ex
  213. if __name__ == "__main__":
  214. main()