Merge pull request #54604 from ClickHouse/autorelease-script

Add basic logic to find releasable commits
This commit is contained in:
Mikhail f. Shiryaev 2023-09-15 11:45:57 +02:00 committed by GitHub
commit b12eb8defd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 203 additions and 14 deletions

45
.github/workflows/auto_release.yml vendored Normal file
View 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
View File

@ -1,3 +1,4 @@
*_lambda/lambda-venv
*_lambda/lambda-package
*_lambda/lambda-package.zip
gh_cache

140
tests/ci/auto_release.py Normal file
View 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()

View File

@ -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

View File

@ -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:

View File

@ -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",
)