ci/release.py (243 lines of code) (raw):

# Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. import argparse import logging import os import re import shutil import subprocess logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) PROJECT_ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../") def prepare(v: str): """Create a new release branch""" logger.info("Start to prepare release branch for version %s", v) _check_release_version(v) os.chdir(PROJECT_ROOT_DIR) branch = f"releases-{v}" try: subprocess.check_call(f"git checkout -b {branch}", shell=True) bump_version(version=v, l="all") subprocess.check_call("git add -u", shell=True) subprocess.check_call(f"git commit -m 'prepare release for {v}'", shell=True) except BaseException: logger.exception("Prepare branch failed") subprocess.check_call(f"git checkout - && git branch -D {branch}", shell=True) raise def build(v: str): """version format: 0.5.1""" logger.info("Start to prepare release artifacts for version %s", v) _check_release_version(v) os.chdir(PROJECT_ROOT_DIR) if os.path.exists("dist"): shutil.rmtree("dist") os.mkdir("dist") subprocess.check_call(f"git checkout releases-{v}", shell=True) branch = f"releases-{v}" src_tar = f"apache-fury-{v}-incubating-src.tar.gz" _check_all_committed() _strip_unnecessary_license() subprocess.check_call( "git add LICENSE && git commit -m 'remove benchmark from license'", shell=True ) subprocess.check_call( f"git archive --format=tar.gz " f"--output=dist/{src_tar} " f"--prefix=apache-fury-{v}-incubating-src/ {branch}", shell=True, ) subprocess.check_call("git reset --hard HEAD~", shell=True) os.chdir("dist") logger.info("Start to generate signature") subprocess.check_call( f"gpg --armor --output {src_tar}.asc --detach-sig {src_tar}", shell=True ) subprocess.check_call(f"sha512sum {src_tar} >{src_tar}.sha512", shell=True) verify(v) def _check_release_version(v: str): assert v if "rc" in v: raise ValueError( "RC should only be contained in tag and svn directory, not in code" ) def _check_all_committed(): proc = subprocess.run("git diff --quiet", capture_output=True, shell=True) result = proc.returncode if result != 0: raise Exception( f"There are some uncommitted files: {proc.stdout}, please commit it." ) def _strip_unnecessary_license(): with open("LICENSE", "r") as f: lines = f.readlines() new_lines = [] line_number = 0 while line_number < len(lines): line = lines[line_number] if "fast-serialization" in line: line_number += 4 elif "benchmark" in line: # strip license in benchmark line_number += 1 else: new_lines.append(line) line_number += 1 text = "".join(new_lines) if lines != new_lines: with open("LICENSE", "w") as f: f.write(text) def verify(v): src_tar = f"apache-fury-{v}-incubating-src.tar.gz" subprocess.check_call(f"gpg --verify {src_tar}.asc {src_tar}", shell=True) logger.info("Verified signature") subprocess.check_call(f"sha512sum --check {src_tar}.sha512", shell=True) logger.info("Verified checksum successfully") def bump_version(**kwargs): new_version = kwargs["version"] langs = kwargs["l"] if langs == "all": langs = ["java", "python", "javascript", "scala", "rust", "kotlin"] else: langs = langs.split(",") for lang in langs: if lang == "java": bump_java_version(new_version) elif lang == "scala": _bump_version("scala", "build.sbt", new_version, _update_scala_version) elif lang == "kotlin": _bump_version("kotlin", "pom.xml", new_version, _update_kotlin_version) elif lang == "rust": _bump_version("rust", "Cargo.toml", new_version, _update_rust_version) elif lang == "python": _bump_version( "python/pyfury", "__init__.py", new_version, _update_python_version ) elif lang == "javascript": _bump_version( "javascript/packages/fury", "package.json", new_version, _update_js_version, ) _bump_version( "javascript/packages/hps", "package.json", new_version, _update_js_version, ) else: raise NotImplementedError(f"Unsupported {lang}") def _bump_version(path, file, new_version, func): os.chdir(os.path.join(PROJECT_ROOT_DIR, path)) with open(file, "r") as f: lines = f.readlines() lines = func(lines, new_version) or lines text = "".join(lines) with open(file, "w") as f: f.write(text) def bump_java_version(new_version): for p in [ "integration_tests/graalvm_tests", "integration_tests/jdk_compatibility_tests", "integration_tests/jpms_tests", "integration_tests/latest_jdk_tests", "integration_tests/latest_jdk_tests", "java/benchmark", ]: _bump_version(p, "pom.xml", new_version, _update_pom_parent_version) os.chdir(os.path.join(PROJECT_ROOT_DIR, "java")) subprocess.check_output( f"mvn versions:set -DnewVersion={new_version}", shell=True, universal_newlines=True, ) def _update_pom_parent_version(lines, new_version): start_index, end_index = -1, -1 for i, line in enumerate(lines): if "<parent>" in line: start_index = i if "</parent>" in line: end_index = i break assert start_index != -1 assert end_index != -1 for line_number in range(start_index, end_index): line = lines[line_number] if "version" in line: line = re.sub( r"(<version>)[^<>]+(</version>)", r"\g<1>" + new_version + r"\2", line ) lines[line_number] = line def _update_scala_version(lines, v): for index, line in enumerate(lines): if "furyVersion = " in line: lines[index] = f'val furyVersion = "{v}"\n' break return lines def _update_kotlin_version(lines, v): target_index = -1 for index, line in enumerate(lines): if "<artifactId>fury-kotlin</artifactId>" in line: target_index = index + 1 break current_version_line = lines[target_index] # Find the start and end of the version number start = current_version_line.index("<version>") + len("<version>") end = current_version_line.index("</version>") # Replace the version number updated_version_line = current_version_line[:start] + v + current_version_line[end:] lines[target_index] = updated_version_line return lines def _update_rust_version(lines, v): for index, line in enumerate(lines): if "version = " in line: lines[index] = f'version = "{v}"\n' break return lines def _update_python_version(lines, v: str): for index, line in enumerate(lines): if "__version__ = " in line: v = v.replace("-alpha", "a") v = v.replace("-beta", "b") v = v.replace("-rc", "rc") lines[index] = f'__version__ = "{v}"\n' break def _update_js_version(lines, v: str): for index, line in enumerate(lines): if "version" in line: # "version": "0.5.9-beta" for x in ["-alpha", "-beta", "-rc"]: if x in v and v.split(x)[-1].isdigit(): v = v.replace(x, x + ".") lines[index] = f' "version": "{v}",\n' break def _parse_args(): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter ) parser.set_defaults(func=parser.print_help) subparsers = parser.add_subparsers() bump_version_parser = subparsers.add_parser( "bump_version", description="Bump version", ) bump_version_parser.add_argument("-version", type=str, help="new version") bump_version_parser.add_argument("-l", type=str, help="language") bump_version_parser.set_defaults(func=bump_version) prepare_parser = subparsers.add_parser( "prepare", description="Prepare release branch", ) prepare_parser.add_argument("-v", type=str, help="new version") prepare_parser.set_defaults(func=prepare) release_parser = subparsers.add_parser( "build", description="Build release artifacts", ) release_parser.add_argument("-v", type=str, help="new version") release_parser.set_defaults(func=build) verify_parser = subparsers.add_parser( "verify", description="Verify release artifacts", ) verify_parser.add_argument("-v", type=str, help="new version") verify_parser.set_defaults(func=verify) args = parser.parse_args() arg_dict = dict(vars(args)) del arg_dict["func"] print(arg_dict) args.func(**arg_dict) if __name__ == "__main__": _parse_args()