Repository: readthedocs/readthedocs.org
Issue: Deleting a branch on GitHub triggers a build

When a branch is defined as an active version on Read the Docs, [deleting it from GitHub](https://github.com/readthedocs/test-builds/pull/8#event-2889925571) will [trigger a build on Read the Docs](https://readthedocs.org/projects/test-builds/builds/10130826/) which will fail because when trying to pull down that branch it will fail.

The [webhook received on Read the Docs](https://readthedocs.org/dashboard/test-builds/integrations/55690/exchange/7c17b617-cf4f-4581-8e8f-2b1c700dd4de/) is a PUSH event with a particular field `deleted: true`. So, we may want to detect these and skip triggering a build for those. Also, we should double check that the version for that branch is deleted.

Changed files:
readthedocs/rtd_tests/tests/test_api.py, readthedocs/api/v2/views/integrations.py

diff --git a/readthedocs/api/v2/views/integrations.py b/readthedocs/api/v2/views/integrations.py
index 3ca08fa85..7c1a3d08b 100644
--- a/readthedocs/api/v2/views/integrations.py
+++ b/readthedocs/api/v2/views/integrations.py
@@ -202,6 +202,7 @@ class WebhookMixin:
             'build_triggered': False,
             'project': project.slug,
             'versions': [version],
+            'versions_synced': True,
         }
 
     def get_external_version_response(self, project):
@@ -359,6 +360,8 @@ class GitHubWebhookView(WebhookMixin, APIView):
     def handle_webhook(self):
         # Get event and trigger other webhook events
         action = self.data.get('action', None)
+        created = self.data.get('created', False)
+        deleted = self.data.get('deleted', False)
         event = self.request.META.get(GITHUB_EVENT_HEADER, GITHUB_PUSH)
         webhook_github.send(
             Project,
@@ -366,17 +369,19 @@ class GitHubWebhookView(WebhookMixin, APIView):
             data=self.data,
             event=event,
         )
-        # Handle push events and trigger builds
-        if event == GITHUB_PUSH:
+        # Don't build a branch if it's a push that was actually a delete
+        # https://developer.github.com/v3/activity/events/types/#pushevent
+        if event == GITHUB_PUSH and not (deleted or created):
             try:
                 branches = [self._normalize_ref(self.data['ref'])]
                 return self.get_response_push(self.project, branches)
             except KeyError:
                 raise ParseError('Parameter "ref" is required')
-        if event in (GITHUB_CREATE, GITHUB_DELETE):
+        # Sync versions on other PUSH events that create or delete
+        elif event in (GITHUB_CREATE, GITHUB_DELETE, GITHUB_PUSH):
             return self.sync_versions(self.project)
 
-        if (
+        elif (
             self.project.has_feature(Feature.EXTERNAL_VERSION_BUILD) and
             event == GITHUB_PULL_REQUEST and action
         ):
diff --git a/readthedocs/rtd_tests/tests/test_api.py b/readthedocs/rtd_tests/tests/test_api.py
index 09d26b2ae..126f9ab4d 100644
--- a/readthedocs/rtd_tests/tests/test_api.py
+++ b/readthedocs/rtd_tests/tests/test_api.py
@@ -909,6 +909,26 @@ class IntegrationsTests(TestCase):
             [mock.call(force=True, version=self.version_tag, project=self.project)],
         )
 
+    @mock.patch('readthedocs.core.views.hooks.sync_repository_task')
+    def test_github_webhook_no_build_on_delete(self, sync_repository_task, trigger_build):
+        client = APIClient()
+
+        payload = {'ref': 'master', 'deleted': True}
+        headers = {GITHUB_EVENT_HEADER: GITHUB_PUSH}
+        resp = client.post(
+            '/api/v2/webhook/github/{}/'.format(self.project.slug),
+            payload,
+            format='json',
+            **headers
+        )
+        self.assertEqual(resp.status_code, status.HTTP_200_OK)
+        self.assertFalse(resp.data['build_triggered'])
+        self.assertEqual(resp.data['project'], self.project.slug)
+        self.assertEqual(resp.data['versions'], [LATEST])
+        trigger_build.assert_not_called()
+        latest_version = self.project.versions.get(slug=LATEST)
+        sync_repository_task.delay.assert_called_with(latest_version.pk)
+
     @mock.patch('readthedocs.core.views.hooks.sync_repository_task')
     def test_github_create_event(self, sync_repository_task, trigger_build):
         client = APIClient()