From e1efce10b0e7c38b5e6e7d3bda11a97395aa4064 Mon Sep 17 00:00:00 2001 From: "Petr \"Stone\" Hracek" Date: Mon, 7 Oct 2024 14:23:04 +0200 Subject: [PATCH] First initial sources Signed-off-by: Petr "Stone" Hracek --- .gitignore | 2 + LICENSE.txt | 21 +++++ README.md | 0 auto-merger | 33 ++++++++ auto_merger/__init__.py | 0 auto_merger/constants.py | 40 ++++++++++ auto_merger/merger.py | 162 +++++++++++++++++++++++++++++++++++++++ auto_merger/utils.py | 101 ++++++++++++++++++++++++ pytest.ini | 3 + requirements.txt | 3 + setup.py | 68 ++++++++++++++++ tox.ini | 7 +- 12 files changed, 435 insertions(+), 5 deletions(-) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100755 auto-merger create mode 100644 auto_merger/__init__.py create mode 100644 auto_merger/constants.py create mode 100644 auto_merger/merger.py create mode 100644 auto_merger/utils.py create mode 100644 pytest.ini create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 82f9275..6a1048a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ __pycache__/ *.py[cod] *$py.class +.idea + # C extensions *.so diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..462d1c5 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/auto-merger b/auto-merger new file mode 100755 index 0000000..90fd6cd --- /dev/null +++ b/auto-merger @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2016-2018 CWT Authors +# +# 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. +# +# Authors: Petr Hracek + +from auto_merger.merger import AutoMerger + + +if __name__ == "__main__": + auto_merger = AutoMerger() + auto_merger.check_all_containers() diff --git a/auto_merger/__init__.py b/auto_merger/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/auto_merger/constants.py b/auto_merger/constants.py new file mode 100644 index 0000000..1e2ed40 --- /dev/null +++ b/auto_merger/constants.py @@ -0,0 +1,40 @@ +#!/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. + +UPSTREAM_REPOS = [ + "s2i-base-container", + "s2i-perl-container", + "s2i-nodejs-container", + "s2i-php-container", + "s2i-ruby-container", + "s2i-python-container", + "nginx-container", + "mysql-container", + "postgresql-container", + "mariadb-container", + "redis-container", + "valkey-container", + "varnish-container", + "httpd-container", +] diff --git a/auto_merger/merger.py b/auto_merger/merger.py new file mode 100644 index 0000000..9d18d2f --- /dev/null +++ b/auto_merger/merger.py @@ -0,0 +1,162 @@ +#!/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 + + +class AutoMerger: + repo_data: List = [] + pr_to_merge: List[int] = [] + container_name: str = "" + container_dir: Path + current_dir = os.getcwd() + + def __init__(self): + self.logger = setup_logger("AutoMerger") + + def is_correct_repo(self) -> bool: + cmd = ["gh repo view --json name"] + repo_name = AutoMerger.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"] + self.repo_data = AutoMerger.get_gh_json_output(cmd=cmd) + + def check_pr_labels(self, labels_to_check) -> bool: + self.logger.debug(f"Labels to check: {labels_to_check}") + if not labels_to_check: + return False + pr_failed_tags = ["pr/missing_review", "pr/failing-ci"] + pr_present = ["READY-to-MERGE"] + failed_pr = True + for label in labels_to_check: + if label["name"] in pr_failed_tags: + failed_pr = False + if label["name"] not in pr_present: + failed_pr = False + return failed_pr + + def check_pr_approvals(self, reviews_to_check) -> bool: + self.logger.debug(f"Approvals to check: {reviews_to_check}") + if not reviews_to_check: + return False + approval = "APPROVED" + approval_cnt = 0 + for review in reviews_to_check: + if review["state"] == approval: + approval_cnt += 1 + if approval_cnt < 2: + self.logger.debug(f"Approval count: {approval_cnt}") + return False + return True + + def check_pr_to_merge(self) -> bool: + if len(self.repo_data) == 0: + return False + pr_to_merge = [] + for pr in self.repo_data: + self.logger.debug(f"PR status: {pr}") + if "labels" not in pr: + continue + if not self.check_pr_labels(pr["labels"]): + self.logger.info( + f"PR {pr['number']} does not have valid flag to merging in repo {self.container_name}." + ) + continue + if not self.check_pr_approvals(pr["reviews"]): + self.logger.info( + f"PR {pr['number']} does not have enought APPROVALS to merging in repo {self.container_name}." + ) + continue + pr_to_merge.append(pr["number"]) + self.logger.debug(f"PR to merge {pr_to_merge}") + if not pr_to_merge: + return False + self.pr_to_merge = pr_to_merge + return True + + 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): + for container in UPSTREAM_REPOS: + self.pr_to_merge = [] + self.container_name = container + 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 + try: + self.get_gh_pr_list() + if self.check_pr_to_merge(): + self.logger.info( + f"This pull request can be merged {self.pr_to_merge}" + ) + # auto_merger.merge_pull_requests() + except subprocess.CalledProcessError: + self.clean_dirs() + self.logger.error(f"Something went wrong {self.container_name}.") + continue + self.clean_dirs() + + +def run(): + auto_merger = AutoMerger() + auto_merger.check_all_containers() diff --git a/auto_merger/utils.py b/auto_merger/utils.py new file mode 100644 index 0000000..c86247f --- /dev/null +++ b/auto_merger/utils.py @@ -0,0 +1,101 @@ +# 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 subprocess +import logging +import tempfile +import sys + +logger = logging.getLogger(__name__) + + +def run_command( + cmd, + return_output: bool = True, + ignore_error: bool = False, + shell: bool = True, + debug: bool = False, + **kwargs, +): + """ + Run provided command on host system using the same user as invoked this code. + Raises subprocess.CalledProcessError if it fails. + :param cmd: list or str + :param return_output: bool, return output of the command + :param ignore_error: bool, do not fail in case nonzero return code + :param shell: bool, run command in shell + :param debug: bool, print command in shell, default is suppressed + :return: None or str + """ + logger.debug(f"command: {cmd}") + try: + if return_output: + return subprocess.check_output( + cmd, + stderr=subprocess.STDOUT, + universal_newlines=True, + shell=shell, + **kwargs, + ) + else: + return subprocess.check_call(cmd, shell=shell, **kwargs) + except subprocess.CalledProcessError as cpe: + if ignore_error: + if return_output: + return cpe.output + else: + return cpe.returncode + else: + logger.error(f"failed with code {cpe.returncode} and output:\n{cpe.output}") + raise cpe + + +def temporary_dir(prefix: str = "automerger") -> str: + temp_file = tempfile.TemporaryDirectory(prefix=prefix) + logger.debug(f"AutoMerger: Temporary dir name: {temp_file.name}") + return temp_file.name + + +def setup_logger(logger_id, level=logging.DEBUG): + logger = logging.getLogger(logger_id) + logger.setLevel(level) + format_str = "%(name)s - %(levelname)s: %(message)s" + # Debug handler + debug = logging.StreamHandler(sys.stdout) + formatter = logging.Formatter(format_str) + debug.setLevel(logging.DEBUG) + debug.addFilter(lambda r: True if r.levelno == logging.DEBUG else False) + debug.setFormatter(formatter) + logger.addHandler(debug) + # Info handler + info = logging.StreamHandler(sys.stdout) + info.setLevel(logging.DEBUG) + info.addFilter(lambda r: True if r.levelno == logging.INFO else False) + logger.addHandler(info) + # Warning, error, critical handler + stderr = logging.StreamHandler(sys.stderr) + formatter = logging.Formatter(format_str) + stderr.setLevel(logging.WARN) + stderr.addFilter(lambda r: True if r.levelno >= logging.WARN else False) + stderr.setFormatter(formatter) + logger.addHandler(stderr) + return logger diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..c5aa348 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +pythonpath = auto_merger +testpaths = tests diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..b4bbc28 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pytest +PyYAML +flexmock diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..84c984e --- /dev/null +++ b/setup.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (c) 2016-2018 CWT Authors +# +# 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. +# +# Authors: Petr Hracek + +from setuptools import setup, find_packages +from pathlib import Path + +this_directory = Path(__file__).parent +long_description = ( + "Tool for merging sclorg pull request that meets criteria" +) # (this_directory / "README.md").read_text() + + +def get_requirements(): + """Parse all packages mentioned in the 'requirements.txt' file.""" + with open(Path(this_directory) / "requirements.txt") as file_stream: + return file_stream.read().splitlines() + + +setup( + name="auto-merger", + description="Tool for merging sclorg pull request that meets criteria.", + long_description=long_description, + long_description_content_type="text/markdown", + version="0.1.0", + keywords="tool,containers,images,tests", + packages=find_packages(exclude=["tests"]), + url="https://github.com/sclorg/auto-merger", + license="MIT", + author="Petr Hracek", + author_email="phracek@redhat.com", + install_requires=get_requirements(), + scripts=[], + entry_points={"console_scripts": ["auto-merger = auto_merger.merger:run"]}, + setup_requires=[], + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Topic :: Software Development", + ], +) diff --git a/tox.ini b/tox.ini index e3a3b5b..e524bc0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,6 @@ -[tox] -skipsdist = True -envlist = py36,py37,py38,py39,py310,py311,py312 - [testenv] -commands = python3 -m pytest --color=yes -v --showlocals tests/ +commands = python3 -m pytest --color=yes -vv --verbose --showlocals deps = pytest + PyYAML flexmock