diff options
author | Олег <150132506+iddqdex@users.noreply.github.com> | 2025-07-30 19:42:39 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-07-30 18:42:39 +0200 |
commit | 9175dc3828a53c082e9e4138a41cdb937afce77f (patch) | |
tree | f6b6ec047a4219361fed3e469f12be111c95a5df | |
parent | 9dea188ad145a16c31cd5b21085454db92bbd0d1 (diff) | |
download | ydb-9175dc3828a53c082e9e4138a41cdb937afce77f.tar.gz |
Add cherry-pick workflow (#21938)
-rwxr-xr-x | .github/scripts/cherry_pick.py | 123 | ||||
-rw-r--r-- | .github/workflows/cherry_pick.yml | 58 |
2 files changed, 181 insertions, 0 deletions
diff --git a/.github/scripts/cherry_pick.py b/.github/scripts/cherry_pick.py new file mode 100755 index 00000000000..24a6a29a1ff --- /dev/null +++ b/.github/scripts/cherry_pick.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +import os +import datetime +import logging +import subprocess +import argparse +from github import Github, GithubException, GithubObject, Commit + + +class CherryPickCreator: + def __init__(self, args): + def __split(s: str, seps: str = ', '): + if not s: + return [] + if not seps: + return [s] + result = [] + for part in s.split(seps[0]): + result += __split(part, seps[1:]) + return result + + self.repo_name = os.environ["REPO"] + self.target_branches = __split(args.target_branches) + self.token = os.environ["TOKEN"] + self.gh = Github(login_or_token=self.token) + self.repo = self.gh.get_repo(self.repo_name) + try: + self.issue = self.repo.get_issue(args.issue) + except: + self.issue = GithubObject.NotSet + self.commit_shas: list[str] = [] + self.pr_title_list: list[str] = [] + self.pr_body_list: list[str] = [] + for c in __split(args.commits): + commit = self.repo.get_commit(c) + self.pr_title_list.append(commit.sha) + self.pr_body_list.append(commit.html_url) + self.commit_shas.append(commit.sha) + for p in __split(args.pulls): + pull = self.repo.get_pull(int(p)) + self.pr_title_list.append(f'PR {pull.number}') + self.pr_body_list.append(pull.html_url) + self.commit_shas.append(pull.merge_commit_sha) + self.dtm = datetime.datetime.now().strftime("%y%m%d-%H%M") + self.logger = logging.getLogger("cherry-pick") + self.workflow_url = None + self.__detect_env() + + def __detect_env(self): + if "GITHUB_RUN_ID" in os.environ: + self.workflow_url = ( + f"{os.environ['GITHUB_SERVER_URL']}/{self.repo_name}/actions/runs/{os.environ['GITHUB_RUN_ID']}" + ) + + def add_summary(self, msg): + summary_path = os.getenv('GITHUB_STEP_SUMMARY') + if summary_path: + with open(summary_path, 'a') as summary: + summary.write(msg) + + def git_run(self, *args): + args = ["git"] + list(args) + + self.logger.info("run: %r", args) + try: + output = subprocess.check_output(args).decode() + except subprocess.CalledProcessError as e: + self.logger.error(e.output.decode()) + raise + else: + self.logger.info("output:\n%s", output) + return output + + def create_pr_for_branch(self, target_branch): + dev_branch_name = f"cherry-pick-{target_branch}-{self.dtm}" + self.git_run("reset", "--hard") + self.git_run("checkout", target_branch) + self.git_run("checkout", "-b", dev_branch_name) + self.git_run("cherry-pick", "--allow-empty", *self.commit_shas) + self.git_run("push", "--set-upstream", "origin", dev_branch_name) + + pr_title = f"Cherry-pick {', '.join(self.pr_title_list)} to {target_branch}" + pr_body = f"Cherry-pick {', '.join(self.pr_body_list)} to {target_branch}\n\n" + if self.workflow_url: + pr_body += f"PR was created by cherry-pick workflow [run]({self.workflow_url})" + else: + pr_body += "PR was created by cherry-pick script" + + pr = self.repo.create_pull( + target_branch, dev_branch_name, title=pr_title, body=pr_body, maintainer_can_modify=True, issue=self.issue + ) + self.add_summary(f'{target_branch}: PR {pr.html_url} created\n') + + def process(self): + if len(self.commit_shas) == 0 or len(self.target_branches) == 0: + self.add_summary("Noting to cherry-pick or no targets branches, my life is meaningless.") + return + self.git_run("clone", f"https://{self.token}@github.com/{self.repo_name}.git", "-c", "protocol.version=2", f"ydb-new-pr") + os.chdir(f"ydb-new-pr") + for target in self.target_branches: + try: + self.create_pr_for_branch(target) + except GithubException as e: + self.add_summary(f'{target} error {type(e)}\n```\n{e}\n```\n') + except BaseException as e: + self.add_summary(f'{target} error {type(e)}\n```\n{e}\n```\n') + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--commits", help="Comma or space separated list of commit SHAs") + parser.add_argument("--pulls", help="Comma or space separated list of PR numbers") + parser.add_argument("--target-branches", help="Comma or space separated list of branchs to cherry-pick") + parser.add_argument("--issue", help="Issue number for attach PR", type=int, default=0) + args = parser.parse_args() + + log_fmt = "%(asctime)s - %(levelname)s - %(name)s - %(message)s" + logging.basicConfig(format=log_fmt, level=logging.DEBUG) + CherryPickCreator(args).process() + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/cherry_pick.yml b/.github/workflows/cherry_pick.yml new file mode 100644 index 00000000000..85c89028dc2 --- /dev/null +++ b/.github/workflows/cherry_pick.yml @@ -0,0 +1,58 @@ +name: Cherry-pick +on: + workflow_dispatch: + inputs: + commits: + type: string + default: "" + description: Comma or space separated commit SHAs for cherry-pick + pulls: + type: string + default: "" + description: Comma or space separated PR numbers for cherry-pick + target_branches: + default: "" + description: Comma or space separated branches to cherry-pick + issue_number: + default: 0 + type: number + description: Issue number for attach PRs + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true +env: + GH_TOKEN: ${{ secrets.YDBOT_TOKEN }} +jobs: + create-pr: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + sparse-checkout: | + .github + - name: install packages + shell: bash + run: | + pip install PyGithub==2.5.0 + + - name: configure + shell: bash + run: | + git config --global user.name YDBot + git config --global user.email ydbot@ydb.tech + git config --local github.token ${{ env.GH_TOKEN }} + + - name: run-command + shell: bash + env: + REPO: ${{ github.repository }} + TOKEN: ${{ env.GH_TOKEN }} + run: | + ./.github/scripts/cherry_pick.py \ + --pulls="${{ inputs.pulls }}" + --commits="${{ inputs.commits }}" \ + --target-branches="${{ inputs.target_branches }}" \ + --issue=${{ inputs.issue_number }} |