mirror of
https://github.com/ClickHouse/ClickHouse.git
synced 2024-11-22 15:42:02 +00:00
Merge pull request #54604 from ClickHouse/autorelease-script
Add basic logic to find releasable commits
This commit is contained in:
commit
b12eb8defd
45
.github/workflows/auto_release.yml
vendored
Normal file
45
.github/workflows/auto_release.yml
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
name: AutoRelease
|
||||
|
||||
env:
|
||||
# Force the stdout and stderr streams to be unbuffered
|
||||
PYTHONUNBUFFERED: 1
|
||||
|
||||
concurrency:
|
||||
group: auto-release
|
||||
on: # yamllint disable-line rule:truthy
|
||||
# schedule:
|
||||
# - cron: '0 10-16 * * 1-5'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
CherryPick:
|
||||
runs-on: [self-hosted, style-checker-aarch64]
|
||||
steps:
|
||||
- name: Set envs
|
||||
# https://docs.github.com/en/actions/learn-github-actions/workflow-commands-for-github-actions#multiline-strings
|
||||
run: |
|
||||
cat >> "$GITHUB_ENV" << 'EOF'
|
||||
TEMP_PATH=${{runner.temp}}/cherry_pick
|
||||
ROBOT_CLICKHOUSE_SSH_KEY<<RCSK
|
||||
${{secrets.ROBOT_CLICKHOUSE_SSH_KEY}}
|
||||
RCSK
|
||||
REPO_OWNER=ClickHouse
|
||||
REPO_NAME=ClickHouse
|
||||
REPO_TEAM=core
|
||||
EOF
|
||||
- name: Check out repository code
|
||||
uses: ClickHouse/checkout@v1
|
||||
with:
|
||||
clear-repository: true
|
||||
token: ${{secrets.ROBOT_CLICKHOUSE_COMMIT_TOKEN}}
|
||||
fetch-depth: 0
|
||||
- name: Auto-release
|
||||
run: |
|
||||
cd "$GITHUB_WORKSPACE/tests/ci"
|
||||
python3 auto_release.py --release-after-days=3
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
docker ps --quiet | xargs --no-run-if-empty docker kill ||:
|
||||
docker ps --all --quiet | xargs --no-run-if-empty docker rm -f ||:
|
||||
sudo rm -fr "$TEMP_PATH"
|
1
tests/ci/.gitignore
vendored
1
tests/ci/.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
*_lambda/lambda-venv
|
||||
*_lambda/lambda-package
|
||||
*_lambda/lambda-package.zip
|
||||
gh_cache
|
140
tests/ci/auto_release.py
Normal file
140
tests/ci/auto_release.py
Normal file
@ -0,0 +1,140 @@
|
||||
import argparse
|
||||
from datetime import timedelta, datetime
|
||||
import logging
|
||||
import os
|
||||
from commit_status_helper import get_commit_filtered_statuses
|
||||
from get_robot_token import get_best_robot_token
|
||||
from github_helper import GitHub
|
||||
from release import Release, Repo as ReleaseRepo, RELEASE_READY_STATUS
|
||||
from report import SUCCESS
|
||||
from ssh import SSHKey
|
||||
|
||||
LOGGER_NAME = __name__
|
||||
HELPER_LOGGERS = ["github_helper", LOGGER_NAME]
|
||||
logger = logging.getLogger(LOGGER_NAME)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
"Checks if enough days elapsed since the last release on each release "
|
||||
"branches and do a release in case for green builds."
|
||||
)
|
||||
parser.add_argument("--token", help="GitHub token, if not set, used from smm")
|
||||
parser.add_argument(
|
||||
"--repo", default="ClickHouse/ClickHouse", help="Repo owner/name"
|
||||
)
|
||||
parser.add_argument("--dry-run", action="store_true", help="Do not create anything")
|
||||
parser.add_argument(
|
||||
"--release-after-days",
|
||||
type=int,
|
||||
default=3,
|
||||
help="Do automatic release on the latest green commit after the latest "
|
||||
"release if the newest release is older than the specified days",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--debug-helpers",
|
||||
action="store_true",
|
||||
help="Add debug logging for this script and github_helper",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--remote-protocol",
|
||||
"-p",
|
||||
default="ssh",
|
||||
choices=ReleaseRepo.VALID,
|
||||
help="repo protocol for git commands remote, 'origin' is a special case and "
|
||||
"uses 'origin' as a remote",
|
||||
)
|
||||
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
if args.debug_helpers:
|
||||
for logger_name in HELPER_LOGGERS:
|
||||
logging.getLogger(logger_name).setLevel(logging.DEBUG)
|
||||
|
||||
token = args.token or get_best_robot_token()
|
||||
days_as_timedelta = timedelta(days=args.release_after_days)
|
||||
now = datetime.now()
|
||||
|
||||
gh = GitHub(token)
|
||||
prs = gh.get_release_pulls(args.repo)
|
||||
branch_names = [pr.head.ref for pr in prs]
|
||||
|
||||
logger.info("Found release branches: %s\n ", " \n".join(branch_names))
|
||||
repo = gh.get_repo(args.repo)
|
||||
|
||||
# In general there is no guarantee on which order the refs/commits are
|
||||
# returned from the API, so we have to order them.
|
||||
for pr in prs:
|
||||
logger.info("Checking PR %s", pr.head.ref)
|
||||
|
||||
refs = list(repo.get_git_matching_refs(f"tags/v{pr.head.ref}"))
|
||||
refs.sort(key=lambda ref: ref.ref)
|
||||
|
||||
latest_release_tag_ref = refs[-1]
|
||||
latest_release_tag = repo.get_git_tag(latest_release_tag_ref.object.sha)
|
||||
logger.info("That last release was done at %s", latest_release_tag.tagger.date)
|
||||
|
||||
if latest_release_tag.tagger.date + days_as_timedelta > now:
|
||||
logger.info(
|
||||
"Not enough days since the last release %s,"
|
||||
" no automatic release can be done",
|
||||
latest_release_tag.tag,
|
||||
)
|
||||
continue
|
||||
|
||||
unreleased_commits = list(
|
||||
repo.get_commits(sha=pr.head.ref, since=latest_release_tag.tagger.date)
|
||||
)
|
||||
unreleased_commits.sort(
|
||||
key=lambda commit: commit.commit.committer.date, reverse=True
|
||||
)
|
||||
|
||||
for commit in unreleased_commits:
|
||||
logger.info("Checking statuses of commit %s", commit.sha)
|
||||
statuses = get_commit_filtered_statuses(commit)
|
||||
all_success = all(st.state == SUCCESS for st in statuses)
|
||||
passed_ready_for_release_check = any(
|
||||
st.context == RELEASE_READY_STATUS and st.state == SUCCESS
|
||||
for st in statuses
|
||||
)
|
||||
if not (all_success and passed_ready_for_release_check):
|
||||
logger.info("Commit is not green, thus not suitable for release")
|
||||
continue
|
||||
|
||||
logger.info("Commit is ready for release, let's release!")
|
||||
|
||||
release = Release(
|
||||
ReleaseRepo(args.repo, args.remote_protocol),
|
||||
commit.sha,
|
||||
"patch",
|
||||
args.dry_run,
|
||||
True,
|
||||
)
|
||||
try:
|
||||
release.do(True, True, True)
|
||||
except:
|
||||
if release.has_rollback:
|
||||
logging.error(
|
||||
"!!The release process finished with error, read the output carefully!!"
|
||||
)
|
||||
logging.error(
|
||||
"Probably, rollback finished with error. "
|
||||
"If you don't see any of the following commands in the output, "
|
||||
"execute them manually:"
|
||||
)
|
||||
release.log_rollback()
|
||||
raise
|
||||
logging.info("New release is done!")
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if os.getenv("ROBOT_CLICKHOUSE_SSH_KEY", ""):
|
||||
with SSHKey("ROBOT_CLICKHOUSE_SSH_KEY"):
|
||||
main()
|
||||
else:
|
||||
main()
|
@ -397,12 +397,7 @@ class Backport:
|
||||
|
||||
def receive_release_prs(self):
|
||||
logging.info("Getting release PRs")
|
||||
self.release_prs = self.gh.get_pulls_from_search(
|
||||
query=f"type:pr repo:{self._repo_name} is:open",
|
||||
sort="created",
|
||||
order="asc",
|
||||
label="release",
|
||||
)
|
||||
self.release_prs = self.gh.get_release_pulls(self._repo_name)
|
||||
self.release_branches = [pr.head.ref for pr in self.release_prs]
|
||||
self.labels_to_backport = [
|
||||
f"v{branch}-must-backport" for branch in self.release_branches
|
||||
|
@ -119,6 +119,14 @@ class GitHub(github.Github):
|
||||
)
|
||||
return prs
|
||||
|
||||
def get_release_pulls(self, repo_name: str) -> PullRequests:
|
||||
return self.get_pulls_from_search(
|
||||
query=f"type:pr repo:{repo_name} is:open",
|
||||
sort="created",
|
||||
order="asc",
|
||||
label="release",
|
||||
)
|
||||
|
||||
def sleep_on_rate_limit(self):
|
||||
for limit, data in self.get_rate_limit().raw_data.items():
|
||||
if data["remaining"] == 0:
|
||||
|
@ -3,7 +3,7 @@
|
||||
"""
|
||||
script to create releases for ClickHouse
|
||||
|
||||
The `gh` CLI prefered over the PyGithub to have an easy way to rollback bad
|
||||
The `gh` CLI preferred over the PyGithub to have an easy way to rollback bad
|
||||
release in command line by simple execution giving rollback commands
|
||||
|
||||
On another hand, PyGithub is used for convenient getting commit's status from API
|
||||
@ -197,11 +197,11 @@ class Release:
|
||||
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"
|
||||
"The release type must be 'major' for minor versions>=12"
|
||||
)
|
||||
if self._version.minor < 12 and self.release_type == "major":
|
||||
raise ValueError(
|
||||
"The relese type must be 'minor' for minor versions<12"
|
||||
"The release type must be 'minor' for minor versions<12"
|
||||
)
|
||||
|
||||
with self._checkout(self.release_commit, True):
|
||||
@ -250,7 +250,7 @@ class Release:
|
||||
elif self.release_type not in self.SMALL:
|
||||
raise (
|
||||
ValueError(
|
||||
f"release_type {self.release_type} neiter in {self.BIG} nor "
|
||||
f"release_type {self.release_type} neither in {self.BIG} nor "
|
||||
f"in {self.SMALL}"
|
||||
)
|
||||
)
|
||||
@ -338,7 +338,7 @@ class Release:
|
||||
self.version = self.version.update(self.release_type)
|
||||
self.version.with_description(version_type)
|
||||
self._update_cmake_contributors(self.version)
|
||||
# Checkouting the commit of the branch and not the branch itself,
|
||||
# Checking out 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")
|
||||
@ -399,7 +399,7 @@ class Release:
|
||||
|
||||
@contextmanager
|
||||
def _bump_release_branch(self):
|
||||
# Update only git, origal version stays the same
|
||||
# Update only git, original version stays the same
|
||||
self._git.update()
|
||||
new_version = self.version.patch_update()
|
||||
version_type = self.get_stable_release_type()
|
||||
@ -610,7 +610,7 @@ def parse_args() -> argparse.Namespace:
|
||||
dest="check_dirty",
|
||||
action="store_false",
|
||||
default=argparse.SUPPRESS,
|
||||
help="(dangerous) if set, skip check repository for uncommited changes",
|
||||
help="(dangerous) if set, skip check repository for uncommitted changes",
|
||||
)
|
||||
parser.add_argument("--check-run-from-master", default=True, help=argparse.SUPPRESS)
|
||||
parser.add_argument(
|
||||
@ -627,7 +627,7 @@ def parse_args() -> argparse.Namespace:
|
||||
action="store_false",
|
||||
default=argparse.SUPPRESS,
|
||||
help="(debug or development only, dangerous) if set, skip the branch check for "
|
||||
"a run. By default, 'major' and 'minor' types workonly for master, and 'patch' "
|
||||
"a run. 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",
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user