prepare_release_candidate.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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. from functools import partial
  36. import sys
  37. VERSION_FILE = 'core/src/main/java/org/elasticsearch/Version.java'
  38. POM_FILE = 'pom.xml'
  39. MAIL_TEMPLATE = """
  40. Hi all
  41. The new release candidate for %(version)s is now available, including the x-plugins and RPM/deb repos. This release is based on:
  42. * Elasticsearch commit: %(hash)s - https://github.com/elastic/elasticsearch/commit/%(hash)s
  43. * X-Plugins commit: FILL_IN_X-PLUGINS_HASH - https://github.com/elastic/x-plugins/commit/FILL_IN_X-PLUGINS_HASH
  44. The packages may be downloaded from the following URLs:
  45. * ZIP - http://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/zip/elasticsearch/%(version)s/elasticsearch-%(version)s.zip
  46. * tar.gz - http://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/tar/elasticsearch/%(version)s/elasticsearch-%(version)s.tar.gz
  47. * RPM - http://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/rpm/elasticsearch/%(version)s/elasticsearch-%(version)s.rpm
  48. * deb - http://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/org/elasticsearch/distribution/deb/elasticsearch/%(version)s/elasticsearch-%(version)s.deb
  49. Plugins can be installed as follows:
  50. bin/elasticsearch-plugin -Des.plugins.staging=true install cloud-aws
  51. The same goes for the x-plugins:
  52. bin/elasticsearch-plugin -Des.plugins.staging=true install license
  53. bin/elasticsearch-plugin -Des.plugins.staging=true install marvel-agent
  54. bin/elasticsearch-plugin -Des.plugins.staging=true install shield
  55. bin/elasticsearch-plugin -Des.plugins.staging=true install watcher
  56. To install the deb from an APT repo:
  57. APT line sources.list line:
  58. deb http://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/repos/%(package_repo_version)s/debian/ stable main
  59. To install the RPM, create a YUM file like:
  60. /etc/yum.repos.d/elasticsearch.repo
  61. containing:
  62. [elasticsearch-2.0]
  63. name=Elasticsearch repository for packages
  64. baseurl=http://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/repos/%(package_repo_version)s/centos
  65. gpgcheck=1
  66. gpgkey=http://packages.elastic.co/GPG-KEY-elasticsearch
  67. enabled=1
  68. To smoke-test the release please run:
  69. python3 -B ./dev-tools/smoke_test_rc.py --version %(version)s --hash %(hash)s --plugins license,shield,watcher
  70. NOTE: this script requires JAVA_HOME to point to a Java 7 Runtime
  71. """
  72. # console colors
  73. COLOR_OK = '\033[92m'
  74. COLOR_END = '\033[0m'
  75. COLOR_FAIL = '\033[91m'
  76. def run(command, env_vars=None):
  77. if env_vars:
  78. for key, value in env_vars.items():
  79. os.putenv(key, value)
  80. print('*** Running: %s%s%s' % (COLOR_OK, command, COLOR_END))
  81. if os.system(command):
  82. raise RuntimeError(' FAILED: %s' % (command))
  83. def ensure_checkout_is_clean():
  84. # Make sure no local mods:
  85. s = subprocess.check_output('git diff --shortstat', shell=True).decode('utf-8')
  86. if len(s) > 0:
  87. raise RuntimeError('git diff --shortstat is non-empty got:\n%s' % s)
  88. # Make sure no untracked files:
  89. s = subprocess.check_output('git status', shell=True).decode('utf-8', errors='replace')
  90. if 'Untracked files:' in s:
  91. if 'dev-tools/__pycache__/' in s:
  92. print('*** NOTE: invoke python with -B to prevent __pycache__ directories ***')
  93. raise RuntimeError('git status shows untracked files got:\n%s' % s)
  94. # Make sure we have all changes from origin:
  95. if 'is behind' in s:
  96. raise RuntimeError('git status shows not all changes pulled from origin; try running "git pull origin" in this branch got:\n%s' % (s))
  97. # Make sure we no local unpushed changes (this is supposed to be a clean area):
  98. if 'is ahead' in s:
  99. 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))
  100. # Reads the given file and applies the
  101. # callback to it. If the callback changed
  102. # a line the given file is replaced with
  103. # the modified input.
  104. def process_file(file_path, line_callback):
  105. fh, abs_path = tempfile.mkstemp()
  106. modified = False
  107. with open(abs_path,'w', encoding='utf-8') as new_file:
  108. with open(file_path, encoding='utf-8') as old_file:
  109. for line in old_file:
  110. new_line = line_callback(line)
  111. modified = modified or (new_line != line)
  112. new_file.write(new_line)
  113. os.close(fh)
  114. if modified:
  115. #Remove original file
  116. os.remove(file_path)
  117. #Move new file
  118. shutil.move(abs_path, file_path)
  119. return True
  120. else:
  121. # nothing to do - just remove the tmp file
  122. os.remove(abs_path)
  123. return False
  124. # Moves the Version.java file from a snapshot to a release
  125. def remove_version_snapshot(version_file, release):
  126. # 1.0.0.Beta1 -> 1_0_0_Beta1
  127. release = release.replace('.', '_')
  128. release = release.replace('-', '_')
  129. pattern = 'new Version(V_%s_ID, true' % (release)
  130. replacement = 'new Version(V_%s_ID, false' % (release)
  131. def callback(line):
  132. return line.replace(pattern, replacement)
  133. processed = process_file(version_file, callback)
  134. if not processed:
  135. raise RuntimeError('failed to remove snapshot version for %s' % (release))
  136. def rename_local_meta_files(path):
  137. for root, _, file_names in os.walk(path):
  138. for file_name in fnmatch.filter(file_names, 'maven-metadata-local.xml*'):
  139. full_path = os.path.join(root, file_name)
  140. os.rename(full_path, os.path.join(root, file_name.replace('-local', '')))
  141. # Checks the pom.xml for the release version.
  142. # This method fails if the pom file has no SNAPSHOT version set ie.
  143. # if the version is already on a release version we fail.
  144. # Returns the next version string ie. 0.90.7
  145. def find_release_version():
  146. with open('pom.xml', encoding='utf-8') as file:
  147. for line in file:
  148. match = re.search(r'<version>(.+)-SNAPSHOT</version>', line)
  149. if match:
  150. return match.group(1)
  151. raise RuntimeError('Could not find release version in branch')
  152. # Checks if the produced RPM is signed with the supplied GPG key
  153. def ensure_rpm_is_signed(rpm, gpg_key):
  154. rpm_check_signature_cmd = 'rpm -v -K %s | grep -qi %s' % (rpm, gpg_key)
  155. try:
  156. subprocess.check_output(rpm_check_signature_cmd, shell=True)
  157. except:
  158. raise RuntimeError('Aborting. RPM does not seem to be signed, check with: rpm -v -K %s' % rpm)
  159. # Checks if a command exists, needed for external binaries
  160. def check_command_exists(name, cmd):
  161. try:
  162. subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT)
  163. except subprocess.CalledProcessError:
  164. raise RuntimeError('Could not run command %s - please make sure it is installed and in $PATH' % (name))
  165. def run_and_print(text, run_function):
  166. try:
  167. print(text, end='')
  168. run_function()
  169. print(COLOR_OK + 'OK' + COLOR_END)
  170. return True
  171. except RuntimeError:
  172. print(COLOR_FAIL + 'NOT OK' + COLOR_END)
  173. return False
  174. def check_env_var(text, env_var):
  175. try:
  176. print(text, end='')
  177. os.environ[env_var]
  178. print(COLOR_OK + 'OK' + COLOR_END)
  179. return True
  180. except KeyError:
  181. print(COLOR_FAIL + 'NOT OK' + COLOR_END)
  182. return False
  183. def check_environment_and_commandline_tools(check_only):
  184. checks = list()
  185. checks.append(check_env_var('Checking for AWS env configuration AWS_SECRET_KEY... ', 'AWS_SECRET_KEY'))
  186. checks.append(check_env_var('Checking for AWS env configuration AWS_ACCESS_KEY... ', 'AWS_ACCESS_KEY'))
  187. checks.append(run_and_print('Checking command: rpm... ', partial(check_command_exists, 'rpm', 'rpm --version')))
  188. checks.append(run_and_print('Checking command: dpkg... ', partial(check_command_exists, 'dpkg', 'dpkg --version')))
  189. checks.append(run_and_print('Checking command: gpg... ', partial(check_command_exists, 'gpg', 'gpg --version')))
  190. checks.append(run_and_print('Checking command: expect... ', partial(check_command_exists, 'expect', 'expect -v')))
  191. checks.append(run_and_print('Checking command: createrepo... ', partial(check_command_exists, 'createrepo', 'createrepo --version')))
  192. checks.append(run_and_print('Checking command: s3cmd... ', partial(check_command_exists, 's3cmd', 's3cmd --version')))
  193. checks.append(run_and_print('Checking command: deb-s3... ', partial(check_command_exists, 'deb-s3', 'deb-s3 -h')))
  194. checks.append(run_and_print('Checking command: rpm-s3... ', partial(check_command_exists, 'rpm-s3', 'rpm-s3 -h')))
  195. if check_only:
  196. sys.exit(0)
  197. if False in checks:
  198. print("Exiting due to failing checks")
  199. sys.exit(0)
  200. if __name__ == "__main__":
  201. parser = argparse.ArgumentParser(description='Builds and publishes a Elasticsearch Release')
  202. parser.add_argument('--deploy-sonatype', dest='deploy_sonatype', action='store_true',
  203. help='Installs and Deploys the release on a sonatype staging repository.')
  204. parser.add_argument('--deploy-s3', dest='deploy_s3', action='store_true',
  205. help='Pushes artifacts to the S3 staging area')
  206. parser.add_argument('--deploy-s3-repos', dest='deploy_s3_repos', action='store_true',
  207. help='Creates package repositories in S3 repo')
  208. parser.add_argument('--no-install', dest='no_install', action='store_true',
  209. help='Does not run "mvn install", expects this to be run already and reuses artifacts from local repo, only useful with --deploy-s3/--deploy-s3-repos, after sonatype deplomeny to ensure same artifacts')
  210. parser.add_argument('--skip-doc-check', dest='skip_doc_check', action='store_false',
  211. help='Skips any checks for pending documentation changes')
  212. parser.add_argument('--skip-tests', dest='skip_tests', action='store_true',
  213. help='Skips any test runs')
  214. parser.add_argument('--gpg-key', dest='gpg_key', default="D88E42B4",
  215. help='Allows you to specify a different gpg_key to be used instead of the default release key')
  216. parser.add_argument('--bucket', '-b', dest='bucket', default="download.elasticsearch.org",
  217. help='Allows you to specify a different s3 bucket to upload the artifacts to')
  218. parser.add_argument('--quiet', dest='quiet', action='store_true',
  219. help='Runs the script in quiet mode')
  220. parser.add_argument('--check', dest='check', action='store_true',
  221. help='Checks and reports for all requirements and then exits')
  222. # by default, we only run mvn install and don't push anything repo
  223. parser.set_defaults(deploy_sonatype=False)
  224. parser.set_defaults(deploy_s3=False)
  225. parser.set_defaults(deploy_s3_repos=False)
  226. parser.set_defaults(no_install=False)
  227. # other defaults
  228. parser.set_defaults(skip_doc_check=False)
  229. parser.set_defaults(quiet=False)
  230. parser.set_defaults(skip_tests=False)
  231. args = parser.parse_args()
  232. skip_doc_check = args.skip_doc_check
  233. gpg_key = args.gpg_key
  234. bucket = args.bucket
  235. deploy_sonatype = args.deploy_sonatype
  236. deploy_s3 = args.deploy_s3
  237. deploy_s3_repos = args.deploy_s3_repos
  238. run_mvn_install = not args.no_install
  239. skip_tests = args.skip_tests
  240. check_environment_and_commandline_tools(args.check)
  241. if not run_mvn_install and deploy_sonatype:
  242. print('Using --no-install and --deploy-sonatype together does not work. Exiting')
  243. sys.exit(-1)
  244. print('*** Preparing a release candidate: ', end='')
  245. print('deploy sonatype: %s%s%s' % (COLOR_OK if deploy_sonatype else COLOR_FAIL, 'yes' if deploy_sonatype else 'no', COLOR_END), end='')
  246. print(', deploy s3: %s%s%s' % (COLOR_OK if deploy_s3 else COLOR_FAIL, 'yes' if deploy_s3 else 'no', COLOR_END), end='')
  247. print(', deploy s3 repos: %s%s%s' % (COLOR_OK if deploy_s3_repos else COLOR_FAIL, 'yes' if deploy_s3_repos else 'no', COLOR_END), end='')
  248. print('')
  249. shortHash = subprocess.check_output('git log --pretty=format:"%h" -n 1', shell=True).decode('utf-8')
  250. releaseDirectory = os.getenv('HOME') + '/elastic-releases'
  251. release_version = find_release_version()
  252. localRepo = '%s/elasticsearch-%s-%s' % (releaseDirectory, release_version, shortHash)
  253. localRepoElasticsearch = localRepo + '/org/elasticsearch'
  254. ensure_checkout_is_clean()
  255. if not re.match('(\d+\.\d+)\.*',release_version):
  256. raise RuntimeError('illegal release version format: %s' % (release_version))
  257. package_repo_version = '%s.x' % re.match('(\d+)\.*', release_version).group(1)
  258. print('*** Preparing release version: [%s]' % release_version)
  259. if not skip_doc_check:
  260. print('*** Check for pending documentation changes')
  261. pending_files = update_reference_docs(release_version)
  262. if pending_files:
  263. raise RuntimeError('pending coming[%s] documentation changes found in %s' % (release_version, pending_files))
  264. run('cd dev-tools && mvn versions:set -DnewVersion=%s -DgenerateBackupPoms=false' % (release_version))
  265. run('cd rest-api-spec && mvn versions:set -DnewVersion=%s -DgenerateBackupPoms=false' % (release_version))
  266. run('mvn versions:set -DnewVersion=%s -DgenerateBackupPoms=false' % (release_version))
  267. remove_version_snapshot(VERSION_FILE, release_version)
  268. print('*** Done removing snapshot version. DO NOT COMMIT THIS, WHEN CREATING A RELEASE CANDIDATE.')
  269. if not os.path.exists(releaseDirectory):
  270. os.mkdir(releaseDirectory)
  271. if os.path.exists(localRepoElasticsearch) and run_mvn_install:
  272. print('clean local repository %s' % localRepoElasticsearch)
  273. shutil.rmtree(localRepoElasticsearch)
  274. mvn_target = 'deploy' if deploy_sonatype else 'install'
  275. tests = '-DskipTests' if skip_tests else '-Dskip.integ.tests=true'
  276. install_command = 'mvn clean %s -Prelease %s -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, tests, gpg_key, localRepo)
  277. clean_repo_command = 'find %s -name _remote.repositories -exec rm {} \;' % (localRepoElasticsearch)
  278. if not run_mvn_install:
  279. print('')
  280. print('*** By choosing --no-install we assume you ran the following commands successfully:')
  281. print(' %s' % (install_command))
  282. print(' 1. Remove all _remote.repositories: %s' % (clean_repo_command))
  283. rename_metadata_files_command = 'for i in $(find %s -name "maven-metadata-local.xml*") ; do mv "$i" "${i/-local/}" ; done' % (localRepoElasticsearch)
  284. print(' 2. Rename all maven metadata files: %s' % (rename_metadata_files_command))
  285. else:
  286. for cmd in [install_command, clean_repo_command]:
  287. run(cmd)
  288. rename_local_meta_files(localRepoElasticsearch)
  289. rpm = '%s/distribution/rpm/elasticsearch/%s/elasticsearch-%s.rpm' % (localRepoElasticsearch, release_version, release_version)
  290. print('Ensuring that RPM has been signed')
  291. ensure_rpm_is_signed(rpm, gpg_key)
  292. # repository push commands
  293. s3cmd_sync_to_staging_bucket_cmd = 's3cmd sync -P %s s3://%s/elasticsearch/staging/%s-%s/org/' % (localRepoElasticsearch, bucket, release_version, shortHash)
  294. s3_bucket_sync_to = '%s/elasticsearch/staging/%s-%s/repos/' % (bucket, release_version, shortHash)
  295. s3cmd_sync_official_repo_cmd = 's3cmd sync s3://packages.elasticsearch.org/elasticsearch/%s s3://%s' % (package_repo_version, s3_bucket_sync_to)
  296. debs3_prefix = 'elasticsearch/staging/%s-%s/repos/%s/debian' % (release_version, shortHash, package_repo_version)
  297. debs3_upload_cmd = 'deb-s3 upload --preserve-versions %s/distribution/deb/elasticsearch/%s/elasticsearch-%s.deb -b %s --prefix %s --sign %s --arch amd64' % (localRepoElasticsearch, release_version, release_version, bucket, debs3_prefix, gpg_key)
  298. debs3_list_cmd = 'deb-s3 list -b %s --prefix %s' % (bucket, debs3_prefix)
  299. debs3_verify_cmd = 'deb-s3 verify -b %s --prefix %s' % (bucket, debs3_prefix)
  300. rpms3_prefix = 'elasticsearch/staging/%s-%s/repos/%s/centos' % (release_version, shortHash, package_repo_version)
  301. # external-1 is the alias name for the us-east-1 region. This is used by rpm-s3 to construct the hostname
  302. rpms3_upload_cmd = 'rpm-s3 -v -b %s -p %s --sign --visibility public-read -k 100 %s -r external-1' % (bucket, rpms3_prefix, rpm)
  303. if deploy_s3:
  304. run(s3cmd_sync_to_staging_bucket_cmd)
  305. else:
  306. print('')
  307. print('*** To push a release candidate to s3 run: ')
  308. print(' 1. Sync %s into S3 bucket' % (localRepoElasticsearch))
  309. print (' %s' % (s3cmd_sync_to_staging_bucket_cmd))
  310. if deploy_s3_repos:
  311. print('*** Syncing official package repository into staging s3 bucket')
  312. run(s3cmd_sync_official_repo_cmd)
  313. print('*** Uploading debian package (you will be prompted for the passphrase!)')
  314. run(debs3_upload_cmd)
  315. run(debs3_list_cmd)
  316. run(debs3_verify_cmd)
  317. print('*** Uploading rpm package (you will be prompted for the passphrase!)')
  318. run(rpms3_upload_cmd)
  319. else:
  320. print('*** To create repositories on S3 run:')
  321. print(' 1. Sync existing repo into staging: %s' % s3cmd_sync_official_repo_cmd)
  322. print(' 2. Upload debian package (and sign it): %s' % debs3_upload_cmd)
  323. print(' 3. List all debian packages: %s' % debs3_list_cmd)
  324. print(' 4. Verify debian packages: %s' % debs3_verify_cmd)
  325. print(' 5. Upload RPM: %s' % rpms3_upload_cmd)
  326. print('')
  327. 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')
  328. 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:')
  329. print("""
  330. <profiles>
  331. <profile>
  332. <id>release</id>
  333. <properties>
  334. <gpg.passphrase>YourPasswordGoesHere</gpg.passphrase>
  335. </properties>
  336. </profile>
  337. </profiles>
  338. """)
  339. 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!')
  340. print('*** Once the release is deployed and published send out the following mail to dev@elastic.co:')
  341. string_format_dict = {'version' : release_version, 'hash': shortHash, 'package_repo_version' : package_repo_version, 'bucket': bucket}
  342. print(MAIL_TEMPLATE % string_format_dict)
  343. print('')
  344. print('You can verify that pushing to the staging repository pushed all the artifacts by running (log into sonatype to find out the correct id):')
  345. print(' python -B dev-tools/validate-maven-repository.py %s https://oss.sonatype.org/service/local/repositories/orgelasticsearch-IDTOFINDOUT/content/org/elasticsearch ' %(localRepoElasticsearch))
  346. print('')
  347. print('To publish the release and the repo on S3 execute the following commands:')
  348. print(' s3cmd cp --recursive s3://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/repos/%(package_repo_version)s/ s3://packages.elasticsearch.org/elasticsearch/%(package_repo_version)s' % string_format_dict)
  349. print(' s3cmd cp --recursive s3://%(bucket)s/elasticsearch/staging/%(version)s-%(hash)s/org/ s3://%(bucket)s/elasticsearch/release/org' % string_format_dict)
  350. print('Now go ahead and tag the release:')
  351. print(' git tag -a v%(version)s %(hash)s' % string_format_dict)
  352. print(' git push origin v%(version)s' % string_format_dict )