mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-25 09:02:00 +00:00
1014 lines
37 KiB
Python
Executable File
1014 lines
37 KiB
Python
Executable File
#!/usr/bin/env python
|
|
|
|
"""
|
|
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 <<EOT > /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
|
|
"""
|
|
|
|
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}' "
|
|
f"-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' "
|
|
f'--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 = "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.rsplit("/", 1)[-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.rsplit("/", 1)[-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 <PROGRESS STEP> 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)
|