create-release.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  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 elyra
  19. import elyra._version
  20. import git
  21. import io
  22. import os
  23. import re
  24. import shutil
  25. import subprocess
  26. import sys
  27. from datetime import datetime
  28. from pathlib import Path
  29. from types import SimpleNamespace
  30. config: SimpleNamespace
  31. VERSION_REG_EX = r"(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(\.(?P<pre_release>[a-z]+)(?P<build>\d+))?"
  32. DEFAULT_GIT_ORG = "elyra-ai"
  33. DEFAULT_GIT_BRANCH = "master"
  34. DEFAULT_BUILD_DIR = "build/release"
  35. class DependencyException(Exception):
  36. """Error if dependency is missing"""
  37. class MissingReleaseArtifactException(Exception):
  38. """Error if an artifact being released is not available"""
  39. class UpdateVersionException(Exception):
  40. """Error if the old version is invalid or cannot be found, or if there's a duplicate version"""
  41. def check_run(args, cwd=os.getcwd(), capture_output=True, env=None, shell=False) -> subprocess.CompletedProcess:
  42. try:
  43. return subprocess.run(args, cwd=cwd, capture_output=capture_output, check=True)
  44. except subprocess.CalledProcessError as ex:
  45. raise RuntimeError(f'Error executing process: {ex.stderr.decode("unicode_escape")}') from ex
  46. def check_output(args, cwd=os.getcwd(), env=None, shell=False) -> str:
  47. response = check_run(args, cwd, capture_output=True, env=env, shell=shell)
  48. return response.stdout.decode("utf-8").replace("\n", "")
  49. def dependency_exists(command) -> bool:
  50. """Returns true if a command exists on the system"""
  51. try:
  52. check_run(["which", command])
  53. except subprocess.CalledProcessError:
  54. return False
  55. return True
  56. def sed(file: str, pattern: str, replace: str) -> None:
  57. """Perform regex substitution on a given file"""
  58. try:
  59. check_run(["sed", "-i", "", "-e", f"s#{pattern}#{replace}#g", file], capture_output=False)
  60. except Exception as ex:
  61. raise RuntimeError(f"Error processing updated to file {file}: ") from ex
  62. def validate_dependencies() -> None:
  63. """Error if a dependency is missing or invalid"""
  64. if not dependency_exists("git"):
  65. raise DependencyException("Please install git https://git-scm.com/downloads")
  66. if not dependency_exists("node"):
  67. raise DependencyException("Please install node.js v16+ https://nodejs.org/")
  68. if not dependency_exists("yarn"):
  69. raise DependencyException("Please install yarn https://classic.yarnpkg.com/")
  70. if not dependency_exists("twine"):
  71. raise DependencyException("Please install twine https://twine.readthedocs.io/en/latest/#installation")
  72. def validate_environment() -> None:
  73. """Validate environment configurations are valid"""
  74. pass
  75. def update_version_to_release() -> None:
  76. global config
  77. old_version = config.old_version
  78. old_npm_version = config.old_npm_version
  79. new_version = config.new_version
  80. new_npm_version = config.new_npm_version
  81. try:
  82. # Update backend version
  83. sed(_source(".bumpversion.cfg"), rf"^current_version* =* {old_version}", f"current_version = {new_version}")
  84. sed(_source("elyra/_version.py"), rf'^__version__* =* "{old_version}"', f'__version__ = "{new_version}"'),
  85. sed(_source("README.md"), rf"elyra {old_version}", f"elyra {new_version}")
  86. sed(_source("docs/source/getting_started/installation.md"), rf"elyra {old_version}", f"elyra {new_version}")
  87. # Update docker related tags
  88. sed(_source("Makefile"), r"^TAG:=dev", f"TAG:={new_version}")
  89. sed(_source("README.md"), r"elyra:dev ", f"elyra:{new_version} ")
  90. if config.rc is None and config.beta is None:
  91. # Update the stable version Binder link
  92. sed(_source("README.md"), r"/v[0-9].[0-9].[0-9]?", f"/v{new_version}?")
  93. sed(_source("etc/docker/kubeflow/README.md"), r"kf-notebook:dev", f"kf-notebook:{new_version}")
  94. sed(_source("docs/source/getting_started/installation.md"), r"elyra:dev ", f"elyra:{new_version} ")
  95. sed(_source("docs/source/getting_started/installation.md"), r"/v[0-9].[0-9].[0-9]?", f"/v{new_version}?")
  96. sed(_source("docs/source/recipes/configure-airflow-as-a-runtime.md"), r"master", f"{config.tag}")
  97. sed(_source("docs/source/recipes/deploying-elyra-in-a-jupyterhub-environment.md"), r"dev", f"{new_version}")
  98. sed(_source("docs/source/recipes/using-elyra-with-kubeflow-notebook-server.md"), r"master", f"{new_version}")
  99. # Update UI component versions
  100. sed(_source("README.md"), rf"v{old_npm_version}", f"v{new_version}")
  101. sed(_source("docs/source/getting_started/installation.md"), rf"v{old_npm_version}", f"v{new_version}")
  102. sed(
  103. _source("packages/theme/src/index.ts"),
  104. r"https://elyra.readthedocs.io/en/latest/",
  105. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  106. )
  107. sed(
  108. _source("elyra/cli/pipeline_app.py"),
  109. r"https://elyra.readthedocs.io/en/latest/",
  110. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  111. )
  112. # Update documentation version for elyra-metadata cli help
  113. sed(
  114. _source("elyra/metadata/metadata_app_utils.py"),
  115. r"https://elyra.readthedocs.io/en/latest/",
  116. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  117. )
  118. sed(
  119. _source("packages/pipeline-editor/src/EmptyPipelineContent.tsx"),
  120. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  121. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  122. )
  123. sed(
  124. _source("packages/pipeline-editor/src/PipelineEditorWidget.tsx"),
  125. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  126. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  127. )
  128. # update documentation references in schema definitions
  129. # located in elyra/metadata/schemas/
  130. sed(
  131. _source("elyra/metadata/schemas/url-catalog.json"),
  132. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  133. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  134. )
  135. sed(
  136. _source("elyra/metadata/schemas/local-directory-catalog.json"),
  137. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  138. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  139. )
  140. sed(
  141. _source("elyra/metadata/schemas/local-file-catalog.json"),
  142. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  143. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  144. )
  145. sed(
  146. _source("elyra/metadata/schemas/airflow.json"),
  147. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  148. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  149. )
  150. sed(
  151. _source("elyra/metadata/schemas/kfp.json"),
  152. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  153. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  154. )
  155. sed(
  156. _source("elyra/metadata/schemas/code-snippet.json"),
  157. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  158. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  159. )
  160. sed(
  161. _source("elyra/metadata/schemas/runtime-image.json"),
  162. r"https://elyra.readthedocs.io/en/latest/user_guide/",
  163. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  164. )
  165. # Update documentation references in documentation
  166. sed(
  167. _source("docs/source/user_guide/jupyterlab-interface.md"),
  168. r"https://elyra.readthedocs.io/en/latest/",
  169. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  170. )
  171. # Update GitHub references in documentation
  172. sed(
  173. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  174. r"elyra-ai/elyra/master/etc/kfp/pip.conf",
  175. rf"elyra-ai/elyra/v{new_version}/etc/kfp/pip.conf/",
  176. )
  177. sed(
  178. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  179. r"elyra-ai/elyra/master/elyra/kfp/bootstrapper.py",
  180. rf"elyra-ai/elyra/v{new_version}/elyra/kfp/bootstrapper.py",
  181. )
  182. sed(
  183. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  184. r"elyra-ai/elyra/master/elyra/airflow/bootstrapper.py",
  185. rf"elyra-ai/elyra/v{new_version}/elyra/airflow/bootstrapper.py",
  186. )
  187. sed(
  188. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  189. r"elyra-ai/elyra/master/etc/generic/requirements-elyra-py37.txt",
  190. rf"elyra-ai/elyra/v{new_version}/etc/generic/requirements-elyra-py37.txt",
  191. )
  192. sed(
  193. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  194. r"elyra-ai/elyra/master/etc/generic/requirements-elyra.txt",
  195. rf"elyra-ai/elyra/v{new_version}/etc/generic/requirements-elyra.txt",
  196. )
  197. check_run(
  198. ["lerna", "version", new_npm_version, "--no-git-tag-version", "--no-push", "--yes", "--exact"],
  199. cwd=config.source_dir,
  200. )
  201. check_run(["yarn", "version", "--new-version", new_npm_version, "--no-git-tag-version"], cwd=config.source_dir)
  202. except Exception as ex:
  203. raise UpdateVersionException from ex
  204. def update_version_to_dev() -> None:
  205. global config
  206. new_version = config.new_version
  207. dev_version = config.dev_version
  208. dev_npm_version = config.dev_npm_version
  209. try:
  210. # Update backend version
  211. sed(_source(".bumpversion.cfg"), rf"^current_version* =* {new_version}", f"current_version = {dev_version}")
  212. sed(_source("elyra/_version.py"), rf'^__version__* =* "{new_version}"', f'__version__ = "{dev_version}"')
  213. sed(_source("README.md"), rf"elyra {new_version}", f"elyra {dev_version}")
  214. sed(_source("docs/source/getting_started/installation.md"), rf"elyra {new_version}", f"elyra {dev_version}")
  215. # Update docker related tags
  216. sed(_source("Makefile"), rf"^TAG:={new_version}", "TAG:=dev")
  217. sed(_source("README.md"), rf"elyra:{new_version} ", "elyra:dev ")
  218. sed(_source("etc/docker/kubeflow/README.md"), rf"kf-notebook:{new_version}", "kf-notebook:dev")
  219. # this does not goes back to dev
  220. # sed(source('README.md'), rf"/v[0-9].[0-9].[0-9]", "/v{dev_version}")
  221. sed(_source("docs/source/getting_started/installation.md"), rf"elyra:{new_version} ", "elyra:dev ")
  222. # this does not goes back to dev
  223. # sed(source('docs/source/getting_started/installation.md'), rf"/v[0-9].[0-9].[0-9]", "/v{dev_version}")
  224. sed(_source("docs/source/recipes/configure-airflow-as-a-runtime.md"), rf"{config.tag}", "master")
  225. sed(_source("docs/source/recipes/deploying-elyra-in-a-jupyterhub-environment.md"), rf"{new_version}", "dev")
  226. sed(_source("docs/source/recipes/using-elyra-with-kubeflow-notebook-server.md"), rf"{new_version}", "master")
  227. # Update UI component versions
  228. sed(_source("README.md"), rf"extension v{new_version}", f"extension v{dev_npm_version}")
  229. sed(
  230. _source("docs/source/getting_started/installation.md"),
  231. rf"extension v{new_version}",
  232. f"extension v{dev_npm_version}",
  233. )
  234. sed(
  235. _source("packages/theme/src/index.ts"),
  236. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  237. rf"https://elyra.readthedocs.io/en/latest/",
  238. )
  239. # Update documentation references in documentation
  240. sed(
  241. _source("docs/source/user_guide/jupyterlab-interface.md"),
  242. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  243. r"https://elyra.readthedocs.io/en/latest/",
  244. )
  245. sed(
  246. _source("elyra/cli/pipeline_app.py"),
  247. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  248. rf"https://elyra.readthedocs.io/en/latest/",
  249. )
  250. # Update documentation version for elyra-metadata cli help
  251. sed(
  252. _source("elyra/metadata/metadata_app_utils.py"),
  253. rf"https://elyra.readthedocs.io/en/latest/",
  254. rf"https://elyra.readthedocs.io/en/v{new_version}/",
  255. )
  256. sed(
  257. _source("packages/pipeline-editor/src/EmptyPipelineContent.tsx"),
  258. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  259. rf"https://elyra.readthedocs.io/en/latest/user_guide/",
  260. )
  261. sed(
  262. _source("packages/pipeline-editor/src/PipelineEditorWidget.tsx"),
  263. rf"https://elyra.readthedocs.io/en/v{new_version}/user_guide/",
  264. rf"https://elyra.readthedocs.io/en/latest/user_guide/",
  265. )
  266. # Update GitHub references in documentation
  267. sed(
  268. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  269. r"elyra-ai/elyra/v{new_version}/etc/kfp/pip.conf",
  270. rf"elyra-ai/elyra/master/etc/kfp/pip.conf/",
  271. )
  272. sed(
  273. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  274. r"elyra-ai/elyra/v{new_version}/elyra/kfp/bootstrapper.py",
  275. rf"elyra-ai/elyra/master/elyra/kfp/bootstrapper.py",
  276. )
  277. sed(
  278. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  279. r"elyra-ai/elyra/v{new_version}/elyra/airflow/bootstrapper.py",
  280. rf"elyra-ai/elyra/master/elyra/airflow/bootstrapper.py",
  281. )
  282. sed(
  283. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  284. r"elyra-ai/elyra/v{new_version}/etc/generic/requirements-elyra-py37.txt",
  285. rf"elyra-ai/elyra/master/etc/generic/requirements-elyra-py37.txt",
  286. )
  287. sed(
  288. _source("docs/source/recipes/running-elyra-in-air-gapped-environment.md"),
  289. r"elyra-ai/elyra/v{new_version}/etc/generic/requirements-elyra.txt",
  290. rf"elyra-ai/elyra/master/etc/generic/requirements-elyra.txt",
  291. )
  292. check_run(
  293. ["lerna", "version", dev_npm_version, "--no-git-tag-version", "--no-push", "--yes", "--exact"],
  294. cwd=config.source_dir,
  295. )
  296. check_run(["yarn", "version", "--new-version", dev_npm_version, "--no-git-tag-version"], cwd=config.source_dir)
  297. except Exception as ex:
  298. raise UpdateVersionException from ex
  299. def _source(file: str) -> str:
  300. global config
  301. return os.path.join(config.source_dir, file)
  302. def checkout_code() -> None:
  303. global config
  304. print("-----------------------------------------------------------------")
  305. print("-------------------- Retrieving source code ---------------------")
  306. print("-----------------------------------------------------------------")
  307. print(f"Cloning repository: {config.git_url}")
  308. if os.path.exists(config.work_dir):
  309. print(f"Removing working directory: {config.work_dir}")
  310. shutil.rmtree(config.work_dir)
  311. print(f"Creating working directory: {config.work_dir}")
  312. os.makedirs(config.work_dir)
  313. print(f"Cloning : {config.git_url} to {config.work_dir}")
  314. check_run(["git", "clone", config.git_url, "-b", config.git_branch], cwd=config.work_dir)
  315. check_run(["git", "config", "user.name", config.git_user_name], cwd=config.source_dir)
  316. check_run(["git", "config", "user.email", config.git_user_email], cwd=config.source_dir)
  317. print("")
  318. def build_release():
  319. global config
  320. print("-----------------------------------------------------------------")
  321. print("----------------------- Building Release ------------------------")
  322. print("-----------------------------------------------------------------")
  323. check_run(["make", "release"], cwd=config.source_dir, capture_output=False)
  324. print("")
  325. def build_server():
  326. global config
  327. print("-----------------------------------------------------------------")
  328. print("------------------------ Building Server ------------------------")
  329. print("-----------------------------------------------------------------")
  330. # update project name
  331. sed(_source("setup.py"), r'name="elyra"', 'name="elyra-server"')
  332. sed(
  333. _source("setup.py"),
  334. r'description="Elyra provides AI Centric extensions to JupyterLab"',
  335. 'description="The elyra-server package provides common core libraries and functions that are required by '
  336. "Elyra's individual extensions. Note: Installing this package alone will not enable the use of Elyra. "
  337. "Please install the 'elyra' package instead. e.g. pip install elyra[all]\"",
  338. )
  339. # build server wheel
  340. check_run(["make", "build-server"], cwd=config.source_dir, capture_output=False)
  341. # revert project name
  342. check_run(["git", "reset", "--hard"], cwd=config.source_dir, capture_output=False)
  343. print("")
  344. def show_release_artifacts():
  345. global config
  346. dist_dir = os.path.join(config.source_dir, "dist")
  347. print("-----------------------------------------------------------------")
  348. print("------------------------ Release Files --------------------------")
  349. print("-----------------------------------------------------------------")
  350. print("")
  351. print(f"Location \t {dist_dir}")
  352. print("")
  353. check_run(["ls", "-la", dist_dir], capture_output=False)
  354. print("")
  355. def copy_extension_dir(extension: str, work_dir: str) -> None:
  356. global config
  357. extension_package_source_dir = os.path.join(config.source_dir, "dist/labextensions/@elyra", extension)
  358. extension_package_dest_dir = os.path.join(work_dir, "dist/labextensions/@elyra", extension)
  359. os.makedirs(os.path.dirname(extension_package_dest_dir), exist_ok=True)
  360. shutil.copytree(extension_package_source_dir, extension_package_dest_dir)
  361. def generate_changelog() -> None:
  362. global config
  363. print("-----------------------------------------------------------------")
  364. print("--------------------- Preparing Changelog -----------------------")
  365. print("-----------------------------------------------------------------")
  366. changelog_path = os.path.join(config.source_dir, "docs/source/getting_started/changelog.md")
  367. changelog_backup_path = os.path.join(config.source_dir, "docs/source/getting_started/changelog.bak")
  368. if os.path.exists(changelog_backup_path):
  369. os.remove(changelog_backup_path)
  370. shutil.copy(changelog_path, changelog_backup_path)
  371. repo = git.Repo(config.source_dir)
  372. # define static header
  373. header_lines = [
  374. "# Changelog\n",
  375. "\n",
  376. "A summary of new feature highlights is located on the [GitHub release page](https://github.com/elyra-ai/elyra/releases).\n",
  377. "\n",
  378. ]
  379. # Start generating the release header on top of the changelog
  380. with io.open(changelog_path, "r+") as changelog:
  381. # add static header
  382. for line in header_lines:
  383. changelog.write(line)
  384. # add release section
  385. changelog.write(f'## Release {config.new_version} - {datetime.now().strftime("%m/%d/%Y")}\n')
  386. changelog.write("\n")
  387. start = 0
  388. page_size = 10
  389. continue_paginating = True
  390. while continue_paginating:
  391. # paginate the list of commits until it finds the begining of release changes
  392. # which is denominated by a commit titled 'Prepare for next development iteration'
  393. commits = list(repo.iter_commits(max_count=page_size, skip=start))
  394. start += page_size
  395. for commit in commits:
  396. # for each commit, get it's title and prepare a changelog
  397. # entry linking to the related pull request
  398. commit_title = commit.message.splitlines()[0]
  399. # commit_hash = commit.hexsha
  400. # print(f'>>> {commit_hash} - {commit_title}')
  401. if commit_title != "Prepare for next development iteration":
  402. pr_string = ""
  403. pr = re.findall("\(#(.*?)\)", commit_title)
  404. if pr:
  405. commit_title = re.sub("\(#(.*?)\)", "", commit_title).strip()
  406. pr_string = f" - [#{pr[0]}](https://github.com/elyra-ai/elyra/pull/{pr[0]})"
  407. changelog_entry = f"- {commit_title}{pr_string}\n"
  408. changelog.write(changelog_entry)
  409. else:
  410. # here it found the first commit of the release
  411. # changelog for the release is done
  412. # exit the loop
  413. continue_paginating = False
  414. break
  415. # copy the remaining changelog at the bottom of the new content
  416. with io.open(changelog_backup_path) as old_changelog:
  417. # ignore existing static header
  418. line = old_changelog.readline()
  419. while line and line.startswith("## Release") is False:
  420. line = old_changelog.readline()
  421. changelog.write("\n")
  422. while line:
  423. changelog.write(line)
  424. line = old_changelog.readline()
  425. def prepare_extensions_release() -> None:
  426. global config
  427. print("-----------------------------------------------------------------")
  428. print("--------------- Preparing Individual Extensions -----------------")
  429. print("-----------------------------------------------------------------")
  430. extensions = {
  431. "elyra-code-snippet-extension": SimpleNamespace(
  432. packages=["code-snippet-extension", "metadata-extension", "theme-extension"],
  433. description=f"The Code Snippet editor extension adds support for reusable code fragments, "
  434. f"making programming in JupyterLab more efficient by reducing repetitive work. "
  435. f"See https://elyra.readthedocs.io/en/{config.new_version}/user_guide/code-snippets.html",
  436. ),
  437. "elyra-code-viewer-extension": SimpleNamespace(
  438. packages=["code-viewer-extension"],
  439. description="The Code Viewer extension adds the ability to display a given chunk of code "
  440. "(string) in a transient read-only 'editor' without needing to create a file."
  441. "This extension will be available in JupyterLab core in a near future release and removed "
  442. "from Elyra as a standalone extension.",
  443. ),
  444. "elyra-pipeline-editor-extension": SimpleNamespace(
  445. packages=["code-viewer-extension", "pipeline-editor-extension", "metadata-extension", "theme-extension"],
  446. description=f"The Visual Editor Pipeline extension is used to build AI pipelines from notebooks, "
  447. f"Python scripts and R scripts, simplifying the conversion of multiple notebooks "
  448. f"or script files into batch jobs or workflows."
  449. f"See https://elyra.readthedocs.io/en/{config.new_version}/user_guide/pipelines.html",
  450. ),
  451. "elyra-python-editor-extension": SimpleNamespace(
  452. packages=["python-editor-extension", "metadata-extension", "theme-extension"],
  453. description=f"The Python Script editor extension contains support for Python files, "
  454. f"which can take advantage of the Hybrid Runtime Support enabling users to "
  455. f"locally edit .py scripts and execute them against local or cloud-based resources."
  456. f"See https://elyra.readthedocs.io/en/{config.new_version}/user_guide/enhanced-script-support.html",
  457. ),
  458. "elyra-r-editor-extension": SimpleNamespace(
  459. packages=["r-editor-extension", "metadata-extension", "theme-extension"],
  460. description=f"The R Script editor extension contains support for R files, which can take "
  461. f"advantage of the Hybrid Runtime Support enabling users to locally edit .R scripts "
  462. f"and execute them against local or cloud-based resources."
  463. f"See https://elyra.readthedocs.io/en/{config.new_version}/user_guide/enhanced-script-support.html",
  464. ),
  465. }
  466. for extension in extensions:
  467. extension_source_dir = os.path.join(config.work_dir, extension)
  468. print(f"Preparing extension : {extension} at {extension_source_dir}")
  469. # copy extension package template to working directory
  470. if os.path.exists(extension_source_dir):
  471. print(f"Removing working directory: {config.source_dir}")
  472. shutil.rmtree(extension_source_dir)
  473. check_run(["mkdir", "-p", extension_source_dir], cwd=config.work_dir)
  474. print(f'Copying : {_source("etc/templates/setup.py")} to {extension_source_dir}')
  475. check_run(["cp", _source("etc/templates/setup.py"), extension_source_dir], cwd=config.work_dir)
  476. # update template
  477. setup_file = os.path.join(extension_source_dir, "setup.py")
  478. sed(setup_file, "{{package-name}}", extension)
  479. sed(setup_file, "{{version}}", config.new_version)
  480. sed(setup_file, "{{data - files}}", re.escape("('share/jupyter/labextensions', 'dist/labextensions', '**')"))
  481. sed(setup_file, "{{install - requires}}", f"'elyra-server=={config.new_version}',")
  482. sed(setup_file, "{{description}}", f"'{extensions[extension].description}'")
  483. for dependency in extensions[extension].packages:
  484. copy_extension_dir(dependency, extension_source_dir)
  485. # build extension
  486. check_run(["python", "setup.py", "bdist_wheel", "sdist"], cwd=extension_source_dir)
  487. print("")
  488. def prepare_runtime_extensions_package_release() -> None:
  489. global config
  490. print("-----------------------------------------------------------------")
  491. print("---------------- Preparing Individual Packages ------------------")
  492. print("-----------------------------------------------------------------")
  493. packages = {"kfp-notebook": ["kfp>=1.6.3"], "airflow-notebook": ["pygithub", "black"]}
  494. packages_source = {"kfp-notebook": "kfp", "airflow-notebook": "airflow"}
  495. for package in packages:
  496. package_source_dir = os.path.join(config.work_dir, package)
  497. print(f"Preparing package : {package} at {package_source_dir}")
  498. # copy extension package template to working directory
  499. if os.path.exists(package_source_dir):
  500. print(f"Removing working directory: {config.source_dir}")
  501. shutil.rmtree(package_source_dir)
  502. check_run(["mkdir", "-p", package_source_dir], cwd=config.work_dir)
  503. print(f'Copying : {_source("etc/templates/setup.py")} to {package_source_dir}')
  504. check_run(["cp", _source("etc/templates/setup.py"), package_source_dir], cwd=config.work_dir)
  505. # update template
  506. setup_file = os.path.join(package_source_dir, "setup.py")
  507. sed(setup_file, "{{package-name}}", package)
  508. sed(setup_file, "{{version}}", config.new_version)
  509. # no data files
  510. sed(setup_file, "{{data - files}}", "")
  511. # prepare package specific dependencies
  512. requires = ""
  513. for dependency in packages[package]:
  514. requires += f"'{dependency}',"
  515. sed(setup_file, "{{install - requires}}", requires)
  516. # copy source files
  517. source_dir = os.path.join(config.source_dir, "elyra", packages_source[package])
  518. dest_dir = os.path.join(package_source_dir, "elyra", packages_source[package])
  519. print(f"Copying package source from {source_dir} to {dest_dir}")
  520. Path(os.path.join(package_source_dir, "elyra")).mkdir(parents=True, exist_ok=True)
  521. shutil.copytree(source_dir, dest_dir)
  522. # build extension
  523. check_run(["python", "setup.py", "bdist_wheel", "sdist"], cwd=package_source_dir)
  524. print("")
  525. def prepare_changelog() -> None:
  526. """
  527. Prepare a release changelog
  528. """
  529. global config
  530. print(f"Generating changelog for release {config.new_version}")
  531. print("")
  532. # clone repository
  533. checkout_code()
  534. # generate changelog with new release list of commits
  535. generate_changelog()
  536. # commit
  537. check_run(
  538. ["git", "commit", "-a", "-m", f"Update changelog for release {config.new_version}"], cwd=config.source_dir
  539. )
  540. def prepare_release() -> None:
  541. """
  542. Prepare a release
  543. """
  544. global config
  545. print(f"Processing release from {config.old_version} to {config.new_version} ")
  546. print("")
  547. # clone repository
  548. checkout_code()
  549. # generate changelog with new release list of commits
  550. prepare_changelog()
  551. # Update to new release version
  552. update_version_to_release()
  553. # commit and tag
  554. check_run(["git", "commit", "-a", "-m", f"Release v{config.new_version}"], cwd=config.source_dir)
  555. check_run(["git", "tag", config.tag], cwd=config.source_dir)
  556. # server-only wheel
  557. build_server()
  558. # build release wheel and npm artifacts
  559. build_release()
  560. # show built release artifacts
  561. show_release_artifacts()
  562. # back to development
  563. update_version_to_dev()
  564. # commit
  565. check_run(["git", "commit", "-a", "-m", f"Prepare for next development iteration"], cwd=config.source_dir)
  566. # prepare extensions
  567. prepare_extensions_release()
  568. # prepare runtime extsnsions
  569. prepare_runtime_extensions_package_release()
  570. def publish_release(working_dir) -> None:
  571. global config
  572. files_to_publish = [
  573. f"{config.source_dir}/dist/elyra-{config.new_version}-py3-none-any.whl",
  574. f"{config.source_dir}/dist/elyra-{config.new_version}.tar.gz",
  575. f"{config.source_dir}/dist/elyra_server-{config.new_version}-py3-none-any.whl",
  576. f"{config.source_dir}/dist/elyra-server-{config.new_version}.tar.gz",
  577. f"{config.work_dir}/airflow-notebook/dist/airflow_notebook-{config.new_version}-py3-none-any.whl",
  578. f"{config.work_dir}/airflow-notebook/dist/airflow-notebook-{config.new_version}.tar.gz",
  579. f"{config.work_dir}/kfp-notebook/dist/kfp_notebook-{config.new_version}-py3-none-any.whl",
  580. f"{config.work_dir}/kfp-notebook/dist/kfp-notebook-{config.new_version}.tar.gz",
  581. f"{config.work_dir}/elyra-code-snippet-extension/dist/elyra_code_snippet_extension-{config.new_version}-py3-none-any.whl",
  582. f"{config.work_dir}/elyra-code-snippet-extension/dist/elyra-code-snippet-extension-{config.new_version}.tar.gz",
  583. f"{config.work_dir}/elyra-code-viewer-extension/dist/elyra_code_viewer_extension-{config.new_version}-py3-none-any.whl",
  584. f"{config.work_dir}/elyra-code-viewer-extension/dist/elyra-code-viewer-extension-{config.new_version}.tar.gz",
  585. f"{config.work_dir}/elyra-pipeline-editor-extension/dist/elyra_pipeline_editor_extension-{config.new_version}-py3-none-any.whl",
  586. f"{config.work_dir}/elyra-pipeline-editor-extension/dist/elyra-pipeline-editor-extension-{config.new_version}.tar.gz",
  587. f"{config.work_dir}/elyra-python-editor-extension/dist/elyra_python_editor_extension-{config.new_version}-py3-none-any.whl",
  588. f"{config.work_dir}/elyra-python-editor-extension/dist/elyra-python-editor-extension-{config.new_version}.tar.gz",
  589. f"{config.work_dir}/elyra-r-editor-extension/dist/elyra_r_editor_extension-{config.new_version}-py3-none-any.whl",
  590. f"{config.work_dir}/elyra-r-editor-extension/dist/elyra-r-editor-extension-{config.new_version}.tar.gz",
  591. ]
  592. print("-----------------------------------------------------------------")
  593. print("---------------------- Publishing to PyPI -----------------------")
  594. print("-----------------------------------------------------------------")
  595. # Validate all artifacts to be published are available
  596. for file in files_to_publish:
  597. if not os.path.exists(file):
  598. raise MissingReleaseArtifactException(f"Missing release file: {file}")
  599. # push files to PyPI
  600. for file in files_to_publish:
  601. print(f"Publishing: {file}")
  602. check_run(["twine", "upload", "--sign", file], cwd=working_dir)
  603. print("-----------------------------------------------------------------")
  604. print("--------------- Pushing Release and Tag to git ------------------")
  605. print("-----------------------------------------------------------------")
  606. # push release and tags to git
  607. print()
  608. print("Pushing release to git")
  609. check_run(["git", "push"], cwd=config.source_dir)
  610. print("Pushing release tag to git")
  611. check_run(["git", "push", "--tags"], cwd=config.source_dir)
  612. print("-----------------------------------------------------------------")
  613. print("--------------- Preparing to push npm packages ------------------")
  614. print("-----------------------------------------------------------------")
  615. # checkout the tag
  616. print()
  617. print(f"Checking out release tag {config.tag}")
  618. check_run(["git", "checkout", config.tag], cwd=config.source_dir)
  619. check_run(["git", "status"], cwd=config.source_dir)
  620. print("-----------------------------------------------------------------")
  621. print("-------------------- Pushing npm packages -----------------------")
  622. print("-----------------------------------------------------------------")
  623. # publish npm packages
  624. print()
  625. print(f"publishing npm packages")
  626. check_run(
  627. ["lerna", "publish", "--yes", "from-package", "--no-git-tag-version", "--no-verify-access", "--no-push"],
  628. cwd=config.source_dir,
  629. )
  630. def initialize_config(args=None) -> SimpleNamespace:
  631. if not args:
  632. raise ValueError("Invalid command line arguments")
  633. v = re.search(VERSION_REG_EX, elyra._version.__version__)
  634. configuration = {
  635. "goal": args.goal,
  636. "git_url": f"git@github.com:{args.org or DEFAULT_GIT_ORG}/elyra.git",
  637. "git_branch": args.branch or DEFAULT_GIT_BRANCH,
  638. "git_hash": "HEAD",
  639. "git_user_name": check_output(["git", "config", "user.name"]),
  640. "git_user_email": check_output(["git", "config", "user.email"]),
  641. "base_dir": os.getcwd(),
  642. "work_dir": os.path.join(os.getcwd(), DEFAULT_BUILD_DIR),
  643. "source_dir": os.path.join(os.getcwd(), DEFAULT_BUILD_DIR, "elyra"),
  644. "old_version": elyra._version.__version__,
  645. "old_npm_version": f"{v['major']}.{v['minor']}.{v['patch']}-dev",
  646. "new_version": args.version
  647. if (not args.rc or not str.isdigit(args.rc)) and (not args.beta or not str.isdigit(args.beta))
  648. else f"{args.version}rc{args.rc}"
  649. if args.rc
  650. else f"{args.version}b{args.beta}",
  651. "new_npm_version": args.version
  652. if (not args.rc or not str.isdigit(args.rc)) and (not args.beta or not str.isdigit(args.beta))
  653. else f"{args.version}-rc.{args.rc}"
  654. if args.rc
  655. else f"{args.version}-beta.{args.beta}",
  656. "rc": args.rc,
  657. "beta": args.beta,
  658. "dev_version": f"{args.dev_version}.dev0",
  659. "dev_npm_version": f"{args.dev_version}-dev",
  660. "tag": f"v{args.version}"
  661. if (not args.rc or not str.isdigit(args.rc)) and (not args.beta or not str.isdigit(args.beta))
  662. else f"v{args.version}rc{args.rc}"
  663. if args.rc
  664. else f"v{args.version}b{args.beta}",
  665. }
  666. global config
  667. config = SimpleNamespace(**configuration)
  668. def print_config() -> None:
  669. global config
  670. print("")
  671. print("-----------------------------------------------------------------")
  672. print("--------------------- Release configuration ---------------------")
  673. print("-----------------------------------------------------------------")
  674. print(f"Goal \t\t\t -> {config.goal}")
  675. print(f"Git URL \t\t -> {config.git_url}")
  676. print(f"Git Branch \t\t -> {config.git_branch}")
  677. print(f"Git reference \t\t -> {config.git_hash}")
  678. print(f"Git user \t\t -> {config.git_user_name}")
  679. print(f"Git user email \t\t -> {config.git_user_email}")
  680. print(f"Work dir \t\t -> {config.work_dir}")
  681. print(f"Source dir \t\t -> {config.source_dir}")
  682. print(f"Old Version \t\t -> {config.old_version}")
  683. print(f"Old NPM Version \t -> {config.old_npm_version}")
  684. print(f"New Version \t\t -> {config.new_version}")
  685. print(f"New NPN Version \t -> {config.new_npm_version}")
  686. if config.rc is not None:
  687. print(f"RC number \t\t -> {config.rc}")
  688. if config.beta is not None:
  689. print(f"Beta number \t\t -> {config.beta}")
  690. print(f"Dev Version \t\t -> {config.dev_version}")
  691. print(f"Dev NPM Version \t -> {config.dev_npm_version}")
  692. print(f"Release Tag \t\t -> {config.tag}")
  693. print("-----------------------------------------------------------------")
  694. print("")
  695. def print_help() -> str:
  696. return """create-release.py [ prepare | publish ] --version VERSION
  697. DESCRIPTION
  698. Creates Elyra release based on git commit hash or from HEAD.
  699. create release prepare-changelog --version 1.3.0 [--beta 0] [--rc 0]
  700. This will prepare the release changelog and make it ready for review on the release workdir.
  701. create-release.py prepare --version 1.3.0 --dev-version 1.4.0 [--beta 0] [--rc 0]
  702. This will prepare a release candidate, build it locally and make it ready for review on the release workdir.
  703. Note: that one can either use a beta or rc modifier for the release, but not both.
  704. create-release.py publish --version 1.3.0 [--beta 0] [--rc 0]
  705. This will build a previously prepared release, and publish the artifacts to public repositories.
  706. Required software dependencies for building and publishing a release:
  707. - Git
  708. - Node
  709. - Twine
  710. - Yarn
  711. Required configurations for publishing a release:
  712. - GPG with signing key configured
  713. """
  714. def main(args=None):
  715. """Perform necessary tasks to create and/or publish a new release"""
  716. parser = argparse.ArgumentParser(usage=print_help())
  717. parser.add_argument(
  718. "goal",
  719. help="Supported goals: {prepare-changelog | prepare | publish}",
  720. type=str,
  721. choices={"prepare-changelog", "prepare", "publish"},
  722. )
  723. parser.add_argument("--version", help="the new release version", type=str, required=True)
  724. parser.add_argument("--dev-version", help="the new development version", type=str, required=False)
  725. parser.add_argument("--beta", help="the release beta number", type=str, required=False)
  726. parser.add_argument("--rc", help="the release candidate number", type=str, required=False)
  727. parser.add_argument("--org", help="the github org or username to use", type=str, required=False)
  728. parser.add_argument("--branch", help="the branch name to use", type=str, required=False)
  729. args = parser.parse_args()
  730. # can't use both rc and beta parameters
  731. if args.beta and args.rc:
  732. print_help()
  733. sys.exit(1)
  734. global config
  735. try:
  736. # Validate all pre-requisites are available
  737. validate_dependencies()
  738. validate_environment()
  739. # Generate release config based on the provided arguments
  740. initialize_config(args)
  741. print_config()
  742. if config.goal == "prepare-changelog":
  743. prepare_changelog()
  744. print("")
  745. print("")
  746. print(f"Changelog for release version: {config.new_version} is ready for review at {config.source_dir}")
  747. print("After you are done, push the reviewed changelog to github.")
  748. print("")
  749. print("")
  750. elif config.goal == "prepare":
  751. if not args.dev_version:
  752. print_help()
  753. sys.exit()
  754. prepare_release()
  755. print("")
  756. print("")
  757. print(f"Release version: {config.new_version} is ready for review")
  758. print("After you are done, run the script again to [publish] the release.")
  759. print("")
  760. print("")
  761. elif args.goal == "publish":
  762. publish_release(working_dir=os.getcwd())
  763. else:
  764. print_help()
  765. sys.exit()
  766. except Exception as ex:
  767. raise RuntimeError(f"Error performing release {args.version}") from ex
  768. if __name__ == "__main__":
  769. main()