mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-12-13 09:52:38 +00:00
337 lines
12 KiB
Python
Executable File
337 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
|
|
from contextlib import contextmanager
|
|
from typing import List, Optional
|
|
import argparse
|
|
import logging
|
|
|
|
from git_helper import commit
|
|
from version_helper import (
|
|
FILE_WITH_VERSION_PATH,
|
|
ClickHouseVersion,
|
|
VersionType,
|
|
git,
|
|
get_abs_path,
|
|
get_version_from_repo,
|
|
update_cmake_version,
|
|
)
|
|
|
|
|
|
class Release:
|
|
BIG = ("major", "minor")
|
|
SMALL = ("patch",)
|
|
|
|
def __init__(self, version: ClickHouseVersion):
|
|
self._version = version
|
|
self._git = version._git
|
|
self._release_commit = ""
|
|
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 update(self):
|
|
self._git.update()
|
|
self.version = get_version_from_repo()
|
|
|
|
def do(self, args: argparse.Namespace):
|
|
self.release_commit = args.commit
|
|
|
|
if not args.no_check_dirty:
|
|
logging.info("Checking if repo is clean")
|
|
self.run("git diff HEAD --exit-code")
|
|
|
|
if not args.no_check_branch:
|
|
self.check_branch(args.release_type)
|
|
|
|
if args.release_type in self.BIG:
|
|
# Checkout to the commit, it will provide the correct current version
|
|
with self._checkout(self.release_commit, True):
|
|
if args.no_prestable:
|
|
logging.info("Skipping prestable stage")
|
|
else:
|
|
with self.prestable(args):
|
|
logging.info("Prestable part of the releasing is done")
|
|
|
|
with self.testing(args):
|
|
logging.info("Testing 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, release_type: str):
|
|
if release_type in self.BIG:
|
|
# Commit to spin up the release must belong to a main branch
|
|
output = self.run(f"git branch --contains={self.release_commit} master")
|
|
if "master" not in output:
|
|
raise Exception(
|
|
f"commit {self.release_commit} must belong to 'master' for "
|
|
f"{release_type} release"
|
|
)
|
|
if release_type in self.SMALL:
|
|
branch = f"{self.version.major}.{self.version.minor}"
|
|
if self._git.branch != branch:
|
|
raise Exception(f"branch must be '{branch}' for {release_type} release")
|
|
|
|
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, args: argparse.Namespace):
|
|
self.check_no_tags_after()
|
|
# Create release branch
|
|
self.update()
|
|
release_branch = f"{self.version.major}.{self.version.minor}"
|
|
with self._create_branch(release_branch, self.release_commit):
|
|
with self._checkout(release_branch, True):
|
|
self.update()
|
|
self.version.with_description(VersionType.PRESTABLE)
|
|
with self._create_gh_release(args):
|
|
with self._bump_prestable_version(release_branch, args):
|
|
# At this point everything will rollback automatically
|
|
yield
|
|
|
|
@contextmanager
|
|
def testing(self, args: argparse.Namespace):
|
|
# Create branch for a version bump
|
|
self.update()
|
|
self.version = self.version.update(args.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):
|
|
self.update()
|
|
self.version = self.version.update(args.release_type)
|
|
with self._bump_testing_version(helper_branch, args):
|
|
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_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, 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
|
|
|
|
@contextmanager
|
|
def _bump_testing_version(self, helper_branch: str, args: argparse.Namespace):
|
|
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, 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
|
|
|
|
@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: str, args: argparse.Namespace):
|
|
self.run(f"gh api repos/{args.repo}/labels -f name={label} -f color={color}")
|
|
rollback_cmd = f"gh api repos/{args.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, args: argparse.Namespace):
|
|
with self._create_tag(args):
|
|
# Preserve tag if version is changed
|
|
tag = self.version.describe
|
|
self.run(
|
|
f"gh release create --prerelease --draft --repo {args.repo} "
|
|
f"--title 'Release {tag}' '{tag}'"
|
|
)
|
|
rollback_cmd = f"gh release delete --yes --repo {args.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, args: argparse.Namespace):
|
|
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}'", args):
|
|
yield
|
|
except BaseException:
|
|
logging.warning("Rolling back tag %s", tag)
|
|
self.run(rollback_cmd)
|
|
raise
|
|
|
|
@contextmanager
|
|
def _push(self, ref: str, args: argparse.Namespace):
|
|
self.run(f"git push git@github.com:{args.repo}.git {ref}")
|
|
rollback_cmd = f"git push -d git@github.com:{args.repo}.git {ref}"
|
|
self._rollback_stack.append(rollback_cmd)
|
|
try:
|
|
yield
|
|
except BaseException:
|
|
logging.warning("Rolling back pushed ref %s", ref)
|
|
self.run(rollback_cmd)
|
|
raise
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
description="Script to release a new ClickHouse version, requires `git` and "
|
|
"`gh` (github-cli) commands",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--repo",
|
|
default="ClickHouse/ClickHouse",
|
|
help="repository to create the release",
|
|
)
|
|
parser.add_argument(
|
|
"--type",
|
|
default="minor",
|
|
# choices=Release.BIG+Release.SMALL, # add support later
|
|
choices=Release.BIG + Release.SMALL,
|
|
dest="release_type",
|
|
help="a release type, new branch is created only for 'major' and 'minor'",
|
|
)
|
|
parser.add_argument(
|
|
"--no-prestable",
|
|
action="store_true",
|
|
help=f"for release types in {Release.BIG} skip creating prestable release and "
|
|
"release branch",
|
|
)
|
|
parser.add_argument(
|
|
"--commit",
|
|
default=git.sha,
|
|
type=commit,
|
|
help="commit create a release, default to HEAD",
|
|
)
|
|
parser.add_argument(
|
|
"--no-check-dirty",
|
|
action="store_true",
|
|
help="skip check repository for uncommited changes",
|
|
)
|
|
parser.add_argument(
|
|
"--no-check-branch",
|
|
action="store_true",
|
|
help="by default, 'major' and 'minor' types work only for master, and 'patch' "
|
|
"works only for a release branches, that name should be the same as "
|
|
"'$MAJOR.$MINOR' version, e.g. 22.2",
|
|
)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def prestable():
|
|
pass
|
|
|
|
|
|
def main():
|
|
logging.basicConfig(level=logging.INFO)
|
|
args = parse_args()
|
|
release = Release(get_version_from_repo())
|
|
|
|
release.do(args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|