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
15 changes: 15 additions & 0 deletions example.config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ jira:
user-mapping:
<GITHUB USER>: <JIRA USER> # if github user is not the same as jira

branch_protection:
strict: True
require_code_owner_reviews: True
dismiss_stale_reviews: False
required_approving_review_count: 1
required_linear_history: True
Comment thread
dbasunag marked this conversation as resolved.
required_conversation_resolution: True

repositories:
my-repository:
name: my-org/my-repository
Expand Down Expand Up @@ -94,3 +102,10 @@ repositories:
<GITHUB USER>: <JIRA USER> # if github user is not the same as jira

conventional-title: "ci,docs,feat,fix,refactor,test,release" # Check PR title start with any of these words + :
branch_protection:
strict: True
require_code_owner_reviews: True
dismiss_stale_reviews: False
required_approving_review_count: 1
required_linear_history: True
required_conversation_resolution: True
30 changes: 30 additions & 0 deletions webhook_server_container/config/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,21 @@ properties:
token:
type: string
format: password
branch_protection:
type: object
properties:
strict:
type: boolean
require_code_owner_reviews:
type: boolean
dismiss_stale_reviews:
type: boolean
required_approving_review_count:
type: integer
required_linear_history:
type: boolean
required_conversation_resolution:
type: boolean
repositories:
type: object
properties:
Expand Down Expand Up @@ -141,6 +156,21 @@ properties:
type: array
items:
type: string
branch_protection:
type: object
properties:
strict:
type: boolean
require_code_owner_reviews:
type: boolean
dismiss_stale_reviews:
type: boolean
required_approving_review_count:
type: integer
required_linear_history:
type: boolean
required_conversation_resolution:
type: boolean
can-be-merged-required-labels:
type: array
items:
Expand Down
95 changes: 95 additions & 0 deletions webhook_server_container/tests/test_branch_protection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import os
import pytest
from webhook_server_container.libs.config import Config
from webhook_server_container.utils.github_repository_settings import (
get_repo_branch_protection_rules,
DEFAULT_BRANCH_PROTECTION,
)


@pytest.fixture()
def branch_protection_rules(request, mocker):
os.environ["WEBHOOK_SERVER_DATA_DIR"] = "webhook_server_container/tests/manifests"
config = Config()
repo_name = "test-repo"
data = config.data
data.setdefault("branch_protection", request.param.get("global", {}))
data["repositories"][repo_name].setdefault("branch_protection", request.param.get("repo", {}))
mocker.patch(
"webhook_server_container.libs.config.Config.data", new_callable=mocker.PropertyMock, return_value=data
)
return get_repo_branch_protection_rules(config_data=config.data, repo_data=config.data["repositories"][repo_name])[
"branch_protection"
]


@pytest.mark.parametrize(
"branch_protection_rules, expected",
[
pytest.param(
{
"global": {
"strict": True,
},
"repo": {
"strict": False,
},
},
{
"strict": False,
},
id="test_repo_branch_protection_rule",
),
pytest.param(
{
"global": {
"strict": False,
},
},
{
"strict": False,
},
id="test_global_branch_protection_rule",
),
pytest.param(
{
"global": {
"strict": False,
"require_code_owner_reviews": True,
"dismiss_stale_reviews": False,
"required_approving_review_count": 2,
"required_linear_history": False,
},
"repo": {
"strict": True,
"require_code_owner_reviews": True,
"dismiss_stale_reviews": False,
"required_approving_review_count": 1,
"required_linear_history": True,
},
},
{
"strict": True,
"require_code_owner_reviews": True,
"dismiss_stale_reviews": False,
"required_approving_review_count": 1,
"required_linear_history": True,
},
id="test_repo_multiple_branch_protection_rule",
),
pytest.param(
{},
{
**DEFAULT_BRANCH_PROTECTION,
},
id="test_default_branch_protection_rule",
),
],
indirect=["branch_protection_rules"],
)
def test_branch_protection_setup(branch_protection_rules, expected):
mismatch = {}
for key in expected:
if branch_protection_rules[key] != expected[key]:
mismatch[key] = f"Expected value for {key}: {expected[key]}, actual: {branch_protection_rules[key]}"
assert not mismatch, f"Following mismatches are found: {mismatch}"
44 changes: 37 additions & 7 deletions webhook_server_container/utils/github_repository_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,18 @@
get_api_with_highest_rate_limit,
get_future_results,
get_logger_with_params,
get_value_from_dicts,
)

