def _bump_testing_version(self, helper_branch: str, args: argparse.Namespace): update_cmake_version(self.version) cmake_path = get_abs_path(FILE_WITH_VERSION_PATH) self.run( f"git commit -m 'Update version to {self.version.string}' '{cmake_path}'" ) with self._push(helper_branch, args): body_file = get_abs_path(".github/PULL_REQUEST_TEMPLATE.md") self.run( f"gh pr create --repo {args.repo} --title 'Update version after " f"release' --head {helper_branch} --body-file '{body_file}'") # Here the prestable part is done yield
def _bump_testing_version(self, helper_branch: str): self.read_version() self.version = self.version.update(self.release_type) self.version.with_description("testing") update_cmake_version(self.version) cmake_path = get_abs_path(FILE_WITH_VERSION_PATH) self.run( f"git commit -m 'Update version to {self.version.string}' '{cmake_path}'" ) with self._push(helper_branch): body_file = get_abs_path(".github/PULL_REQUEST_TEMPLATE.md") self.run( f"gh pr create --repo {self.repo} --title 'Update version after " f"release' --head {helper_branch} --body-file '{body_file}'") # Here the prestable part is done yield
def stable(self): self.check_no_tags_after() self.read_version() version_type = VersionType.STABLE if self.version.minor % 5 == 3: # our 3 and 8 are LTS version_type = VersionType.LTS self.version.with_description(version_type) with self._create_gh_release(False): self.version = self.version.update(self.release_type) self.version.with_description(version_type) update_cmake_version(self.version) cmake_path = get_abs_path(FILE_WITH_VERSION_PATH) # Checkouting the commit of the branch and not the branch itself, # then we are able to skip rollback with self._checkout(f"{self.release_branch}@{{0}}", False): current_commit = self.run("git rev-parse HEAD") self.run( f"git commit -m " f"'Update version to {self.version.string}' '{cmake_path}'" ) with self._push("HEAD", with_rollback_on_fail=False, remote_ref=self.release_branch): # DO NOT PUT ANYTHING ELSE HERE # The push must be the last action and mean the successful release self._rollback_stack.append( f"git push {self.repo.url} " f"+{current_commit}:{self.release_branch}") yield
def _bump_testing_version(self, helper_branch: str): self.read_version() self.version = self.version.update(self.release_type) self.version.with_description("testing") update_cmake_version(self.version) update_contributors(raise_error=True) self.run(f"git commit -m 'Update version to {self.version.string}' " f"'{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'") with self._push(helper_branch): body_file = get_abs_path(".github/PULL_REQUEST_TEMPLATE.md") self.run( f"gh pr create --repo {self.repo} --title 'Update version after " f"release' --head {helper_branch} --body-file '{body_file}'") # Here the prestable part is done yield
def _bump_prestable_version(self, release_branch: str, args: argparse.Namespace): new_version = self.version.patch_update() update_cmake_version(new_version) cmake_path = get_abs_path(FILE_WITH_VERSION_PATH) self.run( f"git commit -m 'Update version to {new_version.string}' '{cmake_path}'" ) with self._push(release_branch, args): self.run( f"gh pr create --repo {args.repo} --title 'Release pull request for " f"branch {release_branch}' --head {release_branch} --body 'This " "PullRequest is a part of ClickHouse release cycle. It is used by CI " "system only. Do not perform any changes with it.' --label release" ) # Here the prestable part is done yield
def _bump_prestable_version(self, release_branch: str, args: argparse.Namespace): self._git.update() new_version = self.version.patch_update() new_version.with_description("prestable") update_cmake_version(new_version) cmake_path = get_abs_path(FILE_WITH_VERSION_PATH) self.run( f"git commit -m 'Update version to {new_version.string}' '{cmake_path}'" ) with self._push(release_branch, args): with self._create_gh_label(f"v{release_branch}-must-backport", "10dbed", args): with self._create_gh_label(f"v{release_branch}-affected", "c2bfff", args): self.run( f"gh pr create --repo {args.repo} --title 'Release pull " f"request for branch {release_branch}' --head {release_branch} " "--body 'This PullRequest is a part of ClickHouse release " "cycle. It is used by CI system only. Do not perform any " "changes with it.' --label release") # Here the prestable part is done yield
class Release: BIG = ("major", "minor") SMALL = ("patch", ) CMAKE_PATH = get_abs_path(FILE_WITH_VERSION_PATH) CONTRIBUTORS_PATH = get_abs_path(GENERATED_CONTRIBUTORS) def __init__(self, repo: Repo, release_commit: str, release_type: str): self.repo = repo self._release_commit = "" self.release_commit = release_commit self.release_type = release_type self._git = git self._version = get_version_from_repo(git=self._git) self._release_branch = "" self._rollback_stack = [] # type: List[str] def run(self, cmd: str, cwd: Optional[str] = None) -> str: cwd_text = "" if cwd: cwd_text = f" (CWD='{cwd}')" logging.info("Running command%s:\n %s", cwd_text, cmd) return self._git.run(cmd, cwd) def set_release_branch(self): # Get the actual version for the commit before check with self._checkout(self.release_commit, True): self.read_version() self.release_branch = f"{self.version.major}.{self.version.minor}" self.read_version() def read_version(self): self._git.update() self.version = get_version_from_repo(git=self._git) def check_prerequisites(self): """ Check tooling installed in the system """ self.run("gh auth status") self.run("git status") def do(self, check_dirty: bool, check_branch: bool, with_prestable: bool): self.check_prerequisites() if check_dirty: logging.info("Checking if repo is clean") self.run("git diff HEAD --exit-code") self.set_release_branch() if check_branch: self.check_branch() with self._checkout(self.release_commit, True): if self.release_type in self.BIG: # Checkout to the commit, it will provide the correct current version if with_prestable: with self.prestable(): logging.info("Prestable part of the releasing is done") else: logging.info("Skipping prestable stage") with self.testing(): logging.info("Testing part of the releasing is done") elif self.release_type in self.SMALL: with self.stable(): logging.info("Stable part of the releasing is done") self.log_rollback() def check_no_tags_after(self): tags_after_commit = self.run( f"git tag --contains={self.release_commit}") if tags_after_commit: raise Exception( f"Commit {self.release_commit} belongs to following tags:\n" f"{tags_after_commit}\nChoose another commit") def check_branch(self): if self.release_type in self.BIG: # Commit to spin up the release must belong to a main branch branch = "master" output = self.run( f"git branch --contains={self.release_commit} {branch}") if branch not in output: raise Exception( f"commit {self.release_commit} must belong to {branch} for " f"{self.release_type} release") return elif self.release_type in self.SMALL: output = self.run( f"git branch --contains={self.release_commit} {self.release_branch}" ) if self.release_branch not in output: raise Exception( f"commit {self.release_commit} must be in " f"'{self.release_branch}' branch for {self.release_type} release" ) return def log_rollback(self): if self._rollback_stack: rollback = self._rollback_stack rollback.reverse() logging.info( "To rollback the action run the following commands:\n %s", "\n ".join(rollback), ) @contextmanager def prestable(self): self.check_no_tags_after() # Create release branch self.read_version() with self._create_branch(self.release_branch, self.release_commit): with self._checkout(self.release_branch, True): self.read_version() self.version.with_description(VersionType.PRESTABLE) with self._create_gh_release(True): with self._bump_prestable_version(): # At this point everything will rollback automatically yield @contextmanager def stable(self): self.check_no_tags_after() self.read_version() version_type = VersionType.STABLE if self.version.minor % 5 == 3: # our 3 and 8 are LTS version_type = VersionType.LTS self.version.with_description(version_type) with self._create_gh_release(False): self.version = self.version.update(self.release_type) self.version.with_description(version_type) update_cmake_version(self.version) update_contributors(raise_error=True) # Checkouting the commit of the branch and not the branch itself, # then we are able to skip rollback with self._checkout(f"{self.release_branch}@{{0}}", False): current_commit = self.run("git rev-parse HEAD") self.run(f"git commit -m " f"'Update version to {self.version.string}' " f"'{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'") with self._push("HEAD", with_rollback_on_fail=False, remote_ref=self.release_branch): # DO NOT PUT ANYTHING ELSE HERE # The push must be the last action and mean the successful release self._rollback_stack.append( f"git push {self.repo.url} " f"+{current_commit}:{self.release_branch}") yield @contextmanager def testing(self): # Create branch for a version bump self.read_version() self.version = self.version.update(self.release_type) helper_branch = f"{self.version.major}.{self.version.minor}-prepare" with self._create_branch(helper_branch, self.release_commit): with self._checkout(helper_branch, True): with self._bump_testing_version(helper_branch): yield @property def version(self) -> ClickHouseVersion: return self._version @version.setter def version(self, version: ClickHouseVersion): if not isinstance(version, ClickHouseVersion): raise ValueError( f"version must be ClickHouseVersion, not {type(version)}") self._version = version @property def release_branch(self) -> str: return self._release_branch @release_branch.setter def release_branch(self, branch: str): self._release_branch = release_branch(branch) @property def release_commit(self) -> str: return self._release_commit @release_commit.setter def release_commit(self, release_commit: str): self._release_commit = commit(release_commit) @contextmanager def _bump_prestable_version(self): # Update only git, origal version stays the same self._git.update() new_version = self.version.patch_update() new_version.with_description("prestable") update_cmake_version(new_version) update_contributors(raise_error=True) self.run(f"git commit -m 'Update version to {new_version.string}' " f"'{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'") with self._push(self.release_branch): with self._create_gh_label(f"v{self.release_branch}-must-backport", "10dbed"): with self._create_gh_label(f"v{self.release_branch}-affected", "c2bfff"): self.run( f"gh pr create --repo {self.repo} --title " f"'Release pull request for branch {self.release_branch}' " f"--head {self.release_branch} --label release " "--body 'This PullRequest is a part of ClickHouse release " "cycle. It is used by CI system only. Do not perform any " "changes with it.'") # Here the prestable part is done yield @contextmanager def _bump_testing_version(self, helper_branch: str): self.read_version() self.version = self.version.update(self.release_type) self.version.with_description("testing") update_cmake_version(self.version) update_contributors(raise_error=True) self.run(f"git commit -m 'Update version to {self.version.string}' " f"'{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'") with self._push(helper_branch): body_file = get_abs_path(".github/PULL_REQUEST_TEMPLATE.md") self.run( f"gh pr create --repo {self.repo} --title 'Update version after " f"release' --head {helper_branch} --body-file '{body_file}'") # Here the prestable part is done yield @contextmanager def _checkout(self, ref: str, with_checkout_back: bool = False): orig_ref = self._git.branch or self._git.sha need_rollback = False if ref not in (self._git.branch, self._git.sha): need_rollback = True self.run(f"git checkout {ref}") # checkout is not put into rollback_stack intentionally rollback_cmd = f"git checkout {orig_ref}" try: yield except BaseException: logging.warning("Rolling back checked out %s for %s", ref, orig_ref) self.run(f"git reset --hard; git checkout {orig_ref}") raise else: if with_checkout_back and need_rollback: self.run(rollback_cmd) @contextmanager def _create_branch(self, name: str, start_point: str = ""): self.run(f"git branch {name} {start_point}") rollback_cmd = f"git branch -D {name}" self._rollback_stack.append(rollback_cmd) try: yield except BaseException: logging.warning("Rolling back created branch %s", name) self.run(rollback_cmd) raise @contextmanager def _create_gh_label(self, label: str, color_hex: str): # API call, https://docs.github.com/en/rest/reference/issues#create-a-label self.run( f"gh api repos/{self.repo}/labels -f name={label} -f color={color_hex}" ) rollback_cmd = f"gh api repos/{self.repo}/labels/{label} -X DELETE" self._rollback_stack.append(rollback_cmd) try: yield except BaseException: logging.warning("Rolling back label %s", label) self.run(rollback_cmd) raise @contextmanager def _create_gh_release(self, as_prerelease: bool): with self._create_tag(): # Preserve tag if version is changed tag = self.version.describe prerelease = "" if as_prerelease: prerelease = "--prerelease" self.run( f"gh release create {prerelease} --draft --repo {self.repo} " f"--title 'Release {tag}' '{tag}'") rollback_cmd = f"gh release delete --yes --repo {self.repo} '{tag}'" self._rollback_stack.append(rollback_cmd) try: yield except BaseException: logging.warning("Rolling back release publishing") self.run(rollback_cmd) raise @contextmanager def _create_tag(self): tag = self.version.describe self.run(f"git tag -a -m 'Release {tag}' '{tag}'") rollback_cmd = f"git tag -d '{tag}'" self._rollback_stack.append(rollback_cmd) try: with self._push(f"'{tag}'"): yield except BaseException: logging.warning("Rolling back tag %s", tag) self.run(rollback_cmd) raise @contextmanager def _push(self, ref: str, with_rollback_on_fail: bool = True, remote_ref: str = ""): if remote_ref == "": remote_ref = ref self.run(f"git push {self.repo.url} {ref}:{remote_ref}") if with_rollback_on_fail: rollback_cmd = f"git push -d {self.repo.url} {remote_ref}" self._rollback_stack.append(rollback_cmd) try: yield except BaseException: if with_rollback_on_fail: logging.warning("Rolling back pushed ref %s", ref) self.run(rollback_cmd) raise