diff --git a/.github-webhook-server.config b/.github-webhook-server.config new file mode 100644 index 00000000..aa597f18 --- /dev/null +++ b/.github-webhook-server.config @@ -0,0 +1 @@ +minimum-lgtm: 1 diff --git a/README.md b/README.md index 878ae675..f6576ef1 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,21 @@ repositories: main: [] ``` +Repository config can be override by config file in the root on the repository named `.github-webhook-server.yaml` + +```yaml +minimum-lgtm: 1 +can-be-merged-required-labels: + - "my-label" +tox: + main: all +set-auto-merge-prs: + - dev +pre-commit: true +``` + +```` + - `github-app-id`: The ID of the GitHub app. Need to add the APP to the repository. - `name`: repository full name (org or user/repository name) - `webhook_ip`: Ip or FQDN where this app will run, this will be added as webhook in the repository setting @@ -112,7 +127,7 @@ repositories: ```yaml slack_webhook_url: https://hooks.slack.com/services/ -``` +```` if `pypi` configured for the repository a new version will be pushed to pypi on new GitHub release diff --git a/webhook_server_container/libs/config.py b/webhook_server_container/libs/config.py index df91988d..f6804b47 100644 --- a/webhook_server_container/libs/config.py +++ b/webhook_server_container/libs/config.py @@ -2,35 +2,69 @@ from typing import Any import yaml +from simple_logger.logger import get_logger + +LOGGER = get_logger(name="config") class Config: - def __init__(self, repository: str | None = None) -> None: + def __init__(self, repository: str | None = None, repository_full_name: str | None = None) -> None: self.data_dir: str = os.environ.get("WEBHOOK_SERVER_DATA_DIR", "/home/podman/data") self.config_path: str = os.path.join(self.data_dir, "config.yaml") self.exists() self.repository = repository + self.repository_full_name = repository_full_name def exists(self) -> None: if not os.path.isfile(self.config_path): raise FileNotFoundError(f"Config file {self.config_path} not found") @property - def data(self) -> dict[str, Any]: - with open(self.config_path) as fd: - return yaml.safe_load(fd) + def root_data(self) -> dict[str, Any]: + try: + with open(self.config_path) as fd: + return yaml.safe_load(fd) + except Exception: + LOGGER.error("Config file is empty") + return {} @property def repository_data(self) -> dict[str, Any]: - return self.data["repositories"].get(self.repository, {}) + return self.root_data.get("repositories", {}).get(self.repository, {}) def get_value(self, value: str, return_on_none: Any = None) -> Any: """ - Get value from config, try first from repository and if not exists get it from root config + Get value from config + + Order of getting value: + 1. Local repository file (.github-webhook-server.yaml) + 2. Repository level global config file (config.yaml) + 3. Root level global config file (config.yaml) """ - _val = self.repository_data.get(value) - if _val is None: - _val = self.data.get(value) + for scope in (self.repository_local_data, self.repository_data, self.root_data): + if value in scope: + value_data = scope[value] + if value_data is not None: + return value_data + + return return_on_none + + @property + def repository_local_data(self) -> dict[str, Any]: + if self.repository and self.repository_full_name: + from webhook_server_container.utils.helpers import get_api_with_highest_rate_limit, get_github_repo_api + + github_api, _, _ = get_api_with_highest_rate_limit(config=self, repository_name=self.repository) + + if github_api: + try: + repo = get_github_repo_api(github_api=github_api, repository=self.repository_full_name) + _path = repo.get_contents(",github-webhook-server.yaml") + config_file = _path[0] if isinstance(_path, list) else _path + yaml.safe_load(config_file.decoded_content) + + except Exception: + return {} - return _val or return_on_none + return {} diff --git a/webhook_server_container/libs/github_api.py b/webhook_server_container/libs/github_api.py index 180decdc..63f83146 100644 --- a/webhook_server_container/libs/github_api.py +++ b/webhook_server_container/libs/github_api.py @@ -2109,7 +2109,7 @@ def _check_if_pr_approved(self, labels: list[str]) -> str: if self.minimum_lgtm: for _label in labels: reviewer = _label.split(LABELS_SEPARATOR)[-1] - if LGTM_BY_LABEL_PREFIX.lower() in _label.lower() and reviewer in all_reviewers: + if LGTM_BY_LABEL_PREFIX.lower() in _label.lower() and reviewer in all_reviewers_without_pr_owner: lgtm_count += 1 for _label in labels: diff --git a/webhook_server_container/tests/conftest.py b/webhook_server_container/tests/conftest.py index a7a560e3..278fd1e0 100644 --- a/webhook_server_container/tests/conftest.py +++ b/webhook_server_container/tests/conftest.py @@ -112,6 +112,7 @@ def process_github_webhook(mocker, request): process_github_webhook.pull_request_branch = "main" if hasattr(request, "param") and request.param: process_github_webhook.changed_files = request.param[0] + else: process_github_webhook.changed_files = ALL_CHANGED_FILES diff --git a/webhook_server_container/tests/manifests/config.yaml b/webhook_server_container/tests/manifests/config.yaml index b300b259..8f9295e3 100644 --- a/webhook_server_container/tests/manifests/config.yaml +++ b/webhook_server_container/tests/manifests/config.yaml @@ -74,3 +74,5 @@ repositories: can-be-merged-required-labels: # check for extra labels to set PR as can be merged - my-label1 - my-label2 + + minimum-lgtm: 0 diff --git a/webhook_server_container/tests/test_branch_protection.py b/webhook_server_container/tests/test_branch_protection.py index 029b191d..ce3ecc40 100644 --- a/webhook_server_container/tests/test_branch_protection.py +++ b/webhook_server_container/tests/test_branch_protection.py @@ -10,19 +10,26 @@ @pytest.fixture() def branch_protection_rules(request, mocker): + config_path = "webhook_server_container.libs.config.Config" os.environ["WEBHOOK_SERVER_DATA_DIR"] = "webhook_server_container/tests/manifests" repo_name = "test-repo" config = Config(repository=repo_name) - data = config.data - data.setdefault("branch_protection", request.param.get("global", {})) - data["repositories"][repo_name].setdefault("branch_protection", request.param.get("repo")) + root_data = config.root_data + root_data.setdefault("branch_protection", request.param.get("global", {})) + root_data["repositories"][repo_name].setdefault("branch_protection", request.param.get("repo")) + + mocker.patch(f"{config_path}.root_data", new_callable=mocker.PropertyMock, return_value=root_data) + mocker.patch( - "webhook_server_container.libs.config.Config.data", new_callable=mocker.PropertyMock, return_value=data + f"{config_path}.repository_data", + new_callable=mocker.PropertyMock, + return_value=root_data["repositories"][repo_name], ) + mocker.patch( - "webhook_server_container.libs.config.Config.repository_data", + f"{config_path}.repository_local_data", new_callable=mocker.PropertyMock, - return_value=data["repositories"][repo_name], + return_value={}, ) return get_repo_branch_protection_rules(config=config) diff --git a/webhook_server_container/tests/test_pull_request_owners.py b/webhook_server_container/tests/test_pull_request_owners.py index fcfe3c6a..af28d430 100644 --- a/webhook_server_container/tests/test_pull_request_owners.py +++ b/webhook_server_container/tests/test_pull_request_owners.py @@ -139,7 +139,7 @@ def test_all_approvers_reviewers(process_github_webhook, all_approvers_and_revie assert all_reviewers == process_github_webhook.all_reviewers -def test_check_pr_approved(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_approved(process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( @@ -157,7 +157,7 @@ def test_check_pr_approved(process_github_webhook, all_approvers_and_reviewers): assert check_if_pr_approved == "" -def test_check_pr_minimum_approved(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_minimum_approved(process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -170,7 +170,7 @@ def test_check_pr_minimum_approved(process_github_webhook, all_approvers_and_rev assert check_if_pr_approved == "" -def test_check_pr_not_approved(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_not_approved(process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -191,7 +191,7 @@ def test_check_pr_not_approved(process_github_webhook, all_approvers_and_reviewe assert missing_approvers == expected_approvers -def test_check_pr_partial_approved(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_partial_approved(process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -226,7 +226,9 @@ def test_check_pr_partial_approved(process_github_webhook, all_approvers_and_rev ], indirect=True, ) -def test_check_pr_approved_specific_folder(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_approved_specific_folder( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -250,7 +252,9 @@ def test_check_pr_approved_specific_folder(process_github_webhook, all_approvers ], indirect=True, ) -def test_check_pr_approved_nested_folder_no_owners(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_approved_nested_folder_no_owners( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -271,7 +275,9 @@ def test_check_pr_approved_nested_folder_no_owners(process_github_webhook, all_a ], indirect=True, ) -def test_check_pr_approved_specific_folder_with_root_approvers(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_approved_specific_folder_with_root_approvers( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -299,7 +305,9 @@ def test_check_pr_approved_specific_folder_with_root_approvers(process_github_we ], indirect=True, ) -def test_check_pr_approved_specific_folder_no_root_approvers(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_approved_specific_folder_no_root_approvers( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -320,7 +328,9 @@ def test_check_pr_approved_specific_folder_no_root_approvers(process_github_webh ], indirect=True, ) -def test_check_pr_not_approved_specific_folder_without_owners(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_not_approved_specific_folder_without_owners( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -348,7 +358,9 @@ def test_check_pr_not_approved_specific_folder_without_owners(process_github_web ], indirect=True, ) -def test_check_pr_approved_specific_folder_without_owners(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_approved_specific_folder_without_owners( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -371,7 +383,7 @@ def test_check_pr_approved_specific_folder_without_owners(process_github_webhook indirect=True, ) def test_check_pr_approved_folder_with_no_owners_and_folder_without_root_approvers( - process_github_webhook, all_approvers_and_reviewers + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers ): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( @@ -396,7 +408,7 @@ def test_check_pr_approved_folder_with_no_owners_and_folder_without_root_approve indirect=True, ) def test_check_pr_not_approved_folder_with_no_owners_and_folder_without_root_approvers( - process_github_webhook, all_approvers_and_reviewers + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers ): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( @@ -425,7 +437,9 @@ def test_check_pr_not_approved_folder_with_no_owners_and_folder_without_root_app ], indirect=True, ) -def test_check_pr_not_approved_subfolder_with_owners(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_not_approved_subfolder_with_owners( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ @@ -453,7 +467,9 @@ def test_check_pr_not_approved_subfolder_with_owners(process_github_webhook, all ], indirect=True, ) -def test_check_pr_approved_subfolder_with_owners_no_root_approvers(process_github_webhook, all_approvers_and_reviewers): +def test_check_pr_approved_subfolder_with_owners_no_root_approvers( + process_github_webhook, all_approvers_and_reviewers, all_approvers_reviewers +): process_github_webhook.all_approvers = process_github_webhook.get_all_approvers() check_if_pr_approved = process_github_webhook._check_if_pr_approved( labels=[ diff --git a/webhook_server_container/tests/test_repo_data_from_config.py b/webhook_server_container/tests/test_repo_data_from_config.py index cef45e5a..5bca191d 100644 --- a/webhook_server_container/tests/test_repo_data_from_config.py +++ b/webhook_server_container/tests/test_repo_data_from_config.py @@ -18,3 +18,4 @@ def test_repo_data_from_config_repository_found(process_github_webhook): assert process_github_webhook.pre_commit assert process_github_webhook.auto_verified_and_merged_users == ["my[bot]"] assert process_github_webhook.can_be_merged_required_labels == ["my-label1", "my-label2"] + assert process_github_webhook.minimum_lgtm == 0 diff --git a/webhook_server_container/utils/github_repository_and_webhook_settings.py b/webhook_server_container/utils/github_repository_and_webhook_settings.py index 4e6d2eae..5f68b0a1 100644 --- a/webhook_server_container/utils/github_repository_and_webhook_settings.py +++ b/webhook_server_container/utils/github_repository_and_webhook_settings.py @@ -26,7 +26,7 @@ def get_repository_api(repository: str) -> tuple[str, github.Github | None, str] apis: list = [] with ThreadPoolExecutor() as executor: - for repo, data in config.data["repositories"].items(): + for repo, data in config.root_data["repositories"].items(): apis.append( executor.submit( get_repository_api, diff --git a/webhook_server_container/utils/github_repository_settings.py b/webhook_server_container/utils/github_repository_settings.py index fdc362c9..fe1742f4 100644 --- a/webhook_server_container/utils/github_repository_settings.py +++ b/webhook_server_container/utils/github_repository_settings.py @@ -200,7 +200,7 @@ def set_repositories_settings(config: Config, apis_dict: dict[str, dict[str, Any logger = get_logger_with_params(name="github-repository-settings") logger.info("Processing repositories") - config_data = config.data + config_data = config.root_data docker: dict[str, str] | None = config_data.get("docker") if docker: @@ -330,7 +330,7 @@ def set_all_in_progress_check_runs_to_queued(repo_config: Config, apis_dict: dic futures: list["Future"] = [] with ThreadPoolExecutor() as executor: - for repo, data in repo_config.data["repositories"].items(): + for repo, data in repo_config.root_data["repositories"].items(): repo_config = Config(repository=repo) futures.append( executor.submit( @@ -393,7 +393,7 @@ def get_repository_github_app_api(config_: Config, repository_name: str) -> Gith with open(os.path.join(config_.data_dir, "webhook-server.private-key.pem")) as fd: private_key = fd.read() - github_app_id: int = config_.data["github-app-id"] + github_app_id: int = config_.root_data["github-app-id"] auth: AppAuth = Auth.AppAuth(app_id=github_app_id, private_key=private_key) app_instance: GithubIntegration = GithubIntegration(auth=auth) owner: str diff --git a/webhook_server_container/utils/helpers.py b/webhook_server_container/utils/helpers.py index b8d5f015..235520af 100644 --- a/webhook_server_container/utils/helpers.py +++ b/webhook_server_container/utils/helpers.py @@ -140,7 +140,7 @@ def get_api_with_highest_rate_limit( repository_name (str, optional): Repository name, if provided try to get token set in config repository section. Returns: - tuple: API, token + tuple: API, token, api_user """ logger = get_logger_with_params(name="helpers") diff --git a/webhook_server_container/utils/webhook.py b/webhook_server_container/utils/webhook.py index 1c1e31c4..a597e578 100644 --- a/webhook_server_container/utils/webhook.py +++ b/webhook_server_container/utils/webhook.py @@ -56,11 +56,11 @@ def process_github_webhook( def create_webhook(config: Config, apis_dict: dict[str, dict[str, Any]]) -> None: LOGGER.info("Preparing webhook configuration") - webhook_ip = config.data["webhook_ip"] + webhook_ip = config.root_data["webhook_ip"] futures = [] with ThreadPoolExecutor() as executor: - for repo, data in config.data["repositories"].items(): + for repo, data in config.root_data["repositories"].items(): futures.append( executor.submit( process_github_webhook,