Skip to content

[feat] Check if auto PR reviews are enabled for a given owner #1285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 23, 2025
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 webhook_handlers/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class GitHubHTTPHeaders:
DELIVERY_TOKEN = "HTTP_X_GITHUB_DELIVERY"
SIGNATURE = "HTTP_X_HUB_SIGNATURE"
SIGNATURE_256 = "HTTP_X_HUB_SIGNATURE_256"
HOOK_INSTALLATION_TARGET_ID = "HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID"


class GitHubWebhookEvents:
Expand Down
103 changes: 102 additions & 1 deletion webhook_handlers/tests/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def __getitem__(self, key):

WEBHOOK_SECRET = b"testixik8qdauiab1yiffydimvi72ekq"
DEFAULT_APP_ID = 1234
AI_FEATURES_GH_APP_ID = 9999


class GithubWebhookHandlerTests(APITestCase):
Expand All @@ -58,16 +59,21 @@ def inject_mocker(request, mocker):
def mock_webhook_secret(self, mocker):
mock_config_helper(mocker, configs={"github.webhook_secret": WEBHOOK_SECRET})

@pytest.fixture(autouse=True)
def mock_ai_features_app_id(self, mocker):
mock_config_helper(mocker, configs={"github.ai_features_app_id": 9999})

@pytest.fixture(autouse=True)
def mock_default_app_id(self, mocker):
mock_config_helper(mocker, configs={"github.integration.id": DEFAULT_APP_ID})

def _post_event_data(self, event, data={}):
def _post_event_data(self, event, data={}, app_id=DEFAULT_APP_ID):
return self.client.post(
reverse("github-webhook"),
**{
GitHubHTTPHeaders.EVENT: event,
GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5),
GitHubHTTPHeaders.HOOK_INSTALLATION_TARGET_ID: app_id,
GitHubHTTPHeaders.SIGNATURE_256: "sha256="
+ hmac.new(
WEBHOOK_SECRET,
Expand Down Expand Up @@ -1420,3 +1426,98 @@ def test_repo_creation_doesnt_crash_for_forked_repo(self):
)

assert owner.repository_set.filter(name="testrepo").exists()

def test_check_codecov_ai_auto_enabled_reviews_enabled(self):
# Create an organization with AI PR review enabled
org_with_ai_enabled = OwnerFactory(
service=Service.GITHUB.value, yaml={"ai_pr_review": {"auto_review": True}}
)

response = self._post_event_data(
event=GitHubWebhookEvents.PULL_REQUEST,
data={
"action": "pull_request",
"repository": {
"id": 506003,
"name": "testrepo",
"private": False,
"default_branch": "main",
"owner": {"id": org_with_ai_enabled.service_id},
"fork": True,
"parent": {
"name": "mainrepo",
"language": "python",
"id": 7940284,
"private": False,
"default_branch": "main",
"owner": {"id": 8495712939, "login": "alogin"},
},
},
},
app_id=9999,
)
assert response.data == {"auto_review_enabled": True}

def test_check_codecov_ai_auto_enabled_reviews_disabled(self):
# Test with AI PR review disabled
org_with_ai_disabled = OwnerFactory(
service=Service.GITHUB.value, yaml={"ai_pr_review": {"auto_review": False}}
)

response = self._post_event_data(
event=GitHubWebhookEvents.PULL_REQUEST,
data={
"action": "pull_request",
"repository": {
"id": 506004,
"name": "testrepo2",
"private": False,
"default_branch": "main",
"owner": {"id": org_with_ai_disabled.service_id},
},
},
app_id=9999,
)
assert response.data == {"auto_review_enabled": False}

def test_check_codecov_ai_auto_enabled_reviews_no_config(self):
# Test with no yaml config
org_with_no_config = OwnerFactory(service=Service.GITHUB.value, yaml={})

response = self._post_event_data(
event=GitHubWebhookEvents.PULL_REQUEST,
data={
"action": "pull_request",
"repository": {
"id": 506005,
"name": "testrepo3",
"private": False,
"default_branch": "main",
"owner": {"id": org_with_no_config.service_id},
},
},
app_id=9999,
)
assert response.data == {"auto_review_enabled": False}

def test_check_codecov_ai_auto_enabled_reviews_partial_config(self):
# Test with partial yaml config
org_with_partial_config = OwnerFactory(
service=Service.GITHUB.value, yaml={"ai_pr_review": {}}
)

response = self._post_event_data(
event=GitHubWebhookEvents.PULL_REQUEST,
data={
"action": "pull_request",
"repository": {
"id": 506006,
"name": "testrepo4",
"private": False,
"default_branch": "main",
"owner": {"id": org_with_partial_config.service_id},
},
},
app_id=9999,
)
assert response.data == {"auto_review_enabled": False}
33 changes: 29 additions & 4 deletions webhook_handlers/views/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class GithubWebhookHandler(APIView):

service_name = "github"

@property
def ai_features_app_id(self):
return get_config("github", "ai_features_app_id")

def _inc_recv(self):
action = self.request.data.get("action", "")
WEBHOOKS_RECEIVED.labels(
Expand Down Expand Up @@ -364,7 +368,14 @@ def status(self, request, *args, **kwargs):

return Response()

def _is_ai_features_request(self, request):
target_id = request.META.get(GitHubHTTPHeaders.HOOK_INSTALLATION_TARGET_ID, "")
return str(target_id) == str(self.ai_features_app_id)

def pull_request(self, request, *args, **kwargs):
if self._is_ai_features_request(request):
return self.check_codecov_ai_auto_enabled_reviews(request)

repo = self._get_repo(request)

if not repo.active:
Expand Down Expand Up @@ -398,6 +409,19 @@ def pull_request(self, request, *args, **kwargs):

return Response()

def check_codecov_ai_auto_enabled_reviews(self, request):
org = Owner.objects.get(
service=self.service_name,
service_id=request.data["repository"]["owner"]["id"],
)

auto_review_enabled = org.yaml.get("ai_pr_review", {}).get("auto_review", False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note for later: think whether you want to use the full yaml vs the org yaml or any variant

return Response(
data={
"auto_review_enabled": auto_review_enabled,
}
)

def _decide_app_name(self, ghapp: GithubAppInstallation) -> str:
"""Possibly updated the name of a GithubAppInstallation that has been fetched from DB or created.
Only the real default installation maybe use the name `GITHUB_APP_INSTALLATION_DEFAULT_NAME`
Expand Down Expand Up @@ -520,9 +544,11 @@ def _handle_installation_events(
AmplitudeEventPublisher().publish(
"App Installed",
{
"user_ownerid": installer.ownerid
if installer is not None
else owner.ownerid,
"user_ownerid": (
installer.ownerid
if installer is not None
else owner.ownerid
),
"ownerid": owner.ownerid,
},
)
Expand Down Expand Up @@ -751,7 +777,6 @@ def post(self, request, *args, **kwargs):
delivery=self.request.META.get(GitHubHTTPHeaders.DELIVERY_TOKEN),
),
)

self.validate_signature(request)

if handler := getattr(self, self.event, None):
Expand Down
Loading