diff --git a/tests/ci/release.py b/tests/ci/release.py index 074b6bd51fe..c6af9a85810 100755 --- a/tests/ci/release.py +++ b/tests/ci/release.py @@ -71,25 +71,34 @@ class Release: repo: Repo, release_commit: str, release_type: Literal["major", "minor", "patch"], + dry_run: bool, ): self.repo = repo self._release_commit = "" self.release_commit = release_commit + self.dry_run = dry_run assert release_type in self.BIG + self.SMALL self.release_type = release_type self._git = Git() self._version = get_version_from_repo(git=self._git) + self.release_version = self.version self._release_branch = "" self._rollback_stack = [] # type: List[str] - def run(self, cmd: str, cwd: Optional[str] = None, **kwargs: Any) -> str: + def run( + self, cmd: str, cwd: Optional[str] = None, dry_run: bool = False, **kwargs: Any + ) -> str: cwd_text = "" if cwd: cwd_text = f" (CWD='{cwd}')" + if dry_run: + logging.info("Would run command%s:\n %s", cwd_text, cmd) + return "" + logging.info("Running command%s:\n %s", cwd_text, cmd) return self._git.run(cmd, cwd, **kwargs) - def set_release_branch(self): + def set_release_info(self): # Fetch release commit and tags in case they don't exist locally self.run(f"git fetch {self.repo.url} {self.release_commit}") self.run(f"git fetch {self.repo.url} --tags") @@ -97,6 +106,8 @@ class Release: # Get the actual version for the commit before check with self._checkout(self.release_commit, True): self.release_branch = f"{self.version.major}.{self.version.minor}" + self.release_version = get_version_from_repo(git=self._git) + self.release_version.with_description(self.get_stable_release_type()) self.read_version() @@ -171,17 +182,18 @@ class Release: if self._git.branch != "master": raise Exception("the script must be launched only from master") - self.set_release_branch() + self.set_release_info() if check_branch: self.check_branch() - with self._checkout(self.release_commit, True): - if self.release_type in self.BIG: - if self._version.minor >= 12 and self.release_type != "major": - raise ValueError( - "The relese type must be 'major' for minor versions>=12" - ) + if self.release_type in self.BIG: + if self._version.minor >= 12 and self.release_type != "major": + raise ValueError( + "The relese type must be 'major' for minor versions>=12" + ) + + with self._checkout(self.release_commit, True): # Checkout to the commit, it will provide the correct current version if with_release_branch: with self.create_release_branch(): @@ -200,10 +212,19 @@ class Release: self.run(cmd) raise - elif self.release_type in self.SMALL: + elif self.release_type in self.SMALL: + with self._checkout(self.release_commit, True): with self.stable(): logging.info("Stable part of the releasing is done") + if self.dry_run: + logging.info("Dry running, clean out possible changes") + rollback = self._rollback_stack.copy() + rollback.reverse() + for cmd in rollback: + self.run(cmd) + return + self.log_post_workflows() self.log_rollback() @@ -240,6 +261,21 @@ class Release: f"for {self.release_type} release" ) + def _commit_cmake_contributors(self, version: ClickHouseVersion) -> None: + update_cmake_version(version) + update_contributors(raise_error=True) + if self.dry_run: + logging.info( + "Dry running, resetting the following changes in the repo:\n%s", + self.run(f"git diff '{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'"), + ) + self.run(f"git checkout '{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'") + self.run( + f"git commit -m 'Update version to {version.string}' " + f"'{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'", + dry_run=self.dry_run, + ) + def log_rollback(self): if self._rollback_stack: rollback = self._rollback_stack.copy() @@ -265,7 +301,6 @@ class Release: self.read_version() with self._create_branch(self.release_branch, self.release_commit): with self._checkout(self.release_branch, True): - self.version.with_description(self.get_stable_release_type()) with self._create_gh_release(False): with self._bump_release_branch(): # At this point everything will rollback automatically @@ -282,6 +317,15 @@ class Release: self.version.with_description(version_type) update_cmake_version(self.version) update_contributors(raise_error=True) + if self.dry_run: + logging.info( + "Dry running, resetting the following changes in the repo:\n%s", + self.run( + f"git diff '{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'" + ), + ) + self.run(f"git checkout '{self.CMAKE_PATH}' '{self.CONTRIBUTORS_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): @@ -289,7 +333,8 @@ class Release: self.run( f"git commit -m " f"'Update version to {self.version.string}' " - f"'{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'" + f"'{self.CMAKE_PATH}' '{self.CONTRIBUTORS_PATH}'", + dry_run=self.dry_run, ) with self._push( "HEAD", with_rollback_on_fail=False, remote_ref=self.release_branch @@ -297,7 +342,7 @@ class Release: # 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"{self.dry_run_prefix}git push {self.repo.url} " f"+{current_commit}:{self.release_branch}" ) yield @@ -339,6 +384,12 @@ class Release: def release_commit(self, release_commit: str) -> None: self._release_commit = commit(release_commit) + @property + def dry_run_prefix(self) -> str: + if self.dry_run: + return "# " + return "" + @contextmanager def _bump_release_branch(self): # Update only git, origal version stays the same @@ -349,12 +400,7 @@ class Release: if version_type == VersionType.LTS: pr_labels += " --label release-lts" new_version.with_description(version_type) - 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}'" - ) + self._commit_cmake_contributors(new_version) with self._push(self.release_branch): with self._create_gh_label( f"v{self.release_branch}-must-backport", "10dbed" @@ -362,14 +408,16 @@ class Release: with self._create_gh_label( f"v{self.release_branch}-affected", "c2bfff" ): - # The following command is rolled back by self._push + # The following command is rolled back by deleting branch + # in self._push self.run( f"gh pr create --repo {self.repo} --title " f"'Release pull request for branch {self.release_branch}' " f"--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.'" + "changes with it.'", + dry_run=self.dry_run, ) # Here the release branch part is done yield @@ -379,18 +427,15 @@ class Release: self.read_version() self.version = self.version.update(self.release_type) self.version.with_description(VersionType.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}'" - ) + self._commit_cmake_contributors(self.version) with self._push(helper_branch): body_file = get_abs_path(".github/PULL_REQUEST_TEMPLATE.md") + # The following command is rolled back by deleting branch in self._push self.run( f"gh pr create --repo {self.repo} --title 'Update version after " f"release' --head {helper_branch} --body-file '{body_file}' " - "--label 'do not test' --assignee @me" + "--label 'do not test' --assignee @me", + dry_run=self.dry_run, ) # Here the testing part is done yield @@ -419,6 +464,7 @@ class Release: @contextmanager def _create_branch(self, name: str, start_point: str = "") -> Iterator[None]: self.run(f"git branch {name} {start_point}") + rollback_cmd = f"git branch -D {name}" self._rollback_stack.append(rollback_cmd) try: @@ -432,9 +478,12 @@ class Release: def _create_gh_label(self, label: str, color_hex: str) -> Iterator[None]: # 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}" + f"gh api repos/{self.repo}/labels -f name={label} -f color={color_hex}", + dry_run=self.dry_run, + ) + rollback_cmd = ( + f"{self.dry_run_prefix}gh api repos/{self.repo}/labels/{label} -X DELETE" ) - rollback_cmd = f"gh api repos/{self.repo}/labels/{label} -X DELETE" self._rollback_stack.append(rollback_cmd) try: yield @@ -447,15 +496,19 @@ class Release: def _create_gh_release(self, as_prerelease: bool) -> Iterator[None]: with self._create_tag(): # Preserve tag if version is changed - tag = self.version.describe + tag = self.release_version.describe prerelease = "" if as_prerelease: prerelease = "--prerelease" self.run( f"gh release create {prerelease} --repo {self.repo} " - f"--title 'Release {tag}' '{tag}'" + f"--title 'Release {tag}' '{tag}'", + dry_run=self.dry_run, + ) + rollback_cmd = ( + f"{self.dry_run_prefix}gh release delete --yes " + f"--repo {self.repo} '{tag}'" ) - rollback_cmd = f"gh release delete --yes --repo {self.repo} '{tag}'" self._rollback_stack.append(rollback_cmd) try: yield @@ -466,12 +519,12 @@ class Release: @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}'" + tag = self.release_version.describe + self.run(f"git tag -a -m 'Release {tag}' '{tag}'", dry_run=self.dry_run) + rollback_cmd = f"{self.dry_run_prefix}git tag -d '{tag}'" self._rollback_stack.append(rollback_cmd) try: - with self._push(f"'{tag}'"): + with self._push(tag): yield except (Exception, KeyboardInterrupt): logging.warning("Rolling back tag %s", tag) @@ -485,9 +538,11 @@ class Release: if remote_ref == "": remote_ref = ref - self.run(f"git push {self.repo.url} {ref}:{remote_ref}") + self.run(f"git push {self.repo.url} {ref}:{remote_ref}", dry_run=self.dry_run) if with_rollback_on_fail: - rollback_cmd = f"git push -d {self.repo.url} {remote_ref}" + rollback_cmd = ( + f"{self.dry_run_prefix}git push -d {self.repo.url} {remote_ref}" + ) self._rollback_stack.append(rollback_cmd) try: @@ -562,6 +617,11 @@ def parse_args() -> argparse.Namespace: "works only for a release branches, that name " "should be the same as '$MAJOR.$MINOR' version, e.g. 22.2", ) + parser.add_argument( + "--dry-run", + action="store_true", + help="do not make any actual changes in the repo, just show what will be done", + ) return parser.parse_args() @@ -570,7 +630,7 @@ def main(): logging.basicConfig(level=logging.INFO) args = parse_args() repo = Repo(args.repo, args.remote_protocol) - release = Release(repo, args.commit, args.release_type) + release = Release(repo, args.commit, args.release_type, args.dry_run) release.do(args.check_dirty, args.check_branch, args.with_release_branch)