diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 8ce75d9..f698d3f 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -13,7 +13,7 @@ jobs: name: Tox test strategy: matrix: - tox_env: [py36, py37, py38, py39, py310, py311, py312] + tox_env: [py38, py39, py310, py311, py312] # Use GitHub's Linux Docker host runs-on: ubuntu-latest steps: diff --git a/auto-merger b/auto-merger index 25c5c9e..b2f7ed7 100755 --- a/auto-merger +++ b/auto-merger @@ -30,39 +30,29 @@ import sys import click -from auto_merger.merger import AutoMerger -from auto_merger.utils import setup_logger +from auto_merger.config import GlobalConfig +from auto_merger.utils import setup_logger +from auto_merger.cli.pr_checker import pr_checker +from auto_merger.cli.merger import merger logger = logging.getLogger(__name__) -@click.command() +@click.group("auto-merger") @click.option("-d", "--debug", is_flag=True, help="Enable debug logs") -@click.option("--print-results", is_flag=True, help="Prints readable summary") -@click.option("--github-labels", required=True, multiple=True, - help="Specify Git Hub labels to meet criteria") -@click.option("--blocking-labels", multiple=True, - help="Specify Git Hub labels that blocks PR to merge") -@click.option("--send-email", multiple=True, help="Specify email addresses to which the mail will be sent.") -@click.option("--approvals", - default=2, type=int, - help="Specify number of approvals to automatically merge PR. Default 2") -def auto_merger(debug, print_results, github_labels, blocking_labels, approvals, send_email): - am = AutoMerger(github_labels, blocking_labels, approvals) +@click.pass_context +def auto_merger(ctx, debug): + ctx.obj = GlobalConfig(debug=debug) if debug: setup_logger("auto-merger", level=logging.DEBUG) + logger.debug("Logging set to DEBUG") else: setup_logger("auto-merger", level=logging.INFO) - ret_value = am.check_all_containers() - if ret_value != 0: - sys.exit(2) - if print_results: - am.print_blocked_pull_request() - am.print_approval_pull_request() - if not am.send_results(send_email): - sys.exit(1) - sys.exit(ret_value) + logger.debug("Logging set to INFO") + +auto_merger.add_command(pr_checker) +auto_merger.add_command(merger) if __name__ == "__main__": - auto_merger() + auto_merger(obj={}) diff --git a/auto_merger/api.py b/auto_merger/api.py new file mode 100644 index 0000000..d549e96 --- /dev/null +++ b/auto_merger/api.py @@ -0,0 +1,60 @@ +# MIT License +# +# Copyright (c) 2018-2019 Red Hat, Inc. + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import logging +import sys + +from pathlib import Path +from typing import Dict + +from auto_merger.pr_checker import PRStatusChecker +from auto_merger.merger import AutoMerger + +logger = logging.getLogger(__name__) + + +def pr_checker(print_results, github_labels, blocking_labels, approvals, send_email) -> int: + """ + Checks NVR from brew build against pulp + """ + pr_status_checker = PRStatusChecker(github_labels, blocking_labels, approvals) + ret_value = pr_status_checker.check_all_containers() + if ret_value != 0: + return ret_value + if print_results: + pr_status_checker.print_blocked_pull_request() + pr_status_checker.print_approval_pull_request() + if not pr_status_checker.send_results(send_email): + return 1 + return ret_value + + +def merger(print_results, merger_labels, approvals, pr_lifetime, send_email) -> int: + auto_merger = AutoMerger(merger_labels, approvals, pr_lifetime) + ret_value = auto_merger.check_all_containers() + if ret_value != 0: + return ret_value + if print_results: + auto_merger.print_pull_request_to_merge() + if not auto_merger.send_results(send_email): + return 1 + return ret_value \ No newline at end of file diff --git a/auto_merger/cli/__init__.py b/auto_merger/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auto_merger/cli/merger.py b/auto_merger/cli/merger.py new file mode 100644 index 0000000..5d817cb --- /dev/null +++ b/auto_merger/cli/merger.py @@ -0,0 +1,47 @@ +# MIT License +# +# Copyright (c) 2018-2019 Red Hat, Inc. + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import logging +import click +import sys + +from auto_merger.config import pass_global_config +from auto_merger import api +logger = logging.getLogger(__name__) + + +@click.command("merger") +@click.option("--print-results", is_flag=True, help="Prints readable summary") +@click.option("--merger-labels", required=True, multiple=True, + help="Specify Git Hub labels to meet criteria") +@click.option("--send-email", multiple=True, help="Specify email addresses to which the mail will be sent.") +@click.option("--approvals", + default=2, type=int, + help="Specify number of approvals to automatically merge PR. Default 2") +@click.option("--pr-lifetime", default=1, type=int, help="Specify a smallest time for which PR should opened") +@pass_global_config +def merger(ctx, print_results, merger_labels, approvals, pr_lifetime, send_email): + logger.debug(ctx.debug) + ret_value = api.merger(print_results, merger_labels, approvals, pr_lifetime, send_email) + sys.exit(ret_value) + diff --git a/auto_merger/cli/pr_checker.py b/auto_merger/cli/pr_checker.py new file mode 100644 index 0000000..05518f0 --- /dev/null +++ b/auto_merger/cli/pr_checker.py @@ -0,0 +1,49 @@ +# MIT License +# +# Copyright (c) 2018-2019 Red Hat, Inc. + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import logging +import click +import sys + +from auto_merger.config import pass_global_config +from auto_merger import api + +logger = logging.getLogger(__name__) + + +@click.command("pr-checker") +@click.option("--print-results", is_flag=True, help="Prints readable summary") +@click.option("--github-labels", required=True, multiple=True, + help="Specify Git Hub labels to meet criteria") +@click.option("--blocking-labels", multiple=True, + help="Specify Git Hub labels that blocks PR to merge") +@click.option("--send-email", multiple=True, help="Specify email addresses to which the mail will be sent.") +@click.option("--approvals", + default=2, type=int, + help="Specify number of approvals to automatically merge PR. Default 2") +@pass_global_config +def pr_checker(ctx, print_results, github_labels, blocking_labels, approvals, send_email): + logger.debug(ctx.debug) + ret_value = api.pr_checker(print_results, github_labels, blocking_labels, approvals, send_email) + sys.exit(ret_value) + diff --git a/auto_merger/config.py b/auto_merger/config.py new file mode 100644 index 0000000..83102fb --- /dev/null +++ b/auto_merger/config.py @@ -0,0 +1,32 @@ +# MIT License +# +# Copyright (c) 2018-2019 Red Hat, Inc. + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import click + + +class GlobalConfig(object): + def __init__(self, debug: bool = False): + self.debug = debug + + +pass_global_config = click.make_pass_decorator(GlobalConfig) diff --git a/auto_merger/merger.py b/auto_merger/merger.py index eb72b84..cb1c7d3 100644 --- a/auto_merger/merger.py +++ b/auto_merger/merger.py @@ -42,17 +42,13 @@ class AutoMerger: container_dir: Path current_dir = os.getcwd() - def __init__(self, github_labels, blocking_labels, approvals=2): + def __init__(self, github_labels, approvals: int = 2, pr_lifetime: int = 1): self.logger = setup_logger("AutoMerger") - self.github_labels = list(github_labels) - self.blocking_labels = list(blocking_labels) + self.approval_labels = list(github_labels) self.approvals = approvals - self.logger.debug(f"GitHub Labels: {self.github_labels}") - self.logger.debug(f"GitHub Blocking Labels: {self.blocking_labels}") + self.logger.debug(f"GitHub Labels: {self.approval_labels}") self.logger.debug(f"Approvals Labels: {self.approvals}") - self.blocked_pr = {} self.pr_to_merge = {} - self.blocked_body = [] self.approval_body = [] self.repo_data: List = [] @@ -93,22 +89,6 @@ def is_authenticated(self): return False return True - def add_blocked_pr(self, pr: {}): - present = False - for stored_pr in self.blocked_pr[self.container_name]: - if int(stored_pr["number"]) == int(pr["number"]): - present = True - if present: - return - self.blocked_pr[self.container_name].append({ - "number": pr["number"], - "pr_dict": { - "title": pr["title"], - "labels": pr["labels"] - } - }) - self.logger.debug(f"PR {pr['number']} added to blocked") - def add_approved_pr(self, pr: {}): self.pr_to_merge[self.container_name].append({ "number": pr["number"], @@ -119,22 +99,11 @@ def add_approved_pr(self, pr: {}): }) self.logger.debug(f"PR {pr['number']} added to approved") - def check_blocked_labels(self): - for pr in self.repo_data: - self.logger.debug(f"Check blocked: {pr}") - if "labels" not in pr: - continue - for label in pr["labels"]: - if label["name"] not in self.blocking_labels: - continue - self.logger.debug(f"Add '{pr['number']}' to blocked PRs.") - self.add_blocked_pr(pr) - def check_labels_to_merge(self, pr): if "labels" not in pr: return True for label in pr["labels"]: - if label["name"] in self.blocking_labels: + if label["name"] in self.approval_labels: return False self.logger.debug(f"Add '{pr['number']}' to approved PRs.") return True @@ -215,17 +184,10 @@ def check_all_containers(self) -> int: self.logger.error(f"This is not correct repo {self.container_name}.") self.clean_dirs() continue - if self.container_name not in self.blocked_pr: - self.blocked_pr[self.container_name] = [] if self.container_name not in self.pr_to_merge: self.pr_to_merge[self.container_name] = [] try: self.get_gh_pr_list() - self.check_blocked_labels() - if len(self.blocked_pr[self.container_name]) != 0: - self.logger.info( - f"This pull request can not be merged {self.pr_to_merge}" - ) self.check_pr_to_merge() except subprocess.CalledProcessError: self.clean_dirs() @@ -234,52 +196,31 @@ def check_all_containers(self) -> int: self.clean_dirs() return 0 - def get_blocked_labels(self, pr_dict) -> List [str]: - labels = [] - for lbl in pr_dict["labels"]: - labels.append(lbl["name"]) - return labels - - def print_blocked_pull_request(self): - # Do not print anything in case we do not have PR. - if not [x for x in self.blocked_pr if self.blocked_pr[x]]: - return 0 - self.blocked_body.append( - f"Pull requests that are blocked by labels <b>[{', '.join(self.blocking_labels)}]</b><br><br>" - ) - - for container, pull_requests in self.blocked_pr.items(): - if not pull_requests: - continue - self.blocked_body.append(f"<b>{container}<b>:") - self.blocked_body.append("<table><tr><th>Pull request URL</th><th>Title</th><th>Missing labels</th></tr>") - for pr in pull_requests: - blocked_labels = self.get_blocked_labels(pr["pr_dict"]) - self.blocked_body.append( - f"<tr><td>https://github.com/sclorg/{container}/pull/{pr['number']}</td>" - f"<td>{pr['pr_dict']['title']}</td><td><p style='color:red;'>{' '.join(blocked_labels)}</p></td></tr>" - ) - self.blocked_body.append("</table><br><br>") - print('\n'.join(self.blocked_body)) - def print_approval_pull_request(self): + def print_pull_request_to_merge(self): # Do not print anything in case we do not have PR. if not [x for x in self.pr_to_merge if self.pr_to_merge[x]]: return 0 - self.approval_body.append(f"Pull requests that can be merged or missing {self.approvals} approvals") - self.approval_body.append("<table><tr><th>Pull request URL</th><th>Title</th><th>Approval status</th></tr>") + to_approval: bool = False + pr_body: List = [] for container, pr in self.pr_to_merge.items(): if not pr: continue - if int(pr["approvals"]) >= self.approvals: - result_pr = f"CAN BE MERGED" - else: - result_pr = f"Missing {self.approvals-int(pr['approvals'])} APPROVAL" - self.approval_body.append( + if int(pr["approvals"]) < self.approvals: + continue + to_approval = True + result_pr = f"CAN BE MERGED" + pr_body.append( f"<tr><td>https://github.com/sclorg/{container}/pull/{pr['number']}</td>" f"<td>{pr['pr_dict']['title']}</td><td><p style='color:red;'>{result_pr}</p></td></tr>" ) - self.approval_body.append("</table><br>") + if to_approval: + self.approval_body.append(f"Pull requests that can be merged.") + self.approval_body.append("<table><tr><th>Pull request URL</th><th>Title</th><th>Approval status</th></tr>") + self.approval_body.extend(pr_body) + self.approval_body.append("</table><br>") + else: + self.approval_body.append("There are not pull requests to be merged.") print('\n'.join(self.approval_body)) def send_results(self, recipients): @@ -288,7 +229,7 @@ def send_results(self, recipients): return 1 sender_class = EmailSender(recipient_email=list(recipients)) subject_msg = "Pull request statuses for organization https://gibhub.com/sclorg" - sender_class.send_email(subject_msg, self.blocked_body + self.approval_body) + sender_class.send_email(subject_msg, self.approval_body) def run(): diff --git a/auto_merger/pr_checker.py b/auto_merger/pr_checker.py new file mode 100644 index 0000000..53e458f --- /dev/null +++ b/auto_merger/pr_checker.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 + +# MIT License +# +# Copyright (c) 2024 Red Hat, Inc. + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + + +import json +import subprocess +import os +import shutil + +from typing import List +from pathlib import Path + +from auto_merger import utils +from auto_merger.constants import UPSTREAM_REPOS +from auto_merger.utils import setup_logger +from auto_merger.email import EmailSender + + +class PRStatusChecker: + container_name: str = "" + container_dir: Path + current_dir = os.getcwd() + + def __init__(self, github_labels, blocking_labels, approvals: int = 2): + self.logger = setup_logger("AutoMerger") + self.github_labels = list(github_labels) + self.blocking_labels = list(blocking_labels) + self.approvals = approvals + self.logger.debug(f"GitHub Labels: {self.github_labels}") + self.logger.debug(f"GitHub Blocking Labels: {self.blocking_labels}") + self.logger.debug(f"Approvals Labels: {self.approvals}") + self.blocked_pr = {} + self.pr_to_merge = {} + self.blocked_body = [] + self.approval_body = [] + self.repo_data: List = [] + + def is_correct_repo(self) -> bool: + cmd = ["gh repo view --json name"] + repo_name = PRStatusChecker.get_gh_json_output(cmd=cmd) + self.logger.debug(repo_name) + if repo_name["name"] == self.container_name: + return True + return False + + @staticmethod + def get_gh_json_output(cmd): + gh_repo_list = utils.run_command(cmd=cmd, return_output=True) + return json.loads(gh_repo_list) + + def get_gh_pr_list(self): + cmd = ["gh pr list -s open --json number,title,labels,reviews,isDraft"] + repo_data_output = PRStatusChecker.get_gh_json_output(cmd=cmd) + for pr in repo_data_output: + if PRStatusChecker.is_draft(pr): + continue + if self.is_changes_requested(pr): + continue + self.repo_data.append(pr) + + def is_authenticated(self): + token = os.getenv("GH_TOKEN") + if token == "": + self.logger.error(f"Environment variable GH_TOKEN is not specified.") + return False + cmd = [f"gh status"] + self.logger.debug(f"Authentication command: {cmd}") + try: + return_output = utils.run_command(cmd=cmd, return_output=True) + except subprocess.CalledProcessError as cpe: + self.logger.error(f"Authentication to GitHub failed. {cpe}") + return False + return True + + def add_blocked_pr(self, pr: {}): + present = False + for stored_pr in self.blocked_pr[self.container_name]: + if int(stored_pr["number"]) == int(pr["number"]): + present = True + if present: + return + self.blocked_pr[self.container_name].append({ + "number": pr["number"], + "pr_dict": { + "title": pr["title"], + "labels": pr["labels"] + } + }) + self.logger.debug(f"PR {pr['number']} added to blocked") + + def add_approved_pr(self, pr: {}): + self.pr_to_merge[self.container_name].append({ + "number": pr["number"], + "pr_dict": { + "title": pr["title"], + "labels": pr["labels"] + } + }) + self.logger.debug(f"PR {pr['number']} added to approved") + + def check_blocked_labels(self): + for pr in self.repo_data: + self.logger.debug(f"Check blocked: {pr}") + if "labels" not in pr: + continue + for label in pr["labels"]: + if label["name"] not in self.blocking_labels: + continue + self.logger.debug(f"Add '{pr['number']}' to blocked PRs.") + self.add_blocked_pr(pr) + + def check_labels_to_merge(self, pr): + if "labels" not in pr: + return True + for label in pr["labels"]: + if label["name"] in self.blocking_labels: + return False + self.logger.debug(f"Add '{pr['number']}' to approved PRs.") + return True + + def check_pr_approvals(self, reviews_to_check) -> int: + self.logger.debug(f"Approvals to check: {reviews_to_check}") + if not reviews_to_check: + return False + approval_cnt = 0 + for review in reviews_to_check: + if review["state"] == "APPROVED": + approval_cnt += 1 + if approval_cnt < 2: + self.logger.debug(f"Approval count: {approval_cnt}") + return approval_cnt + + def is_changes_requested(self, pr): + if "labels" not in pr: + return False + for labels in pr["labels"]: + if "pr/changes-requested" == labels["name"]: + return True + return False + + @staticmethod + def is_draft(pull_request): + if "isDraft" in pull_request: + if pull_request["isDraft"] in ["True", "true"]: + return True + return False + + def check_pr_to_merge(self) -> bool: + if len(self.repo_data) == 0: + return False + for pr in self.repo_data: + if PRStatusChecker.is_draft(pr): + continue + self.logger.debug(f"PR status: {pr}") + if not self.check_labels_to_merge(pr): + continue + if "reviews" not in pr: + continue + approval_count = self.check_pr_approvals(pr["reviews"]) + self.pr_to_merge[self.container_name] = { + "number": pr["number"], + "approvals": approval_count, + "pr_dict": { + "title": pr["title"] + } + } + + def clone_repo(self): + temp_dir = utils.temporary_dir() + utils.run_command( + f"gh repo clone https://github.com/sclorg/{self.container_name} {temp_dir}/{self.container_name}" + ) + self.container_dir = Path(temp_dir) / f"{self.container_name}" + if self.container_dir.exists(): + os.chdir(self.container_dir) + + def merge_pull_requests(self): + for pr in self.pr_to_merge: + self.logger.debug(f"PR to merge {pr} in repo {self.container_name}.") + + def clean_dirs(self): + os.chdir(self.current_dir) + if self.container_dir.exists(): + shutil.rmtree(self.container_dir) + + def check_all_containers(self) -> int: + if not self.is_authenticated(): + return 1 + for container in UPSTREAM_REPOS: + self.container_name = container + self.repo_data = [] + self.clone_repo() + if not self.is_correct_repo(): + self.logger.error(f"This is not correct repo {self.container_name}.") + self.clean_dirs() + continue + if self.container_name not in self.blocked_pr: + self.blocked_pr[self.container_name] = [] + if self.container_name not in self.pr_to_merge: + self.pr_to_merge[self.container_name] = [] + try: + self.get_gh_pr_list() + self.check_blocked_labels() + if len(self.blocked_pr[self.container_name]) != 0: + self.logger.info( + f"This pull request can not be merged {self.pr_to_merge}" + ) + self.check_pr_to_merge() + except subprocess.CalledProcessError: + self.clean_dirs() + self.logger.error(f"Something went wrong {self.container_name}.") + continue + self.clean_dirs() + return 0 + + def get_blocked_labels(self, pr_dict) -> List [str]: + labels = [] + for lbl in pr_dict["labels"]: + labels.append(lbl["name"]) + return labels + + def print_blocked_pull_request(self): + # Do not print anything in case we do not have PR. + if not [x for x in self.blocked_pr if self.blocked_pr[x]]: + return 0 + self.blocked_body.append( + f"Pull requests that are blocked by labels <b>[{', '.join(self.blocking_labels)}]</b><br><br>" + ) + + for container, pull_requests in self.blocked_pr.items(): + if not pull_requests: + continue + self.blocked_body.append(f"<b>{container}<b>:") + self.blocked_body.append("<table><tr><th>Pull request URL</th><th>Title</th><th>Missing labels</th></tr>") + for pr in pull_requests: + blocked_labels = self.get_blocked_labels(pr["pr_dict"]) + self.blocked_body.append( + f"<tr><td>https://github.com/sclorg/{container}/pull/{pr['number']}</td>" + f"<td>{pr['pr_dict']['title']}</td><td><p style='color:red;'>{' '.join(blocked_labels)}</p></td></tr>" + ) + self.blocked_body.append("</table><br><br>") + print('\n'.join(self.blocked_body)) + + def print_approval_pull_request(self): + # Do not print anything in case we do not have PR. + if not [x for x in self.pr_to_merge if self.pr_to_merge[x]]: + return 0 + self.approval_body.append(f"Pull requests that can be merged or missing {self.approvals} approvals") + self.approval_body.append("<table><tr><th>Pull request URL</th><th>Title</th><th>Approval status</th></tr>") + for container, pr in self.pr_to_merge.items(): + if not pr: + continue + if int(pr["approvals"]) >= self.approvals: + result_pr = f"CAN BE MERGED" + else: + result_pr = f"Missing {self.approvals-int(pr['approvals'])} APPROVAL" + self.approval_body.append( + f"<tr><td>https://github.com/sclorg/{container}/pull/{pr['number']}</td>" + f"<td>{pr['pr_dict']['title']}</td><td><p style='color:red;'>{result_pr}</p></td></tr>" + ) + self.approval_body.append("</table><br>") + print('\n'.join(self.approval_body)) + + def send_results(self, recipients): + self.logger.debug(f"Recepients are: {recipients}") + if not recipients: + return 1 + sender_class = EmailSender(recipient_email=list(recipients)) + subject_msg = "Pull request statuses for organization https://gibhub.com/sclorg" + sender_class.send_email(subject_msg, self.blocked_body + self.approval_body) + + +def run(): + auto_merger = PRStatusChecker() + auto_merger.check_all_containers() diff --git a/tests/test_correct_repo.py b/tests/test_correct_repo.py index 1c7f411..9b5c630 100644 --- a/tests/test_correct_repo.py +++ b/tests/test_correct_repo.py @@ -2,20 +2,20 @@ from flexmock import flexmock -from auto_merger.merger import AutoMerger +from auto_merger.pr_checker import PRStatusChecker def test_get_gh_pr_correct_repo(get_repo_name): - flexmock(AutoMerger).should_receive("get_gh_json_output").and_return(get_repo_name) - auto_merger = AutoMerger(github_labels=[], blocking_labels=["pr/missing-review"]) + flexmock(PRStatusChecker).should_receive("get_gh_json_output").and_return(get_repo_name) + auto_merger = PRStatusChecker(github_labels=[], blocking_labels=["pr/missing-review"]) auto_merger.container_name = "s2i-nodejs-container" assert auto_merger.is_correct_repo() def test_get_gh_pr_wrong_repo(get_repo_wrong_name): - flexmock(AutoMerger).should_receive("get_gh_json_output").and_return( + flexmock(PRStatusChecker).should_receive("get_gh_json_output").and_return( get_repo_wrong_name ) - auto_merger = AutoMerger(github_labels=[], blocking_labels=["pr/missing-review"]) + auto_merger = PRStatusChecker(github_labels=[], blocking_labels=["pr/missing-review"]) auto_merger.container_name = "s2i-nodejs-container" assert not auto_merger.is_correct_repo() diff --git a/tests/test_pr_status.py b/tests/test_pr_status.py index 4b50617..53ca9c8 100644 --- a/tests/test_pr_status.py +++ b/tests/test_pr_status.py @@ -2,24 +2,24 @@ from flexmock import flexmock -from auto_merger.merger import AutoMerger +from auto_merger.pr_checker import PRStatusChecker def test_get_gh_pr_list(get_pr_missing_ci): - flexmock(AutoMerger).should_receive("get_gh_json_output").and_return( + flexmock(PRStatusChecker).should_receive("get_gh_json_output").and_return( get_pr_missing_ci ) - auto_merger = AutoMerger(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) + auto_merger = PRStatusChecker(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) auto_merger.container_name = "s2i-nodejs-container" auto_merger.get_gh_pr_list() assert auto_merger.repo_data def test_get_gh_two_pr_labels_missing(get_two_pr_missing_labels): - flexmock(AutoMerger).should_receive("get_gh_json_output").and_return( + flexmock(PRStatusChecker).should_receive("get_gh_json_output").and_return( get_two_pr_missing_labels ) - auto_merger = AutoMerger(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) + auto_merger = PRStatusChecker(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) auto_merger.container_name = "s2i-nodejs-container" auto_merger.get_gh_pr_list() assert auto_merger.repo_data @@ -27,10 +27,10 @@ def test_get_gh_two_pr_labels_missing(get_two_pr_missing_labels): def test_get_gh_pr_missing_ci(get_pr_missing_ci): - flexmock(AutoMerger).should_receive("get_gh_json_output").and_return( + flexmock(PRStatusChecker).should_receive("get_gh_json_output").and_return( get_pr_missing_ci ) - auto_merger = AutoMerger(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) + auto_merger = PRStatusChecker(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) auto_merger.container_name = "s2i-nodejs-container" auto_merger.get_gh_pr_list() assert auto_merger.repo_data @@ -38,8 +38,8 @@ def test_get_gh_pr_missing_ci(get_pr_missing_ci): def test_get_no_pr_for_merge(): - flexmock(AutoMerger).should_receive("get_gh_json_output").and_return([]) - auto_merger = AutoMerger(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) + flexmock(PRStatusChecker).should_receive("get_gh_json_output").and_return([]) + auto_merger = PRStatusChecker(github_labels=["READY-to-MERGE"], blocking_labels=["pr/missing-review", 'pr/failing-ci']) auto_merger.container_name = "s2i-nodejs-container" auto_merger.get_gh_pr_list() assert not auto_merger.repo_data diff --git a/tox.ini b/tox.ini index efeb282..16622a7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = True -envlist = py36,py37,py38,py39,py310,py311,py312 +envlist = py38,py39,py310,py311,py312 [testenv] commands = python3 -m pytest --color=yes -vv --verbose --showlocals