DEFAULT_BRANCH_PROTECTION = {
"strict": True,
"require_code_owner_reviews": False,
"dismiss_stale_reviews": True,
"required_approving_review_count": 0,
"required_linear_history": True,
"required_conversation_resolution": True,
}


def _get_github_repo_api(github_api: github.Github, repository: int | str) -> Repository | None:
logger = get_logger_with_params(name="github-repository-settings")
Expand All @@ -49,19 +59,25 @@ def set_branch_protection(
repository: Repository,
required_status_checks: List[str],
github_api: Github,
strict: bool,
require_code_owner_reviews: bool,
dismiss_stale_reviews: bool,
required_approving_review_count: int,
required_linear_history: bool,
required_conversation_resolution: bool,
) -> bool:
logger = get_logger_with_params(name="github-repository-settings")

api_user = github_api.get_user().login
logger.info(f"Set branch {branch} setting for {repository.name}. enabled checks: {required_status_checks}")
branch.edit_protection(
strict=True,
required_conversation_resolution=True,
strict=strict,
required_conversation_resolution=required_conversation_resolution,
contexts=required_status_checks,
require_code_owner_reviews=False,
dismiss_stale_reviews=True,
required_approving_review_count=0,
required_linear_history=True,
require_code_owner_reviews=require_code_owner_reviews,
dismiss_stale_reviews=dismiss_stale_reviews,
required_approving_review_count=required_approving_review_count,
required_linear_history=required_linear_history,
users_bypass_pull_request_allowances=[api_user],
teams_bypass_pull_request_allowances=[api_user],
apps_bypass_pull_request_allowances=[api_user],
Expand Down Expand Up @@ -168,6 +184,18 @@ def set_repository_labels(repository: Repository) -> str:
return f"{repository}: Setting repository labels is done"


def get_repo_branch_protection_rules(config_data: dict[str, Any], repo_data: dict[str, Any]) -> dict[str, Any]:
for rule_name in DEFAULT_BRANCH_PROTECTION:
repo_data.setdefault("branch_protection", {})
repo_data["branch_protection"][rule_name] = get_value_from_dicts(
primary_dict=repo_data["branch_protection"],
secondary_dict=config_data.get("branch_protection", {}),
key=rule_name,
return_on_none=DEFAULT_BRANCH_PROTECTION[rule_name],
)
return repo_data


def set_repositories_settings(config_: Config, github_api: Github) -> None:
logger = get_logger_with_params(name="github-repository-settings")

Expand All @@ -186,6 +214,7 @@ def set_repositories_settings(config_: Config, github_api: Github) -> None:
futures = []
with ThreadPoolExecutor() as executor:
for _, data in config_data["repositories"].items():
data = get_repo_branch_protection_rules(config_data=config_data, repo_data=data)
Comment thread
myakove marked this conversation as resolved.
futures.append(
executor.submit(
set_repository,
Expand All @@ -208,6 +237,7 @@ def set_repository(
repository: str = data["name"]
logger.info(f"Processing repository {repository}")
protected_branches: Dict[str, Any] = data.get("protected-branches", {})
repo_branch_protection_rules: Dict[str, Any] = data["branch_protection"]
repo = _get_github_repo_api(github_api=github_api, repository=repository)
if not repo:
return False, f"{repository}: Failed to get repository", logger.error
Expand Down Expand Up @@ -241,7 +271,6 @@ def set_repository(
default_status_checks=_default_status_checks,
exclude_status_checks=exclude_status_checks,
)

futures.append(
executor.submit(
set_branch_protection,
Expand All @@ -251,6 +280,7 @@ def set_repository(
"required_status_checks": required_status_checks,
"github_api": github_api,
},
**repo_branch_protection_rules,
)
)

Expand Down