import argparse import dataclasses import json import os from contextlib import contextmanager from copy import copy from pathlib import Path from typing import Iterator, List from ci_buddy import CIBuddy from ci_config import CI from ci_utils import GH, Shell from git_helper import GIT_PREFIX, Git from s3_helper import S3Helper from ssh import SSHAgent from version_helper import ( FILE_WITH_VERSION_PATH, GENERATED_CONTRIBUTORS, VersionType, get_abs_path, get_version_from_repo, update_cmake_version, update_contributors, ) CMAKE_PATH = get_abs_path(FILE_WITH_VERSION_PATH) CONTRIBUTORS_PATH = get_abs_path(GENERATED_CONTRIBUTORS) RELEASE_INFO_FILE = "/tmp/release_info.json" class ReleaseProgress: STARTED = "started" DOWNLOAD_PACKAGES = "download packages" PUSH_RELEASE_TAG = "push release tag" PUSH_NEW_RELEASE_BRANCH = "push new release branch" BUMP_VERSION = "bump version" CREATE_GH_RELEASE = "create GH release" EXPORT_TGZ = "export TGZ packages" EXPORT_RPM = "export RPM packages" EXPORT_DEB = "export DEB packages" TEST_TGZ = "test TGZ packages" TEST_RPM = "test RPM packages" TEST_DEB = "test DEB packages" MERGE_CREATED_PRS = "merge created PRs" COMPLETED = "completed" class ReleaseProgressDescription: OK = "OK" FAILED = "FAILED" class ReleaseContextManager: def __init__(self, release_progress): self.release_progress = release_progress self.release_info = None def __enter__(self): if self.release_progress == ReleaseProgress.STARTED: # create initial release info self.release_info = ReleaseInfo( release_branch="NA", release_type="NA", commit_sha=args.ref, release_tag="NA", version="NA", codename="NA", previous_release_tag="NA", previous_release_sha="NA", release_progress=ReleaseProgress.STARTED, latest=False, ).dump() else: # fetch release info from fs and update self.release_info = ReleaseInfo.from_file() assert self.release_info assert ( self.release_info.progress_status == ReleaseProgressDescription.OK ), "Must be OK on the start of new context" self.release_info.release_progress = self.release_progress self.release_info.dump() return self.release_info def __exit__(self, exc_type, exc_value, traceback): assert self.release_info if exc_type is not None: self.release_info.progress_status = ReleaseProgressDescription.FAILED else: self.release_info.progress_status = ReleaseProgressDescription.OK self.release_info.dump() @dataclasses.dataclass class ReleaseInfo: version: str release_type: str release_tag: str release_branch: str commit_sha: str latest: bool # lts or stable codename: str previous_release_tag: str previous_release_sha: str changelog_pr: str = "" version_bump_pr: str = "" prs_merged: bool = False release_url: str = "" debian: str = "" rpm: str = "" tgz: str = "" docker: str = "" release_progress: str = "" progress_status: str = "" def is_patch(self): return self.release_branch != "master" def is_new_release_branch(self): return self.release_branch == "master" @staticmethod def from_file() -> "ReleaseInfo": with open(RELEASE_INFO_FILE, "r", encoding="utf-8") as json_file: res = json.load(json_file) return ReleaseInfo(**res) def dump(self): print(f"Dump release info into [{RELEASE_INFO_FILE}]") with open(RELEASE_INFO_FILE, "w", encoding="utf-8") as f: print(json.dumps(dataclasses.asdict(self), indent=2), file=f) return self def prepare( self, commit_ref: str, release_type: str, _skip_tag_check: bool ) -> "ReleaseInfo": version = None release_branch = None release_tag = None previous_release_tag = None previous_release_sha = None latest_release = False codename = "" assert release_type in ("patch", "new") if release_type == "new": # check commit_ref is right and on a right branch if commit_ref != "master": Shell.check( f"git merge-base --is-ancestor {commit_ref} origin/master", strict=True, verbose=True, ) with checkout(commit_ref): commit_sha = Shell.get_output_or_raise(f"git rev-list -n1 {commit_ref}") # Git() must be inside "with checkout" contextmanager git = Git() version = get_version_from_repo(git=git) release_branch = f"{version.major}.{version.minor}" expected_prev_tag = f"v{version.major}.{version.minor}.1.1-new" version.bump().with_description(VersionType.NEW) assert ( git.latest_tag == expected_prev_tag ), f"BUG: latest tag [{git.latest_tag}], expected [{expected_prev_tag}]" release_tag = version.describe previous_release_tag = expected_prev_tag previous_release_sha = Shell.get_output_or_raise( f"git rev-list -n1 {previous_release_tag}" ) assert previous_release_sha if release_type == "patch": with checkout(commit_ref): commit_sha = Shell.get_output_or_raise(f"git rev-list -n1 {commit_ref}") # Git() must be inside "with checkout" contextmanager git = Git() version = get_version_from_repo(git=git) codename = version.get_stable_release_type() version.with_description(codename) release_branch = f"{version.major}.{version.minor}" release_tag = version.describe Shell.check( f"{GIT_PREFIX} fetch origin {release_branch} --tags", strict=True, verbose=True, ) # check commit is right and on a right branch Shell.check( f"git merge-base --is-ancestor {commit_ref} origin/{release_branch}", strict=True, verbose=True, ) if version.patch == 1: expected_version = copy(version) previous_release_tag = f"v{version.major}.{version.minor}.1.1-new" expected_version.bump() expected_tag_prefix = ( f"v{expected_version.major}.{expected_version.minor}." ) expected_tag_suffix = "-new" else: expected_tag_prefix = ( f"v{version.major}.{version.minor}.{version.patch-1}." ) expected_tag_suffix = f"-{version.get_stable_release_type()}" previous_release_tag = git.latest_tag if git.latest_tag.startswith( expected_tag_prefix ) and git.latest_tag.endswith(expected_tag_suffix): pass # TODO: uncomment and check with dry-run # elif not skip_tag_check: # assert ( # False # ), f"BUG: Unexpected latest tag [{git.latest_tag}] expected [{expected_tag_prefix}*{expected_tag_suffix}]. Already Released?" previous_release_sha = Shell.get_output_or_raise( f"git rev-list -n1 {previous_release_tag}" ) assert previous_release_sha if CI.GH.is_latest_release_branch(release_branch): print("This is going to be the latest release!") latest_release = True assert ( release_branch and previous_release_tag and previous_release_sha and commit_sha and release_tag and version and (codename in ("lts", "stable") or release_type == "new") ), f"Check: {release_branch}, {previous_release_tag}, {previous_release_sha}, {commit_sha}, {release_tag}, {version}" self.release_branch = release_branch self.commit_sha = commit_sha self.release_tag = release_tag self.version = version.string self.codename = codename self.previous_release_tag = previous_release_tag self.previous_release_sha = previous_release_sha self.release_progress = ReleaseProgress.STARTED self.progress_status = ReleaseProgressDescription.OK self.latest = latest_release self.release_type = release_type return self def push_release_tag(self, dry_run: bool) -> None: if dry_run: # remove locally created tag from prev run Shell.check( f"{GIT_PREFIX} tag -l | grep -q {self.release_tag} && git tag -d {self.release_tag}" ) # Create release tag print( f"Create and push release tag [{self.release_tag}], commit [{self.commit_sha}]" ) tag_message = f"Release {self.release_tag}" Shell.check( f"{GIT_PREFIX} tag -a -m '{tag_message}' {self.release_tag} {self.commit_sha}", strict=True, verbose=True, ) cmd_push_tag = f"{GIT_PREFIX} push origin {self.release_tag}:{self.release_tag}" Shell.check(cmd_push_tag, dry_run=dry_run, strict=True, verbose=True) @staticmethod def _create_gh_label(label: str, color_hex: str, dry_run: bool) -> None: cmd = f"gh api repos/{CI.Envs.GITHUB_REPOSITORY}/labels -f name={label} -f color={color_hex}" res = Shell.check(cmd, dry_run=dry_run, verbose=True) if not res: # not a critical error - do not fail. branch might be created already (recovery case) print("WARNING: failed to create backport labels for the new branch") def push_new_release_branch(self, dry_run: bool) -> None: git = Git() version = get_version_from_repo(git=git) new_release_branch = self.release_branch version_after_release = copy(version) version_after_release.bump() assert ( version_after_release.string == self.version ), f"Unexpected current version in git, must precede [{self.version}] by one step, actual [{version.string}]" if dry_run: # remove locally created branch from prev run Shell.check( f"{GIT_PREFIX} branch -l | grep -q {new_release_branch} && git branch -d {new_release_branch}" ) print( f"Create and push new release branch [{new_release_branch}], commit [{self.commit_sha}]" ) with checkout("master"): with checkout_new(new_release_branch): cmd_push_branch = ( f"{GIT_PREFIX} push --set-upstream origin {new_release_branch}" ) Shell.check(cmd_push_branch, dry_run=dry_run, strict=True, verbose=True) print("Create and push backport tags for new release branch") ReleaseInfo._create_gh_label( f"v{new_release_branch}-must-backport", "10dbed", dry_run=dry_run ) ReleaseInfo._create_gh_label( f"v{new_release_branch}-affected", "c2bfff", dry_run=dry_run ) def get_version_bump_branch(self): return f"bump_version_{self.version}" def update_version_and_contributors_list(self, dry_run: bool) -> None: # Bump version, update contributors list, create on release branch with checkout(self.commit_sha): git = Git() version = get_version_from_repo(git=git) if self.release_type == "patch": assert ( version.string == self.version ), f"BUG: version in release info does not match version in git commit, expected [{self.version}], got [{version.string}]" version.bump_patch() else: version.reset_tweak() version.with_description(version.get_stable_release_type()) with checkout(self.release_branch): update_cmake_version(version) update_contributors(raise_error=True) cmd_commit_version_upd = f"{GIT_PREFIX} commit '{CMAKE_PATH}' '{CONTRIBUTORS_PATH}' -m 'Update autogenerated version to {self.version} and contributors'" cmd_push_branch = f"{GIT_PREFIX} push" Shell.check( cmd_commit_version_upd, strict=True, dry_run=dry_run, verbose=True ) Shell.check(cmd_push_branch, strict=True, dry_run=dry_run, verbose=True) if dry_run: Shell.check( f"{GIT_PREFIX} diff '{CMAKE_PATH}' '{CONTRIBUTORS_PATH}'", verbose=True, ) Shell.check( f"{GIT_PREFIX} checkout '{CMAKE_PATH}' '{CONTRIBUTORS_PATH}'", verbose=True, ) # TODO: move to new GH step? if self.release_type == "new": print("Update version on master branch") branch_upd_version_contributors = self.get_version_bump_branch() with checkout(self.commit_sha): git = Git() version = get_version_from_repo(git=git) version.bump() version.with_description(VersionType.TESTING) with checkout("master"): with checkout_new(branch_upd_version_contributors): update_cmake_version(version) update_contributors(raise_error=True) cmd_commit_version_upd = f"{GIT_PREFIX} commit '{CMAKE_PATH}' '{CONTRIBUTORS_PATH}' -m 'Update autogenerated version to {self.version} and contributors'" cmd_push_branch = f"{GIT_PREFIX} push --set-upstream origin {branch_upd_version_contributors}" actor = os.getenv("GITHUB_ACTOR", "") or "me" body = f"Automatic version bump after release {self.release_tag}\n### Changelog category (leave one):\n- Not for changelog (changelog entry is not required)\n" cmd_create_pr = f"gh pr create --repo {CI.Envs.GITHUB_REPOSITORY} --title 'Update version after release' --head {branch_upd_version_contributors} --base master --body \"{body}\" --assignee {actor}" Shell.check( cmd_commit_version_upd, strict=True, dry_run=dry_run, verbose=True, ) Shell.check( cmd_push_branch, strict=True, dry_run=dry_run, verbose=True ) Shell.check( cmd_create_pr, strict=True, dry_run=dry_run, verbose=True ) if dry_run: Shell.check( f"{GIT_PREFIX} diff '{CMAKE_PATH}' '{CONTRIBUTORS_PATH}'", verbose=True, ) Shell.check( f"{GIT_PREFIX} checkout '{CMAKE_PATH}' '{CONTRIBUTORS_PATH}'", verbose=True, ) self.version_bump_pr = "dry-run" else: self.version_bump_pr = GH.get_pr_url_by_branch( branch=branch_upd_version_contributors ) # TODO: move to new GH step? print("Create Release PR") with checkout(self.release_branch): pr_labels = f"--label {CI.Labels.RELEASE}" if version.get_stable_release_type() == VersionType.LTS: pr_labels += f" --label {CI.Labels.RELEASE_LTS}" Shell.check( f"""gh pr create --repo {CI.Envs.GITHUB_REPOSITORY} --title 'Release pull request for branch {self.release_branch}' \ --head {self.release_branch} {pr_labels} \ --body 'This PullRequest is a part of ClickHouse release cycle. It is used by CI system only. Do not perform any changes with it.'""", dry_run=dry_run, strict=True, verbose=True, ) def get_change_log_branch(self): return f"auto/{self.release_tag}" def update_release_info(self, dry_run: bool) -> "ReleaseInfo": if self.release_type == "patch": if not self.changelog_pr: branch = self.get_change_log_branch() if not dry_run: url = GH.get_pr_url_by_branch(branch=branch) else: url = "dry-run" print(f"ChangeLog PR url [{url}]") self.changelog_pr = url self.docker = f"docker run --rm clickhouse/clickhouse:{self.version} clickhouse --version" else: # new release branch - find version bump pr on a master branch branch = self.get_version_bump_branch() if not dry_run: url = GH.get_pr_url_by_branch(branch=branch) else: url = "dry-run" print(f"Version bump PR url [{url}]") self.version_bump_pr = url self.release_url = f"https://github.com/{CI.Envs.GITHUB_REPOSITORY}/releases/tag/{self.release_tag}" print(f"Release url [{self.release_url}]") self.dump() return self def create_gh_release(self, packages_files: List[str], dry_run: bool) -> None: repo = CI.Envs.GITHUB_REPOSITORY assert repo cmds = [ f"gh release create --repo {repo} --title 'Release {self.release_tag}' {self.release_tag}" ] for file in packages_files: cmds.append(f"gh release upload {self.release_tag} {file}") if not dry_run: for cmd in cmds: Shell.check(cmd, strict=True, verbose=True) self.release_url = ( f"https://github.com/{repo}/releases/tag/{self.release_tag}" ) else: print("Dry-run, would run commands:") print("\n * ".join(cmds)) self.release_url = f"dry-run" self.dump() def merge_prs(self, dry_run: bool) -> None: repo = CI.Envs.GITHUB_REPOSITORY if self.release_type == "patch": assert self.changelog_pr print("Merging ChangeLog PR") if dry_run: changelog_pr_num = 23456 else: changelog_pr_num = int(self.changelog_pr.split("/")[-1]) res = Shell.check( f"gh pr merge {changelog_pr_num} --repo {repo} --merge --auto", verbose=True, dry_run=dry_run, ) else: if not dry_run: assert not self.changelog_pr res = True if self.release_type == "new": assert self.version_bump_pr print("Merging Version Bump PR") if dry_run: version_bump_pr = 23456 else: version_bump_pr = int(self.version_bump_pr.split("/")[-1]) res = res and Shell.check( f"gh pr merge {version_bump_pr} --repo {repo} --merge --auto", verbose=True, dry_run=dry_run, ) else: if not dry_run: assert not self.version_bump_pr self.prs_merged = res class RepoTypes: RPM = "rpm" DEBIAN = "deb" TGZ = "tgz" class PackageDownloader: PACKAGES = ( "clickhouse-client", "clickhouse-common-static", "clickhouse-common-static-dbg", "clickhouse-keeper", "clickhouse-keeper-dbg", "clickhouse-server", ) EXTRA_PACKAGES = ( "clickhouse-library-bridge", "clickhouse-odbc-bridge", ) PACKAGE_TYPES = (CI.BuildNames.PACKAGE_RELEASE, CI.BuildNames.PACKAGE_AARCH64) MACOS_PACKAGE_TO_BIN_SUFFIX = { CI.BuildNames.BINARY_DARWIN: "macos", CI.BuildNames.BINARY_DARWIN_AARCH64: "macos-aarch64", } LOCAL_DIR = "/tmp/packages" @classmethod def _get_arch_suffix(cls, package_arch, repo_type): if package_arch == CI.BuildNames.PACKAGE_RELEASE: return ( "amd64" if repo_type in (RepoTypes.DEBIAN, RepoTypes.TGZ) else "x86_64" ) if package_arch == CI.BuildNames.PACKAGE_AARCH64: return ( "arm64" if repo_type in (RepoTypes.DEBIAN, RepoTypes.TGZ) else "aarch64" ) assert False, "BUG" def __init__(self, release, commit_sha, version): assert version.startswith(release), "Invalid release branch or version" major, minor = map(int, release.split(".")) self.package_names = list(self.PACKAGES) if major > 24 or (major == 24 and minor > 3): self.package_names += list(self.EXTRA_PACKAGES) self.release = release self.commit_sha = commit_sha self.version = version self.s3 = S3Helper() self.deb_package_files = [] self.rpm_package_files = [] self.tgz_package_files = [] # just binaries for macos self.macos_package_files = ["clickhouse-macos", "clickhouse-macos-aarch64"] self.file_to_type = {} Shell.check(f"mkdir -p {self.LOCAL_DIR}") for package_type in self.PACKAGE_TYPES: for package in self.package_names: deb_package_file_name = f"{package}_{self.version}_{self._get_arch_suffix(package_type, RepoTypes.DEBIAN)}.deb" self.deb_package_files.append(deb_package_file_name) self.file_to_type[deb_package_file_name] = package_type rpm_package_file_name = f"{package}-{self.version}.{self._get_arch_suffix(package_type, RepoTypes.RPM)}.rpm" self.rpm_package_files.append(rpm_package_file_name) self.file_to_type[rpm_package_file_name] = package_type tgz_package_file_name = f"{package}-{self.version}-{self._get_arch_suffix(package_type, RepoTypes.TGZ)}.tgz" self.tgz_package_files.append(tgz_package_file_name) self.file_to_type[tgz_package_file_name] = package_type tgz_package_file_name += ".sha512" self.tgz_package_files.append(tgz_package_file_name) self.file_to_type[tgz_package_file_name] = package_type def get_deb_packages_files(self): return self.deb_package_files def get_rpm_packages_files(self): return self.rpm_package_files def get_tgz_packages_files(self): return self.tgz_package_files def get_macos_packages_files(self): return self.macos_package_files def get_packages_names(self): return self.package_names def get_all_packages_files(self): assert self.local_tgz_packages_ready() assert self.local_deb_packages_ready() assert self.local_rpm_packages_ready() assert self.local_macos_packages_ready() res = [] for package_file in ( self.deb_package_files + self.rpm_package_files + self.tgz_package_files + self.macos_package_files ): res.append(self.LOCAL_DIR + "/" + package_file) return res def run(self): Shell.check(f"rm -rf {self.LOCAL_DIR}/*") for package_file in ( self.deb_package_files + self.rpm_package_files + self.tgz_package_files ): print(f"Downloading: [{package_file}]") s3_path = "/".join( [ self.release, self.commit_sha, self.file_to_type[package_file], package_file, ] ) self.s3.download_file( bucket=CI.Envs.S3_BUILDS_BUCKET, s3_path=s3_path, local_file_path="/".join([self.LOCAL_DIR, package_file]), ) for macos_package, bin_suffix in self.MACOS_PACKAGE_TO_BIN_SUFFIX.items(): binary_name = "clickhouse" destination_binary_name = f"{binary_name}-{bin_suffix}" assert destination_binary_name in self.macos_package_files print( f"Downloading: [{macos_package}] binary to [{destination_binary_name}]" ) s3_path = "/".join( [ self.release, self.commit_sha, macos_package, binary_name, ] ) self.s3.download_file( bucket=CI.Envs.S3_BUILDS_BUCKET, s3_path=s3_path, local_file_path="/".join([self.LOCAL_DIR, destination_binary_name]), ) def local_deb_packages_ready(self) -> bool: assert self.deb_package_files for package_file in self.deb_package_files: print(f"Check package is downloaded [{package_file}]") if not Path(self.LOCAL_DIR + "/" + package_file).is_file(): return False return True def local_rpm_packages_ready(self) -> bool: assert self.rpm_package_files for package_file in self.rpm_package_files: print(f"Check package is downloaded [{package_file}]") if not Path(self.LOCAL_DIR + "/" + package_file).is_file(): return False return True def local_tgz_packages_ready(self) -> bool: assert self.tgz_package_files for package_file in self.tgz_package_files: print(f"Check package is downloaded [{package_file}]") if not Path(self.LOCAL_DIR + "/" + package_file).is_file(): return False return True def local_macos_packages_ready(self) -> bool: assert self.macos_package_files for package_file in self.macos_package_files: print(f"Check package is downloaded [{package_file}]") if not Path(self.LOCAL_DIR + "/" + package_file).is_file(): return False return True @contextmanager def checkout(ref: str) -> Iterator[None]: orig_ref = Shell.get_output_or_raise(f"{GIT_PREFIX} symbolic-ref --short HEAD") rollback_cmd = f"{GIT_PREFIX} checkout {orig_ref}" assert orig_ref if ref not in (orig_ref,): Shell.check(f"{GIT_PREFIX} checkout {ref}", strict=True, verbose=True) try: yield except (Exception, KeyboardInterrupt) as e: print(f"ERROR: Exception [{e}]") Shell.check(rollback_cmd, verbose=True) raise Shell.check(rollback_cmd, verbose=True) @contextmanager def checkout_new(ref: str) -> Iterator[None]: orig_ref = Shell.get_output_or_raise(f"{GIT_PREFIX} symbolic-ref --short HEAD") rollback_cmd = f"{GIT_PREFIX} checkout {orig_ref}" assert orig_ref Shell.check(f"{GIT_PREFIX} checkout -b {ref}", strict=True, verbose=True) try: yield except (Exception, KeyboardInterrupt) as e: print(f"ERROR: Exception [{e}]") Shell.check(rollback_cmd, verbose=True) raise Shell.check(rollback_cmd, verbose=True) def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description="Creates release", ) parser.add_argument( "--prepare-release-info", action="store_true", help="Initial step to prepare info like release branch, release tag, etc.", ) parser.add_argument( "--skip-tag-check", action="store_true", help="To skip check against latest git tag on a release branch", ) parser.add_argument( "--push-release-tag", action="store_true", help="Creates and pushes git tag", ) parser.add_argument( "--push-new-release-branch", action="store_true", help="Creates and pushes new release branch and corresponding service gh tags for backports", ) parser.add_argument( "--create-bump-version-pr", action="store_true", help="Updates version, contributors' list and creates PR", ) parser.add_argument( "--download-packages", action="store_true", help="Downloads all required packages from s3", ) parser.add_argument( "--create-gh-release", action="store_true", help="Create GH Release object and attach all packages", ) parser.add_argument( "--merge-prs", action="store_true", help="Merge PRs with version, changelog updates", ) parser.add_argument( "--post-status", action="store_true", help="Post release status into Slack", ) parser.add_argument( "--ref", type=str, help="the commit hash or branch", ) parser.add_argument( "--release-type", choices=("new", "patch"), # dest="release_type", help="a release type to bump the major.minor.patch version part, " "new branch is created only for the value 'new'", ) parser.add_argument( "--dry-run", action="store_true", help="do not make any actual changes in the repo, just show what will be done", ) parser.add_argument( "--set-progress-started", action="store_true", help="Set new progress step, --progress must be set", ) parser.add_argument( "--progress", type=str, help="Progress step name, see @ReleaseProgress", ) parser.add_argument( "--set-progress-completed", action="store_true", help="Set current progress step to OK (completed)", ) return parser.parse_args() if __name__ == "__main__": args = parse_args() # prepare ssh for git if needed _ssh_agent = None _key_pub = None if os.getenv("ROBOT_CLICKHOUSE_SSH_KEY", ""): _key = os.getenv("ROBOT_CLICKHOUSE_SSH_KEY") _ssh_agent = SSHAgent() _key_pub = _ssh_agent.add(_key) _ssh_agent.print_keys() if args.prepare_release_info: with ReleaseContextManager( release_progress=ReleaseProgress.STARTED ) as release_info: assert ( args.ref and args.release_type ), "--ref and --release-type must be provided with --prepare-release-info" release_info.prepare( commit_ref=args.ref, release_type=args.release_type, _skip_tag_check=args.skip_tag_check, ) if args.download_packages: with ReleaseContextManager( release_progress=ReleaseProgress.DOWNLOAD_PACKAGES ) as release_info: p = PackageDownloader( release=release_info.release_branch, commit_sha=release_info.commit_sha, version=release_info.version, ) p.run() if args.push_release_tag: with ReleaseContextManager( release_progress=ReleaseProgress.PUSH_RELEASE_TAG ) as release_info: release_info.push_release_tag(dry_run=args.dry_run) if args.push_new_release_branch: with ReleaseContextManager( release_progress=ReleaseProgress.PUSH_NEW_RELEASE_BRANCH ) as release_info: release_info.push_new_release_branch(dry_run=args.dry_run) if args.create_bump_version_pr: with ReleaseContextManager( release_progress=ReleaseProgress.BUMP_VERSION ) as release_info: release_info.update_version_and_contributors_list(dry_run=args.dry_run) if args.create_gh_release: with ReleaseContextManager( release_progress=ReleaseProgress.CREATE_GH_RELEASE ) as release_info: p = PackageDownloader( release=release_info.release_branch, commit_sha=release_info.commit_sha, version=release_info.version, ) release_info.create_gh_release( packages_files=p.get_all_packages_files(), dry_run=args.dry_run ) if args.post_status: release_info = ReleaseInfo.from_file() if release_info.is_new_release_branch(): title = "New release branch" else: title = "New release" if ( release_info.progress_status == ReleaseProgressDescription.OK and release_info.release_progress == ReleaseProgress.COMPLETED ): title = "Completed: " + title CIBuddy(dry_run=args.dry_run).post_done( title, dataclasses.asdict(release_info) ) else: title = "Failed: " + title CIBuddy(dry_run=args.dry_run).post_critical( title, dataclasses.asdict(release_info), channels=[CIBuddy.Channels.ALERTS, CIBuddy.Channels.INFO], ) if args.set_progress_started: ri = ReleaseInfo.from_file() ri.release_progress = args.progress ri.progress_status = ReleaseProgressDescription.FAILED ri.dump() assert args.progress, "Progress step name must be provided" if args.set_progress_completed: ri = ReleaseInfo.from_file() assert ( ri.progress_status == ReleaseProgressDescription.FAILED ), "Must be FAILED before set to OK" ri.progress_status = ReleaseProgressDescription.OK ri.dump() if args.merge_prs: with ReleaseContextManager( release_progress=ReleaseProgress.MERGE_CREATED_PRS ) as release_info: release_info.update_release_info(dry_run=args.dry_run) release_info.merge_prs(dry_run=args.dry_run) # tear down ssh if _ssh_agent and _key_pub: _ssh_agent.remove(_key_pub) """ Prepare release machine: ### INSTALL PACKAGES sudo apt update sudo apt install --yes --no-install-recommends python3-dev python3-pip gh unzip sudo apt install --yes python3-boto3 sudo apt install --yes python3-github sudo apt install --yes python3-unidiff sudo apt install --yes python3-tqdm # cloud changelog sudo apt install --yes python3-thefuzz # cloud changelog sudo apt install --yes s3fs ### INSTALL AWS CLI cd /tmp curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "awscliv2.zip" unzip awscliv2.zip sudo ./aws/install rm -rf aws* cd - ### INSTALL GH ACTIONS RUNNER: # Create a folder RUNNER_VERSION=2.317.0 cd ~ mkdir actions-runner && cd actions-runner # Download the latest runner package runner_arch() { case $(uname -m) in x86_64 ) echo x64;; aarch64 ) echo arm64;; esac } curl -O -L https://github.com/actions/runner/releases/download/v$RUNNER_VERSION/actions-runner-linux-$(runner_arch)-$RUNNER_VERSION.tar.gz # Extract the installer tar xzf ./actions-runner-linux-$(runner_arch)-$RUNNER_VERSION.tar.gz rm ./actions-runner-linux-$(runner_arch)-$RUNNER_VERSION.tar.gz ### Install reprepro: cd ~ sudo apt install dpkg-dev libgpgme-dev libdb-dev libbz2-dev liblzma-dev libarchive-dev shunit2 db-util debhelper git clone https://salsa.debian.org/debian/reprepro.git cd reprepro dpkg-buildpackage -b --no-sign && sudo dpkg -i ../reprepro_$(dpkg-parsechangelog --show-field Version)_$(dpkg-architecture -q DEB_HOST_ARCH).deb ### Install createrepo-c: sudo apt install createrepo-c createrepo_c --version #Version: 0.17.3 (Features: DeltaRPM LegacyWeakdeps ) ### Import gpg sign key gpg --import key.pgp gpg --list-secret-keys ### Install docker sudo su; cd ~ deb_arch() { case $(uname -m) in x86_64 ) echo amd64;; aarch64 ) echo arm64;; esac } curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo "deb [arch=$(deb_arch) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install --yes --no-install-recommends docker-ce docker-buildx-plugin docker-ce-cli containerd.io sudo usermod -aG docker ubuntu # enable ipv6 in containers (fixed-cidr-v6 is some random network mask) cat < /etc/docker/daemon.json { "ipv6": true, "fixed-cidr-v6": "2001:db8:1::/64", "log-driver": "json-file", "log-opts": { "max-file": "5", "max-size": "1000m" }, "insecure-registries" : ["dockerhub-proxy.dockerhub-proxy-zone:5000"], "registry-mirrors" : ["http://dockerhub-proxy.dockerhub-proxy-zone:5000"] } EOT # if docker build does not work: sudo systemctl restart docker docker buildx rm mybuilder docker buildx create --name mybuilder --driver docker-container --use docker buildx inspect mybuilder --bootstrap ### Install tailscale ### Configure GH runner """