release.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. from __future__ import absolute_import
  2. from __future__ import print_function
  3. from __future__ import unicode_literals
  4. import argparse
  5. import os
  6. import sys
  7. import time
  8. from jinja2 import Template
  9. from release.bintray import BintrayAPI
  10. from release.const import BINTRAY_ORG
  11. from release.const import NAME
  12. from release.const import REPO_ROOT
  13. from release.downloader import BinaryDownloader
  14. from release.repository import get_contributors
  15. from release.repository import Repository
  16. from release.repository import upload_assets
  17. from release.utils import branch_name
  18. from release.utils import compatibility_matrix
  19. from release.utils import read_release_notes_from_changelog
  20. from release.utils import ScriptError
  21. from release.utils import update_init_py_version
  22. from release.utils import update_run_sh_version
  23. def create_initial_branch(repository, release, base, bintray_user):
  24. release_branch = repository.create_release_branch(release, base)
  25. return create_bump_commit(repository, release_branch, bintray_user)
  26. def create_bump_commit(repository, release_branch, bintray_user):
  27. with release_branch.config_reader() as cfg:
  28. release = cfg.get('release')
  29. print('Updating version info in __init__.py and run.sh')
  30. update_run_sh_version(release)
  31. update_init_py_version(release)
  32. input('Please add the release notes to the CHANGELOG.md file, then press Enter to continue.')
  33. proceed = ''
  34. while proceed.lower() != 'y':
  35. print(repository.diff())
  36. proceed = input('Are these changes ok? y/N ')
  37. if repository.diff():
  38. repository.create_bump_commit(release_branch, release)
  39. repository.push_branch_to_remote(release_branch)
  40. bintray_api = BintrayAPI(os.environ['BINTRAY_TOKEN'], bintray_user)
  41. print('Creating data repository {} on bintray'.format(release_branch.name))
  42. bintray_api.create_repository(BINTRAY_ORG, release_branch.name, 'generic')
  43. def monitor_pr_status(pr_data):
  44. print('Waiting for CI to complete...')
  45. last_commit = pr_data.get_commits().reversed[0]
  46. while True:
  47. status = last_commit.get_combined_status()
  48. if status.state == 'pending':
  49. summary = {
  50. 'pending': 0,
  51. 'success': 0,
  52. 'failure': 0,
  53. }
  54. for detail in status.statuses:
  55. summary[detail.state] += 1
  56. print('{pending} pending, {success} successes, {failure} failures'.format(**summary))
  57. if status.total_count == 0:
  58. # Mostly for testing purposes against repos with no CI setup
  59. return True
  60. time.sleep(30)
  61. elif status.state == 'success':
  62. print('{} successes: all clear!'.format(status.total_count))
  63. return True
  64. else:
  65. raise ScriptError('CI failure detected')
  66. def create_release_draft(repository, version, pr_data, files):
  67. print('Creating Github release draft')
  68. with open(os.path.join(os.path.dirname(__file__), 'release.md.tmpl'), 'r') as f:
  69. template = Template(f.read())
  70. print('Rendering release notes based on template')
  71. release_notes = template.render(
  72. version=version,
  73. compat_matrix=compatibility_matrix(),
  74. integrity=files,
  75. contributors=get_contributors(pr_data),
  76. changelog=read_release_notes_from_changelog(),
  77. )
  78. gh_release = repository.create_release(
  79. version, release_notes, draft=True, prerelease='-rc' in version,
  80. target_commitish='release'
  81. )
  82. print('Release draft initialized')
  83. return gh_release
  84. def print_final_instructions(gh_release):
  85. print("""
  86. You're almost done! The following steps should be executed after you've
  87. verified that everything is in order and are ready to make the release public:
  88. 1.
  89. 2.
  90. 3.""")
  91. def resume(args):
  92. try:
  93. repository = Repository(REPO_ROOT, args.repo or NAME)
  94. br_name = branch_name(args.release)
  95. if not repository.branch_exists(br_name):
  96. raise ScriptError('No local branch exists for this release.')
  97. release_branch = repository.checkout_branch(br_name)
  98. create_bump_commit(repository, release_branch, args.bintray_user)
  99. pr_data = repository.find_release_pr(args.release)
  100. if not pr_data:
  101. pr_data = repository.create_release_pull_request(args.release)
  102. monitor_pr_status(pr_data)
  103. downloader = BinaryDownloader(args.destination)
  104. files = downloader.download_all(args.release)
  105. gh_release = repository.find_release(args.release)
  106. if not gh_release:
  107. gh_release = create_release_draft(repository, args.release, pr_data, files)
  108. elif not gh_release.draft:
  109. print('WARNING!! Found non-draft (public) release for this version!')
  110. proceed = input(
  111. 'Are you sure you wish to proceed? Modifying an already '
  112. 'released version is dangerous! y/N'
  113. )
  114. if proceed.lower() != 'y':
  115. raise ScriptError('Aborting release')
  116. for asset in gh_release.get_assets():
  117. asset.delete_asset()
  118. upload_assets(gh_release, files)
  119. except ScriptError as e:
  120. print(e)
  121. return 1
  122. print_final_instructions(gh_release)
  123. return 0
  124. def cancel(args):
  125. try:
  126. repository = Repository(REPO_ROOT, args.repo or NAME)
  127. repository.close_release_pr(args.release)
  128. repository.remove_release(args.release)
  129. repository.remove_bump_branch(args.release)
  130. # TODO: uncomment after testing is complete
  131. # bintray_api = BintrayAPI(os.environ['BINTRAY_TOKEN'], args.bintray_user)
  132. # print('Removing Bintray data repository for {}'.format(args.release))
  133. # bintray_api.delete_repository(BINTRAY_ORG, branch_name(args.release))
  134. except ScriptError as e:
  135. print(e)
  136. return 1
  137. print('Release cancellation complete.')
  138. return 0
  139. def start(args):
  140. try:
  141. repository = Repository(REPO_ROOT, args.repo or NAME)
  142. create_initial_branch(repository, args.release, args.base, args.bintray_user)
  143. pr_data = repository.create_release_pull_request(args.release)
  144. monitor_pr_status(pr_data)
  145. downloader = BinaryDownloader(args.destination)
  146. files = downloader.download_all(args.release)
  147. gh_release = create_release_draft(repository, args.release, pr_data, files)
  148. upload_assets(gh_release, files)
  149. except ScriptError as e:
  150. print(e)
  151. return 1
  152. print_final_instructions(gh_release)
  153. return 0
  154. def main():
  155. if 'GITHUB_TOKEN' not in os.environ:
  156. print('GITHUB_TOKEN environment variable must be set')
  157. return 1
  158. if 'BINTRAY_TOKEN' not in os.environ:
  159. print('BINTRAY_TOKEN environment variable must be set')
  160. return 1
  161. parser = argparse.ArgumentParser(
  162. description='Orchestrate a new release of docker/compose. This tool assumes that you have '
  163. 'obtained a Github API token and Bintray API key and set the GITHUB_TOKEN and '
  164. 'BINTRAY_TOKEN environment variables accordingly.',
  165. epilog='''Example uses:
  166. * Start a new feature release (includes all changes currently in master)
  167. release.py -b user start 1.23.0
  168. * Start a new patch release
  169. release.py -b user --patch 1.21.0 start 1.21.1
  170. * Cancel / rollback an existing release draft
  171. release.py -b user cancel 1.23.0
  172. * Restart a previously aborted patch release
  173. release.py -b user -p 1.21.0 resume 1.21.1''', formatter_class=argparse.RawTextHelpFormatter)
  174. parser.add_argument(
  175. 'action', choices=['start', 'resume', 'cancel'],
  176. help='The action to be performed for this release'
  177. )
  178. parser.add_argument('release', help='Release number, e.g. 1.9.0-rc1, 2.1.1')
  179. parser.add_argument(
  180. '--patch', '-p', dest='base',
  181. help='Which version is being patched by this release'
  182. )
  183. parser.add_argument(
  184. '--repo', '-r', dest='repo',
  185. help='Start a release for the given repo (default: {})'.format(NAME)
  186. )
  187. parser.add_argument(
  188. '-b', dest='bintray_user', required=True, metavar='USER',
  189. help='Username associated with the Bintray API key'
  190. )
  191. parser.add_argument(
  192. '--destination', '-o', metavar='DIR', default='binaries',
  193. help='Directory where release binaries will be downloaded relative to the project root'
  194. )
  195. args = parser.parse_args()
  196. if args.action == 'start':
  197. return start(args)
  198. elif args.action == 'resume':
  199. return resume(args)
  200. elif args.action == 'cancel':
  201. return cancel(args)
  202. print('Unexpected action "{}"'.format(args.action), file=sys.stderr)
  203. return 1
  204. if __name__ == '__main__':
  205. sys.exit(main())