prepare_release_candidate.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # Licensed to Elasticsearch under one or more contributor
  2. # license agreements. See the NOTICE file distributed with
  3. # this work for additional information regarding copyright
  4. # ownership. Elasticsearch licenses this file to you under
  5. # the Apache License, Version 2.0 (the "License"); you may
  6. # 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,
  12. # software distributed under the License is distributed on
  13. # an 'AS IS' BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
  14. # either express or implied. See the License for the specific
  15. # language governing permissions and limitations under the License.
  16. # Prepare a release
  17. #
  18. # 1. Update the Version.java to remove the snapshot bit
  19. # 2. Remove the -SNAPSHOT suffix in all pom.xml files
  20. #
  21. # USAGE:
  22. #
  23. # python3 ./dev-tools/prepare-release.py
  24. #
  25. # Note: Ensure the script is run from the elasticsearch top level directory
  26. #
  27. import fnmatch
  28. import argparse
  29. from prepare_release_update_documentation import update_reference_docs
  30. import subprocess
  31. import tempfile
  32. import re
  33. import os
  34. import shutil
  35. VERSION_FILE = 'core/src/main/java/org/elasticsearch/Version.java'
  36. POM_FILE = 'pom.xml'
  37. MAIL_TEMPLATE = """
  38. Hi all
  39. The new release candidate for %(version)s based on this commit[1] is now available, including the x-plugins, and RPM/deb repos:
  40. - ZIP [2]
  41. - tar.gz [3]
  42. - RPM [4]
  43. - deb [5]
  44. Plugins can be installed as follows,
  45. bin/plugin -Des.plugins.staging=true install cloud-aws
  46. The same goes for the x-plugins:
  47. bin/plugin -Des.plugins.staging=true install license
  48. bin/plugin -Des.plugins.staging=true install shield
  49. bin/plugin -Des.plugins.staging=true install watcher
  50. To install the deb from an APT repo:
  51. APT line sources.list line:
  52. deb http://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/repos/elasticsearch/%(major_minor_version)s/debian/ stable main
  53. To install the RPM, create a YUM file like:
  54. /etc/yum.repos.d/elasticsearch.repo
  55. containing:
  56. [elasticsearch-2.0]
  57. name=Elasticsearch repository for packages
  58. baseurl=http://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/repos/elasticsearch/%(major_minor_version)s/centos
  59. gpgcheck=1
  60. gpgkey=http://packages.elastic.co/GPG-KEY-elasticsearch
  61. enabled=1
  62. To smoke-test the release please run:
  63. python3 -B ./dev-tools/smoke_tests_rc.py --version %(version)s --hash %(hash)s --plugins license,shield,watcher
  64. NOTE: this script requires JAVA_HOME to point to a Java 7 Runtime
  65. [1] https://github.com/elastic/elasticsearch/commit/%(hash)s
  66. [2] http://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/zip/elasticsearch/%(version)s/elasticsearch-%(version)s.zip
  67. [3] http://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/tar/elasticsearch/%(version)s/elasticsearch-%(version)s.tar.gz
  68. [4] http://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/rpm/elasticsearch/%(version)s/elasticsearch-%(version)s.rpm
  69. [5] http://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/deb/elasticsearch/%(version)s/elasticsearch-%(version)s.deb
  70. """
  71. VERBOSE=True
  72. def run(command, env_vars=None, verbose=VERBOSE):
  73. if env_vars:
  74. for key, value in env_vars.items():
  75. os.putenv(key, value)
  76. if not verbose:
  77. command = '%s >> /dev/null 2>&1' % (command)
  78. if os.system(command):
  79. raise RuntimeError(' FAILED: %s' % (command))
  80. def ensure_checkout_is_clean():
  81. # Make sure no local mods:
  82. s = subprocess.check_output('git diff --shortstat', shell=True).decode('utf-8')
  83. if len(s) > 0:
  84. raise RuntimeError('git diff --shortstat is non-empty got:\n%s' % s)
  85. # Make sure no untracked files:
  86. s = subprocess.check_output('git status', shell=True).decode('utf-8', errors='replace')
  87. if 'Untracked files:' in s:
  88. if 'dev-tools/__pycache__/' in s:
  89. print('*** NOTE: invoke python with -B to prevent __pycache__ directories ***')
  90. raise RuntimeError('git status shows untracked files got:\n%s' % s)
  91. # Make sure we have all changes from origin:
  92. if 'is behind' in s:
  93. raise RuntimeError('git status shows not all changes pulled from origin; try running "git pull origin" in this branch got:\n%s' % (s))
  94. # Make sure we no local unpushed changes (this is supposed to be a clean area):
  95. if 'is ahead' in s:
  96. raise RuntimeError('git status shows local commits; try running "git fetch origin", "git checkout ", "git reset --hard origin/" in this branch got:\n%s' % (s))
  97. # Reads the given file and applies the
  98. # callback to it. If the callback changed
  99. # a line the given file is replaced with
  100. # the modified input.
  101. def process_file(file_path, line_callback):
  102. fh, abs_path = tempfile.mkstemp()
  103. modified = False
  104. with open(abs_path,'w', encoding='utf-8') as new_file:
  105. with open(file_path, encoding='utf-8') as old_file:
  106. for line in old_file:
  107. new_line = line_callback(line)
  108. modified = modified or (new_line != line)
  109. new_file.write(new_line)
  110. os.close(fh)
  111. if modified:
  112. #Remove original file
  113. os.remove(file_path)
  114. #Move new file
  115. shutil.move(abs_path, file_path)
  116. return True
  117. else:
  118. # nothing to do - just remove the tmp file
  119. os.remove(abs_path)
  120. return False
  121. # Moves the Version.java file from a snapshot to a release
  122. def remove_version_snapshot(version_file, release):
  123. # 1.0.0.Beta1 -> 1_0_0_Beta1
  124. release = release.replace('.', '_')
  125. release = release.replace('-', '_')
  126. pattern = 'new Version(V_%s_ID, true' % (release)
  127. replacement = 'new Version(V_%s_ID, false' % (release)
  128. def callback(line):
  129. return line.replace(pattern, replacement)
  130. processed = process_file(version_file, callback)
  131. if not processed:
  132. raise RuntimeError('failed to remove snapshot version for %s' % (release))
  133. def rename_local_meta_files(path):
  134. for root, _, file_names in os.walk(path):
  135. for file_name in fnmatch.filter(file_names, 'maven-metadata-local.xml*'):
  136. full_path = os.path.join(root, file_name)
  137. os.rename(full_path, os.path.join(root, file_name.replace('-local', '')))
  138. # Checks the pom.xml for the release version.
  139. # This method fails if the pom file has no SNAPSHOT version set ie.
  140. # if the version is already on a release version we fail.
  141. # Returns the next version string ie. 0.90.7
  142. def find_release_version():
  143. with open('pom.xml', encoding='utf-8') as file:
  144. for line in file:
  145. match = re.search(r'<version>(.+)-SNAPSHOT</version>', line)
  146. if match:
  147. return match.group(1)
  148. raise RuntimeError('Could not find release version in branch')
  149. if __name__ == "__main__":
  150. parser = argparse.ArgumentParser(description='Builds and publishes a Elasticsearch Release')
  151. parser.add_argument('--deploy', '-d', dest='deploy', action='store_true',
  152. help='Installs and Deploys the release on a sonartype staging repository.')
  153. parser.add_argument('--skipDocCheck', '-c', dest='skip_doc_check', action='store_false',
  154. help='Skips any checks for pending documentation changes')
  155. parser.add_argument('--push-s3', '-p', dest='push', action='store_true',
  156. help='Pushes artifacts to the S3 staging area')
  157. parser.add_argument('--install_only', '-i', dest='install_only', action='store_true',
  158. help='Only runs a maven install to skip the remove deployment step')
  159. parser.add_argument('--gpg-key', '-k', dest='gpg_key', default="D88E42B4",
  160. help='Allows you to specify a different gpg_key to be used instead of the default release key')
  161. parser.add_argument('--verbose', '-b', dest='verbose',
  162. help='Runs the script in verbose mode')
  163. parser.set_defaults(deploy=False)
  164. parser.set_defaults(skip_doc_check=False)
  165. parser.set_defaults(push=False)
  166. parser.set_defaults(install_only=False)
  167. parser.set_defaults(verbose=False)
  168. args = parser.parse_args()
  169. install_and_deploy = args.deploy
  170. skip_doc_check = args.skip_doc_check
  171. push = args.push
  172. gpg_key = args.gpg_key
  173. install_only = args.install_only
  174. VERBOSE = args.verbose
  175. ensure_checkout_is_clean()
  176. release_version = find_release_version()
  177. if not re.match('(\d+\.\d+)\.*',release_version):
  178. raise RuntimeError('illegal release version format: %s' % (release_version))
  179. major_minor_version = re.match('(\d+\.\d+)\.*',release_version).group(1)
  180. print('*** Preparing release version: [%s]' % release_version)
  181. if not skip_doc_check:
  182. print('*** Check for pending documentation changes')
  183. pending_files = update_reference_docs(release_version)
  184. if pending_files:
  185. raise RuntimeError('pending coming[%s] documentation changes found in %s' % (release_version, pending_files))
  186. run('cd dev-tools && mvn versions:set -DnewVersion=%s -DgenerateBackupPoms=false' % (release_version))
  187. run('cd rest-api-spec && mvn versions:set -DnewVersion=%s -DgenerateBackupPoms=false' % (release_version))
  188. run('mvn versions:set -DnewVersion=%s -DgenerateBackupPoms=false' % (release_version))
  189. remove_version_snapshot(VERSION_FILE, release_version)
  190. print('*** Done removing snapshot version. DO NOT COMMIT THIS, WHEN CREATING A RELEASE CANDIDATE.')
  191. shortHash = subprocess.check_output('git log --pretty=format:"%h" -n 1', shell=True).decode('utf-8')
  192. localRepo = '/tmp/elasticsearch-%s-%s' % (release_version, shortHash)
  193. localRepoElasticsearch = localRepo + '/org/elasticsearch'
  194. if os.path.exists(localRepoElasticsearch):
  195. print('clean local repository %s' % localRepoElasticsearch)
  196. shutil.rmtree(localRepoElasticsearch)
  197. if install_only:
  198. mvn_target = 'install'
  199. else:
  200. mvn_target = 'deploy'
  201. install_command = 'mvn clean %s -Prelease -Dskip.integ.tests=true -Dgpg.key="%s" -Dpackaging.rpm.rpmbuild=/usr/bin/rpmbuild -Drpm.sign=true -Dmaven.repo.local=%s -Dno.commit.pattern="\\bno(n|)commit\\b" -Dforbidden.test.signatures=""' % (mvn_target, gpg_key, localRepo)
  202. clean_repo_command = 'find %s -name _remote.repositories -exec rm {} \;' % (localRepoElasticsearch)
  203. rename_metadata_files_command = 'for i in $(find %s -name "maven-metadata-local.xml*") ; do mv "$i" "${i/-local/}" ; done' % (localRepoElasticsearch)
  204. s3_sync_command = 's3cmd sync %s s3://download.elasticsearch.org/elasticsearch/staging/%s-%s/org/' % (localRepoElasticsearch, release_version, shortHash)
  205. s3_bucket_sync_to = 'download.elasticsearch.org/elasticsearch/staging/%s-%s/repos' % (release_version, shortHash)
  206. if install_and_deploy:
  207. for cmd in [install_command, clean_repo_command]:
  208. run(cmd)
  209. rename_local_meta_files(localRepoElasticsearch)
  210. else:
  211. print('')
  212. print('*** To create a release candidate run: ')
  213. print(' %s' % (install_command))
  214. print(' 1. Remove all _remote.repositories: %s' % (clean_repo_command))
  215. print(' 2. Rename all maven metadata files: %s' % (rename_metadata_files_command))
  216. if push:
  217. run(s3_sync_command)
  218. print('Use rpm-s3/deb-s3 to push into repositories at %s' % s3_bucket_sync_to)
  219. else:
  220. print('')
  221. print('*** To push a release candidate to s3 run: ')
  222. print(' 1. Sync %s into S3 bucket' % (localRepoElasticsearch))
  223. print (' %s' % (s3_sync_command))
  224. print(' 2. Create repositories: ')
  225. print(' Use rpm-s3/deb-s3 to push into repositories at %s' % s3_bucket_sync_to)
  226. print('')
  227. print('NOTE: the above mvn command will promt you several times for the GPG passphrase of the key you specified you can alternatively pass it via -Dgpg.passphrase=yourPassPhrase')
  228. print(' since RPM signing doesn\'t support gpg-agents the recommended way to set the password is to add a release profile to your settings.xml:')
  229. print("""
  230. <profiles>
  231. <profile>
  232. <id>release</id>
  233. <properties>
  234. <gpg.passphrase>YourPasswordGoesHere</gpg.passphrase>
  235. </properties>
  236. </profile>
  237. </profiles>
  238. """)
  239. print('NOTE: Running s3cmd might require you to create a config file with your credentials, if the s3cmd does not support suppliying them via the command line!')
  240. print('*** Once the release is deployed and published send out the following mail to dev@elastic.co:')
  241. string_format_dict = {'version' : release_version, 'hash': shortHash, 'major_minor_version' : major_minor_version}
  242. print(MAIL_TEMPLATE % string_format_dict)
  243. print('To publish the release and the repo on S3 execute the following commands:')
  244. print(' s3cmd cp --recursive s3://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/repos/elasticsearch/%(major_minor_version)s/ s3://packages.elasticsearch.org/elasticsearch/%(major_minor_version)s' % string_format_dict)
  245. print(' s3cmd cp --recursive s3://download.elasticsearch.org/elasticsearch/staging/%(version)s-%(hash)s/org/ s3://download.elasticsearch.org/elasticsearch/release/org' % string_format_dict)
  246. print('Now go ahead and tag the release:')
  247. print(' git tag -a v%(version)s %(hash)s' % string_format_dict)
  248. print(' git push origin v%(version)s' % string_format_dict )