diff --git a/static/images/integrations/logos/netlify.svg b/static/images/integrations/logos/netlify.svg new file mode 100644 index 0000000000..b76a5d70f6 --- /dev/null +++ b/static/images/integrations/logos/netlify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/images/integrations/netlify/001.png b/static/images/integrations/netlify/001.png new file mode 100644 index 0000000000..c6d0826a02 Binary files /dev/null and b/static/images/integrations/netlify/001.png differ diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index 0904f3dad1..d3ef70565b 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -329,6 +329,7 @@ WEBHOOK_INTEGRATIONS = [ WebhookIntegration('jira', ['project-management'], display_name='JIRA'), WebhookIntegration('librato', ['monitoring']), WebhookIntegration('mention', ['marketing'], display_name='Mention'), + WebhookIntegration('netlify', ['continuous-integration', 'deployment'], display_name='Netlify'), WebhookIntegration('newrelic', ['monitoring'], display_name='New Relic'), WebhookIntegration( 'opbeat', diff --git a/zerver/webhooks/netlify/__init__.py b/zerver/webhooks/netlify/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/zerver/webhooks/netlify/doc.md b/zerver/webhooks/netlify/doc.md new file mode 100644 index 0000000000..6a7ad90b5a --- /dev/null +++ b/zerver/webhooks/netlify/doc.md @@ -0,0 +1,19 @@ +Get Zulip notifications for your Netlify deployments! + +1. {!create-stream.md!} + +2. {!create-bot-construct-url-indented.md!} + +3. Go to your Netlify project, and click **Settings**. Click **Build & deploy**, and select **Deploy notifications**. + Click **Add Notification**, and select **Outgoing webhook**. + +4. Select an **Event**, and set **URL to notify** to the URL constructed above. Click **Save**. + +{!congrats.md!} + +![](/static/images/integrations/netlify/001.png) + +!!! tip "" + For more information regarding Netlify webhooks, see: [Netlify's webhook documentation][1]. + +[1]: https://www.netlify.com/docs/webhooks/ diff --git a/zerver/webhooks/netlify/fixtures/deploy_building.json b/zerver/webhooks/netlify/fixtures/deploy_building.json new file mode 100644 index 0000000000..8bc2421653 --- /dev/null +++ b/zerver/webhooks/netlify/fixtures/deploy_building.json @@ -0,0 +1,45 @@ +{ + "id":"5b78192ac9659217dbf7c9aa", + "site_id":"573f11b2-f5f3-481f-a1f8-85feb457ff49", + "build_id":"5b78192ac9659217dbf7c9ab", + "state":"building", + "name":"objective-jepsen-35fbb2", + "url":"http://objective-jepsen-35fbb2.netlify.com", + "ssl_url":"https://objective-jepsen-35fbb2.netlify.com", + "admin_url":"https://app.netlify.com/sites/objective-jepsen-35fbb2", + "deploy_url":"http://5b78192ac9659217dbf7c9aa.objective-jepsen-35fbb2.netlify.com", + "deploy_ssl_url":"https://5b78192ac9659217dbf7c9aa--objective-jepsen-35fbb2.netlify.com", + "created_at":"2018-08-18T13:03:38.315Z", + "updated_at":"2018-08-18T13:03:39.800Z", + "user_id":"5b68ca02b13fb17905788d44", + "error_message":null, + "required":[ + + ], + "required_functions":null, + "commit_ref":null, + "review_id":null, + "branch":"master", + "commit_url":null, + "skipped":null, + "locked":null, + "log_access_attributes":{ + "type":"firebase", + "url":"https://netlify.firebaseio.com/builds/5b78192ac9659217dbf7c9ab/log" + }, + "title":null, + "review_url":null, + "published_at":null, + "context":"production", + "deploy_time":null, + "available_functions":[ + + ], + "summary":{ + "status":"building", + "messages":[ + + ] + }, + "screenshot_url":null +} diff --git a/zerver/webhooks/netlify/fixtures/deploy_created.json b/zerver/webhooks/netlify/fixtures/deploy_created.json new file mode 100644 index 0000000000..1ad81db962 --- /dev/null +++ b/zerver/webhooks/netlify/fixtures/deploy_created.json @@ -0,0 +1,52 @@ +{ + "id":"5b78192ac9659217dbf7c9aa", + "site_id":"573f11b2-f5f3-481f-a1f8-85feb457ff49", + "build_id":"5b78192ac9659217dbf7c9ab", + "state":"ready", + "name":"objective-jepsen-35fbb2", + "url":"http://objective-jepsen-35fbb2.netlify.com", + "ssl_url":"https://objective-jepsen-35fbb2.netlify.com", + "admin_url":"https://app.netlify.com/sites/objective-jepsen-35fbb2", + "deploy_url":"http://5b78192ac9659217dbf7c9aa.objective-jepsen-35fbb2.netlify.com", + "deploy_ssl_url":"https://5b78192ac9659217dbf7c9aa--objective-jepsen-35fbb2.netlify.com", + "created_at":"2018-08-18T13:03:38.315Z", + "updated_at":"2018-08-18T13:03:44.609Z", + "user_id":"5b68ca02b13fb17905788d44", + "error_message":null, + "required":[ + + ], + "required_functions":[ + + ], + "commit_ref":null, + "review_id":null, + "branch":"master", + "commit_url":null, + "skipped":null, + "locked":null, + "log_access_attributes":{ + "type":"firebase", + "url":"https://netlify.firebaseio.com/builds/5b78192ac9659217dbf7c9ab/log" + }, + "title":null, + "review_url":null, + "published_at":"2018-08-18T13:03:42.462Z", + "context":"production", + "deploy_time":3, + "available_functions":[ + + ], + "summary":{ + "status":"ready", + "messages":[ + { + "type":"info", + "title":"No new files created for this deploy", + "description":null, + "image":null + } + ] + }, + "screenshot_url":"https://353a23c500dde3b2ad58-c49fe7e7355d384845270f4a7a0a7aa1.ssl.cf2.rackcdn.com/5b78192ac9659217dbf7c9aa/screenshot.png" +} diff --git a/zerver/webhooks/netlify/fixtures/deploy_failed.json b/zerver/webhooks/netlify/fixtures/deploy_failed.json new file mode 100644 index 0000000000..ec83265ef7 --- /dev/null +++ b/zerver/webhooks/netlify/fixtures/deploy_failed.json @@ -0,0 +1,41 @@ +{ + "id":"5b64d77c02ed83730664c2f6", + "site_id":"6f2ad239-fce7-4b54-81fe-873d4fcf5c78", + "build_id":"5b64d77c02ed83730664c2f7", + "state":"error", + "name":"objective-jepsen-35fbb2", + "url":"http://objective-jepsen-35fbb2.netlify.com", + "ssl_url":"https://objective-jepsen-35fbb2.netlify.com", + "admin_url":"https://app.netlify.com/sites/objective-jepsen-35fbb2", + "deploy_url":"http://5b78192ac9659217dbf7c9aa.objective-jepsen-35fbb2.netlify.com", + "deploy_ssl_url":"https://5b78192ac9659217dbf7c9aa--objective-jepsen-35fbb2.netlify.com", + "created_at":"2018-08-03T22:30:20.261Z", + "updated_at":"2018-08-03T22:30:27.734Z", + "user_id":"5b64d15c82d3f16bcbbdcdbe", + "error_message":"failed during stage 'building site': Build script returned non-zero exit code: 127", + "required":[ + + ], + "required_functions":null, + "commit_ref":null, + "review_id":null, + "branch":"master", + "commit_url":null, + "skipped":null, + "locked":null, + "log_access_attributes":{ + "type":"firebase", + "url":"https://netlify.firebaseio.com/builds/5b64d77c02ed83730664c2f7/log" + }, + "title":null, + "review_url":null, + "published_at":null, + "context":"production", + "deploy_time":null, + "available_functions":[ + + ], + "summary":{ + + } +} diff --git a/zerver/webhooks/netlify/fixtures/deploy_locked.json b/zerver/webhooks/netlify/fixtures/deploy_locked.json new file mode 100644 index 0000000000..347f8e4c36 --- /dev/null +++ b/zerver/webhooks/netlify/fixtures/deploy_locked.json @@ -0,0 +1,43 @@ +{ + "id":"5b64d2c3792f8946ae9ddc8b", + "site_id":"6f2ad239-fce7-4b54-81fe-873d4fcf5c78", + "build_id":"5b64d2c3792f8946ae9ddc8c", + "state":"ready", + "name":"objective-jepsen-35fbb2", + "url":"http://objective-jepsen-35fbb2.netlify.com", + "ssl_url":"https://objective-jepsen-35fbb2.netlify.com", + "admin_url":"https://app.netlify.com/sites/objective-jepsen-35fbb2", + "deploy_url":"http://5b78192ac9659217dbf7c9aa.objective-jepsen-35fbb2.netlify.com", + "deploy_ssl_url":"https://5b78192ac9659217dbf7c9aa--objective-jepsen-35fbb2.netlify.com", + "created_at":"2018-08-03T22:10:11.180Z", + "updated_at":"2018-08-03T22:21:59.875Z", + "user_id":"5b64d15c82d3f16bcbbdcdbe", + "error_message":null, + "required":[ + + ], + "required_functions":[ + + ], + "commit_ref":null, + "review_id":null, + "branch":"master", + "commit_url":null, + "skipped":null, + "locked":true, + "log_access_attributes":{ + "type":"firebase", + "url":"https://netlify.firebaseio.com/builds/5b64d2c3792f8946ae9ddc8c/log" + }, + "title":null, + "review_url":null, + "published_at":"2018-08-03T22:21:59.747Z", + "context":"production", + "deploy_time":2, + "available_functions":[ + + ], + "summary":{ + + } +} diff --git a/zerver/webhooks/netlify/fixtures/deploy_unlocked.json b/zerver/webhooks/netlify/fixtures/deploy_unlocked.json new file mode 100644 index 0000000000..3cafb85665 --- /dev/null +++ b/zerver/webhooks/netlify/fixtures/deploy_unlocked.json @@ -0,0 +1,43 @@ +{ + "id":"5b64d2c3792f8946ae9ddc8b", + "site_id":"6f2ad239-fce7-4b54-81fe-873d4fcf5c78", + "build_id":"5b64d2c3792f8946ae9ddc8c", + "state":"ready", + "name":"objective-jepsen-35fbb2", + "url":"http://objective-jepsen-35fbb2.netlify.com", + "ssl_url":"https://objective-jepsen-35fbb2.netlify.com", + "admin_url":"https://app.netlify.com/sites/objective-jepsen-35fbb2", + "deploy_url":"http://5b78192ac9659217dbf7c9aa.objective-jepsen-35fbb2.netlify.com", + "deploy_ssl_url":"https://5b78192ac9659217dbf7c9aa--objective-jepsen-35fbb2.netlify.com", + "created_at":"2018-08-03T22:10:11.180Z", + "updated_at":"2018-08-03T22:24:04.622Z", + "user_id":"5b64d15c82d3f16bcbbdcdbe", + "error_message":null, + "required":[ + + ], + "required_functions":[ + + ], + "commit_ref":null, + "review_id":null, + "branch":"master", + "commit_url":null, + "skipped":null, + "locked":false, + "log_access_attributes":{ + "type":"firebase", + "url":"https://netlify.firebaseio.com/builds/5b64d2c3792f8946ae9ddc8c/log" + }, + "title":null, + "review_url":null, + "published_at":"2018-08-03T22:21:59.747Z", + "context":"production", + "deploy_time":2, + "available_functions":[ + + ], + "summary":{ + + } +} diff --git a/zerver/webhooks/netlify/tests.py b/zerver/webhooks/netlify/tests.py new file mode 100644 index 0000000000..43867afce4 --- /dev/null +++ b/zerver/webhooks/netlify/tests.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +from django.conf import settings + +from zerver.lib.test_classes import WebhookTestCase +from zerver.models import get_system_bot + +class NetlifyHookTests(WebhookTestCase): + STREAM_NAME = 'netlify' + URL_TEMPLATE = u"/api/v1/external/netlify?stream={stream}&api_key={api_key}" + + def test_building_message(self) -> None: + expected_subject = u"master" + expected_message = u'The build [objective-jepsen-35fbb2](http://objective-jepsen-35fbb2.netlify.com) on branch master is now building.' + + self.send_and_test_stream_message('deploy_building', expected_subject, expected_message, + content_type="application/json", HTTP_X_NETLIFY_EVENT='deploy_building') + + def test_created_message(self) -> None: + expected_subject = u"master" + expected_message = u'The build [objective-jepsen-35fbb2](http://objective-jepsen-35fbb2.netlify.com) on branch master is now ready.' + + self.send_and_test_stream_message('deploy_created', expected_subject, expected_message, + content_type="application/json", HTTP_X_NETLIFY_EVENT='deploy_created') + + def test_failed_message(self) -> None: + expected_subject = u"master" + expected_message = (u"The build [objective-jepsen-35fbb2](http://objective-jepsen-35fbb2.netlify.com) " + u"on branch master failed during stage 'building site': Build script returned non-zero exit code: 127" + ) + + self.send_and_test_stream_message('deploy_failed', expected_subject, expected_message, + content_type="application/json", HTTP_X_NETLIFY_EVENT='deploy_failed') + + def test_locked_message(self) -> None: + expected_subject = u"master" + expected_message = (u"The build [objective-jepsen-35fbb2](http://objective-jepsen-35fbb2.netlify.com) " + u"on branch master is now locked." + ) + + self.send_and_test_stream_message('deploy_locked', expected_subject, expected_message, + content_type="application/json", HTTP_X_NETLIFY_EVENT='deploy_locked') + + def test_unlocked_message(self) -> None: + expected_subject = u"master" + expected_message = (u"The build [objective-jepsen-35fbb2](http://objective-jepsen-35fbb2.netlify.com) " + u"on branch master is now unlocked." + ) + + self.send_and_test_stream_message('deploy_unlocked', expected_subject, expected_message, + content_type="application/json", HTTP_X_NETLIFY_EVENT='deploy_unlocked') + + def get_body(self, fixture_name: str) -> str: + return self.webhook_fixture_data("netlify", fixture_name, file_type="json") diff --git a/zerver/webhooks/netlify/view.py b/zerver/webhooks/netlify/view.py new file mode 100644 index 0000000000..872e0cd13c --- /dev/null +++ b/zerver/webhooks/netlify/view.py @@ -0,0 +1,50 @@ +from typing import Any, Dict, Iterable, Optional + +from django.http import HttpRequest, HttpResponse +from django.utils.translation import ugettext as _ + +from zerver.decorator import api_key_only_webhook_view +from zerver.lib.webhooks.common import check_send_webhook_message, \ + validate_extract_webhook_http_header, UnexpectedWebhookEventType +from zerver.lib.request import REQ, has_request_variables +from zerver.lib.response import json_error, json_success +from zerver.lib.validator import check_dict, check_string +from zerver.models import UserProfile + +EVENTS = ['deploy_failed', 'deploy_locked', 'deploy_unlocked', 'deploy_building', 'deploy_created'] + +@api_key_only_webhook_view('Netlify') +@has_request_variables +def api_netlify_webhook( + request: HttpRequest, user_profile: UserProfile, + payload: Dict[str, Iterable[Dict[str, Any]]]=REQ(argument_type='body') +) -> HttpResponse: + + message_template = get_template(request, payload) + + body = message_template.format(build_name=payload['name'], + build_url=payload['url'], + branch_name=payload['branch'], + state=payload['state']) + + topic = "{topic}".format(topic=payload['branch']) + + check_send_webhook_message(request, user_profile, topic, body) + + return json_success() + +def get_template(request: HttpRequest, payload: Dict[str, Any]) -> str: + + message_template = u'The build [{build_name}]({build_url}) on branch {branch_name} ' + event = validate_extract_webhook_http_header(request, 'X_NETLIFY_EVENT', 'Netlify') + + if event == 'deploy_failed': + return message_template + payload['error_message'] + elif event == 'deploy_locked': + return message_template + 'is now locked.' + elif event == 'deploy_unlocked': + return message_template + 'is now unlocked.' + elif event in EVENTS: + return message_template + 'is now {state}.'.format(state=payload['state']) + else: + raise UnexpectedWebhookEventType('Netlify', event)