Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github-webhook-server.config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
minimum-lgtm: 1
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -112,7 +127,7 @@ repositories:

```yaml
slack_webhook_url: https://hooks.slack.com/services/<channel>
```
````

if `pypi` configured for the repository a new version will be pushed to pypi on new GitHub release

Expand Down
54 changes: 44 additions & 10 deletions webhook_server_container/libs/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}
2 changes: 1 addition & 1 deletion webhook_server_container/libs/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions webhook_server_container/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions webhook_server_container/tests/manifests/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 13 additions & 6 deletions webhook_server_container/tests/test_branch_protection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
44 changes: 30 additions & 14 deletions webhook_server_container/tests/test_pull_request_owners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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=[
Expand All @@ -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=[
Expand All @@ -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=[
Expand Down Expand Up @@ -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=[
Expand All @@ -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=[
Expand All @@ -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=[
Expand Down Expand Up @@ -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=[
Expand All @@ -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=[
Expand Down Expand Up @@ -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=[
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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=[
Expand Down Expand Up @@ -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=[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions webhook_server_container/utils/github_repository_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment thread
myakove marked this conversation as resolved.
if docker:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion webhook_server_container/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
4 changes: 2 additions & 2 deletions webhook_server_container/utils/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down