Пример #1
0
 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
Пример #2
0
 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
Пример #3
0
 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
Пример #4
0
 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
Пример #5
0
 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
Пример #6
0
 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
Пример #7
0
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