Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit d3b6462

Browse files
[feat] Check if auto PR reviews are enabled for a given owner (#1285)
Co-authored-by: codecov-ai[bot] <156709835+codecov-ai[bot]@users.noreply.github.com>
1 parent 6f1d6b5 commit d3b6462

File tree

3 files changed

+132
-5
lines changed

3 files changed

+132
-5
lines changed

webhook_handlers/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ class GitHubHTTPHeaders:
33
DELIVERY_TOKEN = "HTTP_X_GITHUB_DELIVERY"
44
SIGNATURE = "HTTP_X_HUB_SIGNATURE"
55
SIGNATURE_256 = "HTTP_X_HUB_SIGNATURE_256"
6+
HOOK_INSTALLATION_TARGET_ID = "HTTP_X_GITHUB_HOOK_INSTALLATION_TARGET_ID"
67

78

89
class GitHubWebhookEvents:

webhook_handlers/tests/test_github.py

Lines changed: 102 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ def __getitem__(self, key):
4747

4848
WEBHOOK_SECRET = b"testixik8qdauiab1yiffydimvi72ekq"
4949
DEFAULT_APP_ID = 1234
50+
AI_FEATURES_GH_APP_ID = 9999
5051

5152

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

62+
@pytest.fixture(autouse=True)
63+
def mock_ai_features_app_id(self, mocker):
64+
mock_config_helper(mocker, configs={"github.ai_features_app_id": 9999})
65+
6166
@pytest.fixture(autouse=True)
6267
def mock_default_app_id(self, mocker):
6368
mock_config_helper(mocker, configs={"github.integration.id": DEFAULT_APP_ID})
6469

65-
def _post_event_data(self, event, data={}):
70+
def _post_event_data(self, event, data={}, app_id=DEFAULT_APP_ID):
6671
return self.client.post(
6772
reverse("github-webhook"),
6873
**{
6974
GitHubHTTPHeaders.EVENT: event,
7075
GitHubHTTPHeaders.DELIVERY_TOKEN: uuid.UUID(int=5),
76+
GitHubHTTPHeaders.HOOK_INSTALLATION_TARGET_ID: app_id,
7177
GitHubHTTPHeaders.SIGNATURE_256: "sha256="
7278
+ hmac.new(
7379
WEBHOOK_SECRET,
@@ -1420,3 +1426,98 @@ def test_repo_creation_doesnt_crash_for_forked_repo(self):
14201426
)
14211427

14221428
assert owner.repository_set.filter(name="testrepo").exists()
1429+
1430+
def test_check_codecov_ai_auto_enabled_reviews_enabled(self):
1431+
# Create an organization with AI PR review enabled
1432+
org_with_ai_enabled = OwnerFactory(
1433+
service=Service.GITHUB.value, yaml={"ai_pr_review": {"auto_review": True}}
1434+
)
1435+
1436+
response = self._post_event_data(
1437+
event=GitHubWebhookEvents.PULL_REQUEST,
1438+
data={
1439+
"action": "pull_request",
1440+
"repository": {
1441+
"id": 506003,
1442+
"name": "testrepo",
1443+
"private": False,
1444+
"default_branch": "main",
1445+
"owner": {"id": org_with_ai_enabled.service_id},
1446+
"fork": True,
1447+
"parent": {
1448+
"name": "mainrepo",
1449+
"language": "python",
1450+
"id": 7940284,
1451+
"private": False,
1452+
"default_branch": "main",
1453+
"owner": {"id": 8495712939, "login": "alogin"},
1454+
},
1455+
},
1456+
},
1457+
app_id=9999,
1458+
)
1459+
assert response.data == {"auto_review_enabled": True}
1460+
1461+
def test_check_codecov_ai_auto_enabled_reviews_disabled(self):
1462+
# Test with AI PR review disabled
1463+
org_with_ai_disabled = OwnerFactory(
1464+
service=Service.GITHUB.value, yaml={"ai_pr_review": {"auto_review": False}}
1465+
)
1466+
1467+
response = self._post_event_data(
1468+
event=GitHubWebhookEvents.PULL_REQUEST,
1469+
data={
1470+
"action": "pull_request",
1471+
"repository": {
1472+
"id": 506004,
1473+
"name": "testrepo2",
1474+
"private": False,
1475+
"default_branch": "main",
1476+
"owner": {"id": org_with_ai_disabled.service_id},
1477+
},
1478+
},
1479+
app_id=9999,
1480+
)
1481+
assert response.data == {"auto_review_enabled": False}
1482+
1483+
def test_check_codecov_ai_auto_enabled_reviews_no_config(self):
1484+
# Test with no yaml config
1485+
org_with_no_config = OwnerFactory(service=Service.GITHUB.value, yaml={})
1486+
1487+
response = self._post_event_data(
1488+
event=GitHubWebhookEvents.PULL_REQUEST,
1489+
data={
1490+
"action": "pull_request",
1491+
"repository": {
1492+
"id": 506005,
1493+
"name": "testrepo3",
1494+
"private": False,
1495+
"default_branch": "main",
1496+
"owner": {"id": org_with_no_config.service_id},
1497+
},
1498+
},
1499+
app_id=9999,
1500+
)
1501+
assert response.data == {"auto_review_enabled": False}
1502+
1503+
def test_check_codecov_ai_auto_enabled_reviews_partial_config(self):
1504+
# Test with partial yaml config
1505+
org_with_partial_config = OwnerFactory(
1506+
service=Service.GITHUB.value, yaml={"ai_pr_review": {}}
1507+
)
1508+
1509+
response = self._post_event_data(
1510+
event=GitHubWebhookEvents.PULL_REQUEST,
1511+
data={
1512+
"action": "pull_request",
1513+
"repository": {
1514+
"id": 506006,
1515+
"name": "testrepo4",
1516+
"private": False,
1517+
"default_branch": "main",
1518+
"owner": {"id": org_with_partial_config.service_id},
1519+
},
1520+
},
1521+
app_id=9999,
1522+
)
1523+
assert response.data == {"auto_review_enabled": False}

webhook_handlers/views/github.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ class GithubWebhookHandler(APIView):
5252

5353
service_name = "github"
5454

55+
@property
56+
def ai_features_app_id(self):
57+
return get_config("github", "ai_features_app_id")
58+
5559
def _inc_recv(self):
5660
action = self.request.data.get("action", "")
5761
WEBHOOKS_RECEIVED.labels(
@@ -364,7 +368,14 @@ def status(self, request, *args, **kwargs):
364368

365369
return Response()
366370

371+
def _is_ai_features_request(self, request):
372+
target_id = request.META.get(GitHubHTTPHeaders.HOOK_INSTALLATION_TARGET_ID, "")
373+
return str(target_id) == str(self.ai_features_app_id)
374+
367375
def pull_request(self, request, *args, **kwargs):
376+
if self._is_ai_features_request(request):
377+
return self.check_codecov_ai_auto_enabled_reviews(request)
378+
368379
repo = self._get_repo(request)
369380

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

399410
return Response()
400411

412+
def check_codecov_ai_auto_enabled_reviews(self, request):
413+
org = Owner.objects.get(
414+
service=self.service_name,
415+
service_id=request.data["repository"]["owner"]["id"],
416+
)
417+
418+
auto_review_enabled = org.yaml.get("ai_pr_review", {}).get("auto_review", False)
419+
return Response(
420+
data={
421+
"auto_review_enabled": auto_review_enabled,
422+
}
423+
)
424+
401425
def _decide_app_name(self, ghapp: GithubAppInstallation) -> str:
402426
"""Possibly updated the name of a GithubAppInstallation that has been fetched from DB or created.
403427
Only the real default installation maybe use the name `GITHUB_APP_INSTALLATION_DEFAULT_NAME`
@@ -520,9 +544,11 @@ def _handle_installation_events(
520544
AmplitudeEventPublisher().publish(
521545
"App Installed",
522546
{
523-
"user_ownerid": installer.ownerid
524-
if installer is not None
525-
else owner.ownerid,
547+
"user_ownerid": (
548+
installer.ownerid
549+
if installer is not None
550+
else owner.ownerid
551+
),
526552
"ownerid": owner.ownerid,
527553
},
528554
)
@@ -751,7 +777,6 @@ def post(self, request, *args, **kwargs):
751777
delivery=self.request.META.get(GitHubHTTPHeaders.DELIVERY_TOKEN),
752778
),
753779
)
754-
755780
self.validate_signature(request)
756781

757782
if handler := getattr(self, self.event, None):

0 commit comments

Comments
 (0)