diff --git a/readthedocs/api/v2/views/integrations.py b/readthedocs/api/v2/views/integrations.py index 7717ab8a17a..bd75fb5eab8 100644 --- a/readthedocs/api/v2/views/integrations.py +++ b/readthedocs/api/v2/views/integrations.py @@ -32,7 +32,9 @@ from readthedocs.core.views.hooks import trigger_sync_versions from readthedocs.integrations.models import HttpExchange from readthedocs.integrations.models import Integration +from readthedocs.notifications.models import Notification from readthedocs.projects.models import Project +from readthedocs.projects.notifications import MESSAGE_PROJECT_DEPRECATED_WEBHOOK from readthedocs.vcs_support.backends.git import parse_version_from_ref @@ -448,6 +450,29 @@ def handle_webhook(self): See https://developer.github.com/v3/activity/events/types/ """ + if self.project.is_github_app_project: + Notification.objects.add( + message_id=MESSAGE_PROJECT_DEPRECATED_WEBHOOK, + attached_to=self.project, + dismissable=True, + ) + return Response( + { + "detail": " ".join( + dedent( + """ + This project is connected to our GitHub App and doesn't require a separate webhook, ignoring webhook event. + Remove the deprecated webhook from your repository to avoid duplicate events, + see https://docs.readthedocs.com/platform/stable/reference/git-integration.html#manually-migrating-a-project. + """ + ) + .strip() + .splitlines() + ) + }, + status=status.HTTP_400_BAD_REQUEST, + ) + # Get event and trigger other webhook events action = self.data.get("action", None) created = self.data.get("created", False) diff --git a/readthedocs/projects/notifications.py b/readthedocs/projects/notifications.py index 9b9e5d43e7a..6cf0e873e7d 100644 --- a/readthedocs/projects/notifications.py +++ b/readthedocs/projects/notifications.py @@ -18,6 +18,7 @@ MESSAGE_PROJECT_SKIP_BUILDS = "project:invalid:skip-builds" MESSAGE_PROJECT_ADDONS_BY_DEFAULT = "project:addons:by-default" MESSAGE_PROJECT_SSH_KEY_WITH_WRITE_ACCESS = "project:ssh-key-with-write-access" +MESSAGE_PROJECT_DEPRECATED_WEBHOOK = "project:webhooks:deprecated" messages = [ Message( @@ -193,5 +194,19 @@ ), type=WARNING, ), + Message( + id=MESSAGE_PROJECT_DEPRECATED_WEBHOOK, + header=_("Remove deprecated webhook"), + body=_( + textwrap.dedent( + """ + This project is connected to our GitHub App and doesn't require a separate webhook. + Remove the deprecated webhook from your repository + to avoid duplicate events. + """ + ).strip(), + ), + type=INFO, + ), ] registry.add(messages) diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index 6400910f527..82673747f18 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -74,6 +74,7 @@ from readthedocs.projects.models import Project from readthedocs.projects.models import ProjectRelationship from readthedocs.projects.models import WebHook +from readthedocs.projects.notifications import MESSAGE_PROJECT_DEPRECATED_WEBHOOK from readthedocs.projects.tasks.utils import clean_project_resources from readthedocs.projects.utils import get_csv_file from readthedocs.projects.views.base import ProjectAdminMixin @@ -967,6 +968,22 @@ class IntegrationDelete(IntegrationMixin, DeleteViewWithMessage): success_message = _("Integration deleted") http_method_names = ["post"] + def post(self, request, *args, **kwargs): + resp = super().post(request, *args, **kwargs) + # Dismiss notification about removing the GitHub webhook. + project = self.get_project() + if ( + project.is_github_app_project + and not project.integrations.filter( + integration_type=Integration.GITHUB_WEBHOOK + ).exists() + ): + Notification.objects.cancel( + attached_to=project, + message_id=MESSAGE_PROJECT_DEPRECATED_WEBHOOK, + ) + return resp + class IntegrationExchangeDetail(IntegrationMixin, DetailView): model = HttpExchange diff --git a/readthedocs/rtd_tests/tests/test_api.py b/readthedocs/rtd_tests/tests/test_api.py index 8f547ce28c8..b8092f2e9ef 100644 --- a/readthedocs/rtd_tests/tests/test_api.py +++ b/readthedocs/rtd_tests/tests/test_api.py @@ -75,6 +75,7 @@ Project, ) from readthedocs.aws.security_token_service import AWSS3TemporaryCredentials +from readthedocs.projects.notifications import MESSAGE_PROJECT_DEPRECATED_WEBHOOK from readthedocs.subscriptions.constants import TYPE_CONCURRENT_BUILDS from readthedocs.subscriptions.products import RTDProductFeature from readthedocs.vcs_support.backends.git import parse_version_from_ref @@ -2628,6 +2629,50 @@ def test_github_get_external_version_data(self, trigger_build): self.assertEqual(version_data.source_branch, "source_branch") self.assertEqual(version_data.base_branch, "master") + def test_github_skip_githubapp_projects(self, trigger_build): + installation = get( + GitHubAppInstallation, + installation_id=1111, + target_id=1111, + target_type=GitHubAccountType.USER, + ) + remote_repository = get( + RemoteRepository, + remote_id="1234", + name="repo", + full_name="user/repo", + vcs_provider=GitHubAppProvider.id, + github_app_installation=installation, + ) + self.project.remote_repository = remote_repository + self.project.save() + + assert self.project.is_github_app_project + assert self.project.notifications.count() == 0 + + client = APIClient() + payload = '{"ref":"refs/heads/master"}' + signature = get_signature( + self.github_integration, + payload, + ) + headers = { + GITHUB_EVENT_HEADER: GITHUB_PUSH, + GITHUB_SIGNATURE_HEADER: signature, + } + resp = client.post( + reverse("api_webhook_github", kwargs={"project_slug": self.project.slug}), + json.loads(payload), + format="json", + headers=headers, + ) + assert resp.status_code == 400 + assert "This project is connected to our GitHub App" in resp.data["detail"] + + notification = self.project.notifications.first() + assert notification is not None + assert notification.message_id == MESSAGE_PROJECT_DEPRECATED_WEBHOOK + def test_gitlab_webhook_for_branches(self, trigger_build): """GitLab webhook API.""" client = APIClient()