diff --git a/webhook_handlers/constants.py b/webhook_handlers/constants.py index 189e7520f1..0f8a5a6014 100644 --- a/webhook_handlers/constants.py +++ b/webhook_handlers/constants.py @@ -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: diff --git a/webhook_handlers/tests/test_github.py b/webhook_handlers/tests/test_github.py index 9e11cc7e5b..09c9fb1889 100644 --- a/webhook_handlers/tests/test_github.py +++ b/webhook_handlers/tests/test_github.py @@ -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): @@ -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, @@ -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} diff --git a/webhook_handlers/views/github.py b/webhook_handlers/views/github.py index b3d3a56112..637c41f36c 100644 --- a/webhook_handlers/views/github.py +++ b/webhook_handlers/views/github.py @@ -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( @@ -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: @@ -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) + 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` @@ -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, }, ) @@ -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):