create-release.py 44 KB

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