webhooks/semaphore: Add support for Semaphore 2.0 notifications.

Semaphore has currently has two different versions of their product -
Classic and 2.0. This commit adds support for Semaphore 2.0, along side
Semaphore Classic, using the same webhook. This would let the integration
work seamlessly for users who have already configured a Zulip integration in
their Semaphore 2.0 projects.

Semaphore 2.0 currently only supports GitHub and their payloads do not
contain URLs for common entities like commits, pull requests and tags. We
construct URLs for them using templates, but also try to support other
services by providing notifications without URLs.

Closes #14171

Co-authored-by: Puneeth Chaganti <punchagan@muse-amuse.in>
This commit is contained in:
Abhinav 2020-03-14 17:42:03 +05:30 committed by Tim Abbott
parent a3164a3316
commit 41fc7b2ae1
7 changed files with 486 additions and 10 deletions

View File

@ -327,7 +327,7 @@ WEBHOOK_INTEGRATIONS = [
WebhookIntegration('pivotal', ['project-management'], display_name='Pivotal Tracker'),
WebhookIntegration('raygun', ['monitoring'], display_name="Raygun"),
WebhookIntegration('reviewboard', ['version-control'], display_name="ReviewBoard"),
WebhookIntegration('semaphore', ['continuous-integration', 'deployment'], stream_name='builds'),
WebhookIntegration('semaphore', ['continuous-integration', 'deployment']),
WebhookIntegration('sentry', ['monitoring']),
WebhookIntegration('slack', ['communication']),
WebhookIntegration('solano', ['continuous-integration'], display_name='Solano Labs'),

View File

@ -4,9 +4,13 @@ Get Zulip notifications for your Semaphore builds!
1. {!create-bot-construct-url-indented.md!}
1. In your Semaphore project, go to **Project settings**,
and select the **Notifications** tab. Click on **Webhooks**, and
click **+ Add Webhook**.
1. In Semaphore 2.0, under **Configuration** select **Notifications**. Click on
**Create New Notification**. Add the the URL constructed above to the Webhook
**Endpoint** field.
If you are using Semaphore Classic, in your Semaphore project, go to
**Project settings**, and select the **Notifications** tab. Click on
**Webhooks**, and click **+ Add Webhook**.
1. Set **URL** to the URL constructed above, and click
**Save Settings**.

View File

@ -0,0 +1,74 @@
{
"workflow": {
"initial_pipeline_id": "17e8dfaf-6649-413d-b611-4a888aebc29e",
"id": "84383f37-d025-4811-b719-61c6acc92a1e",
"created_at": "2020-04-08T12:54:46Z"
},
"version": "1.0.0",
"revision": {
"tag": null,
"sender": {
"login": "radwo",
"email": "",
"avatar_url": "https://avatars0.githubusercontent.com/u/315678?v=4"
},
"reference_type": "pull_request",
"reference": "refs/semaphoreci/0387f435823058fdc05f3ef86fb9f28b885ccf7f",
"pull_request": {
"number": "3",
"name": "Testing PR notifications",
"head_sha": "6fd84f4c2aecae7b1e6ce516b634c220d4a3e3e4",
"head_repo_slug": "renderedtext/notifications",
"commit_range": "8d31831ff2a512e23a8042a76cf39246908256a1...6fd84f4c2aecae7b1e6ce516b634c220d4a3e3e4",
"branch_name": "test-notifications"
},
"commit_sha": "0387f435823058fdc05f3ef86fb9f28b885ccf7f",
"commit_message": "Add docs for webhook notifications",
"branch": null
},
"repository": {
"url": "https://github.com/renderedtext/notifications",
"slug": "renderedtext/notifications"
},
"project": {
"name": "notifications",
"id": "7379341d-a22a-475f-b0c9-4553b06a926c"
},
"pipeline": {
"yaml_file_name": "semaphore.yml",
"working_directory": ".semaphore",
"stopping_at": "1970-01-01T00:00:00Z",
"state": "done",
"running_at": "2020-04-08T12:54:46Z",
"result_reason": "test",
"result": "failed",
"queuing_at": "2020-04-08T12:54:46Z",
"pending_at": "2020-04-08T12:54:46Z",
"name": "Notifications",
"id": "17e8dfaf-6649-413d-b611-4a888aebc29e",
"error_description": "",
"done_at": "2020-04-08T12:54:54Z",
"created_at": "2020-04-08T12:54:46Z"
},
"organization": {
"name": "semaphore",
"id": "0172946d-5523-48d8-912d-e13f01189e20"
},
"blocks": [
{
"state": "done",
"result_reason": "test",
"result": "failed",
"name": "Test",
"jobs": [
{
"status": "finished",
"result": "failed",
"name": "pytest",
"index": 0,
"id": "570f998f-4f20-4529-a1b6-afeba9c8f5bd"
}
]
}
]
}

View File

@ -0,0 +1,113 @@
{
"version": "1.0.0",
"organization": {
"name": "semaphore",
"id": "36360e31-fee6-42b2-9f6c-999d4c06ce81"
},
"project": {
"name": "notifications",
"id": "91e34570-bebe-42b6-b47a-ca710b2b8927"
},
"repository": {
"url": "https://github.com/renderedtext/notifications",
"slug": "renderedtext/notifications"
},
"revision": {
"tag": null,
"sender": {
"login": "radwo",
"email": "184065+radwo@users.noreply.github.com",
"avatar_url": "https://avatars2.githubusercontent.com/u/184065?v=4"
},
"reference_type": "branch",
"reference": "refs/heads/rw/webhook_impl",
"pull_request": null,
"commit_sha": "2d9f5fcec1ca7c68fa7bd44dd58ec4ff65814563",
"commit_message": "Implement webhooks for SemaphoreCI",
"branch": {
"name": "rw/webhook_impl",
"commit_range": "36ebdf6e906cf3491391442d2f779b512ca49485...2d9f5fcec1ca7c68fa7bd44dd58ec4ff65814563"
}
},
"workflow": {
"initial_pipeline_id": "fa02c7bd-7a8b-42e0-8d6e-aa0d8a194e19",
"id": "acabe58e-4bcc-4d39-be06-e98d71917703",
"created_at": "2019-12-10T13:09:54Z"
},
"pipeline": {
"yaml_file_name": "semaphore.yml",
"working_directory": ".semaphore",
"stopping_at": "2019-12-10T13:10:22Z",
"state": "done",
"running_at": "2019-12-10T13:09:58Z",
"result_reason": "user",
"result": "stopped",
"queuing_at": "2019-12-10T13:09:55Z",
"pending_at": "2019-12-10T13:09:55Z",
"name": "Notifications",
"id": "fa02c7bd-7a8b-42e0-8d6e-aa0d8a194e19",
"error_description": "",
"done_at": "2019-12-10T13:10:28Z",
"created_at": "2019-12-10T13:09:54Z"
},
"blocks": [
{
"state": "done",
"result_reason": "user",
"result": "stopped",
"name": "List & Test & Build",
"jobs": [
{
"status": "finished",
"result": "stopped",
"name": "Test",
"index": 1,
"id": "21df03d2-c4e0-4e0a-acd7-5ff60dc0727e"
},
{
"status": "finished",
"result": "stopped",
"name": "Build",
"index": 2,
"id": "84190263-362c-4051-8260-e43637f148de"
},
{
"status": "finished",
"result": "passed",
"name": "Lint",
"index": 0,
"id": "d4b93a5b-69a5-43e6-ab24-06b095fc49bf"
}
]
},
{
"state": "done",
"result_reason": "user",
"result": "stopped",
"name": "List & Test & Build 2",
"jobs": [
{
"status": "finished",
"result": "stopped",
"name": "Test 2",
"index": 1,
"id": "21df03d2-c4e0-4e0a-acd7-5ff60dc0727e"
},
{
"status": "finished",
"result": "stopped",
"name": "Build 2",
"index": 2,
"id": "84190263-362c-4051-8260-e43637f148de"
},
{
"status": "finished",
"result": "passed",
"name": "Lint",
"index": 0,
"id": "d4b93a5b-69a5-43e6-ab24-06b095fc49bf"
}
]
}
]
}

View File

@ -0,0 +1,69 @@
{
"workflow": {
"initial_pipeline_id": "cbe5d41a-a1a0-4a54-ba2c-ca0bab4a5db7",
"id": "a8704319-2422-4828-9b11-6b2afa3554e6",
"created_at": "2020-04-05T13:35:22Z"
},
"version": "1.0.0",
"revision": {
"tag": {
"name": "v1.0.1"
},
"sender": {
"login": "radwo",
"email": "radwo@semaphoreci.com",
"avatar_url": "https://avatars0.githubusercontent.com/u/1111?v=4"
},
"reference_type": "tag",
"reference": "refs/tags/v1.0.1",
"pull_request": null,
"commit_sha": "8d31831ff2a512e23a8042a76cf39246908256a1",
"commit_message": "Use pytest starter workflow",
"branch": null
},
"repository": {
"url": "https://github.com/renderedtext/notifications",
"slug": "renderedtext/notifications"
},
"project": {
"name": "notifications",
"id": "7379341d-a22a-475f-b0c9-4553b06a926c"
},
"pipeline": {
"yaml_file_name": "semaphore.yml",
"working_directory": ".semaphore",
"stopping_at": "1970-01-01T00:00:00Z",
"state": "done",
"running_at": "2020-04-05T13:35:23Z",
"result_reason": "test",
"result": "stopped",
"queuing_at": "2020-04-05T13:35:23Z",
"pending_at": "2020-04-05T13:35:22Z",
"name": "Notifications",
"id": "cbe5d41a-a1a0-4a54-ba2c-ca0bab4a5db7",
"error_description": "",
"done_at": "2020-04-05T13:35:31Z",
"created_at": "2020-04-05T13:35:22Z"
},
"organization": {
"name": "semaphore",
"id": "0172946d-5523-48d8-912d-e13f01189e20"
},
"blocks": [
{
"state": "done",
"result_reason": "test",
"result": "stopped",
"name": "List & Test & Build",
"jobs": [
{
"status": "finished",
"result": "stopped",
"name": "pytest",
"index": 0,
"id": "c10604e1-1519-419a-ba84-2a2df4a053d2"
}
]
}
]
}

View File

@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import ujson
from mock import patch
from zerver.lib.test_classes import WebhookTestCase
@ -10,6 +12,7 @@ class SemaphoreHookTests(WebhookTestCase):
# contain information on the repo and branch, and the message has links and
# details about the build, deploy, server, author, and commit
# Tests for Semaphore Classic
def test_semaphore_build(self) -> None:
expected_topic = u"knighthood/master" # repo/branch
expected_message = """
@ -31,5 +34,89 @@ class SemaphoreHookTests(WebhookTestCase):
self.send_and_test_stream_message('deploy', expected_topic, expected_message,
content_type="application/x-www-form-urlencoded")
# Tests For Semaphore 2.0
def test_semaphore2_push(self) -> None:
expected_topic = u"notifications/rw/webhook_impl" # repo/branch
expected_message = """
[Notifications](https://semaphore.semaphoreci.com/workflows/acabe58e-4bcc-4d39-be06-e98d71917703) pipeline **stopped**:
* **Commit**: [(2d9f5fc)](https://github.com/renderedtext/notifications/commit/2d9f5fcec1ca7c68fa7bd44dd58ec4ff65814563) Implement webhooks for SemaphoreCI
* **Branch**: rw/webhook_impl
* **Author**: [radwo](https://github.com/radwo)
""".strip()
self.send_and_test_stream_message('push', expected_topic, expected_message,
content_type="application/json")
def test_semaphore2_push_non_gh_repo(self) -> None:
expected_topic = u"notifications/rw/webhook_impl" # repo/branch
expected_message = """
[Notifications](https://semaphore.semaphoreci.com/workflows/acabe58e-4bcc-4d39-be06-e98d71917703) pipeline **stopped**:
* **Commit**: (2d9f5fc) Implement webhooks for SemaphoreCI
* **Branch**: rw/webhook_impl
* **Author**: radwo
""".strip()
with patch('zerver.webhooks.semaphore.view.is_github_repo', return_value=False):
self.send_and_test_stream_message('push', expected_topic, expected_message,
content_type="application/json")
def test_semaphore_pull_request(self) -> None:
expected_topic = u"notifications/test-notifications"
expected_message = """
[Notifications](https://semaphore.semaphoreci.com/workflows/84383f37-d025-4811-b719-61c6acc92a1e) pipeline **failed**:
* **Pull Request**: [Testing PR notifications](https://github.com/renderedtext/notifications/pull/3)
* **Branch**: test-notifications
* **Author**: [radwo](https://github.com/radwo)
""".strip()
self.send_and_test_stream_message('pull_request', expected_topic, expected_message,
content_type="application/json")
def test_semaphore_pull_request_non_gh_repo(self) -> None:
expected_topic = u"notifications/test-notifications"
expected_message = """
[Notifications](https://semaphore.semaphoreci.com/workflows/84383f37-d025-4811-b719-61c6acc92a1e) pipeline **failed**:
* **Pull Request**: Testing PR notifications (#3)
* **Branch**: test-notifications
* **Author**: radwo
""".strip()
with patch('zerver.webhooks.semaphore.view.is_github_repo', return_value=False):
self.send_and_test_stream_message('pull_request', expected_topic, expected_message,
content_type="application/json")
def test_semaphore_tag(self) -> None:
expected_topic = u"notifications"
expected_message = """
[Notifications](https://semaphore.semaphoreci.com/workflows/a8704319-2422-4828-9b11-6b2afa3554e6) pipeline **stopped**:
* **Tag**: [v1.0.1](https://github.com/renderedtext/notifications/tree/v1.0.1)
* **Author**: [radwo](https://github.com/radwo)
""".strip()
self.send_and_test_stream_message('tag', expected_topic, expected_message,
content_type="application/json")
def test_semaphore_tag_non_gh_repo(self) -> None:
expected_topic = u"notifications"
expected_message = """
[Notifications](https://semaphore.semaphoreci.com/workflows/a8704319-2422-4828-9b11-6b2afa3554e6) pipeline **stopped**:
* **Tag**: v1.0.1
* **Author**: radwo
""".strip()
with patch('zerver.webhooks.semaphore.view.is_github_repo', return_value=False):
self.send_and_test_stream_message('tag', expected_topic, expected_message,
content_type="application/json")
def test_semaphore_unknown_event(self) -> None:
expected_topic = u"notifications"
expected_message = """
[Notifications](https://semaphore.semaphoreci.com/workflows/a8704319-2422-4828-9b11-6b2afa3554e6) pipeline **stopped** for unknown event
""".strip()
with patch('zerver.webhooks.semaphore.tests.SemaphoreHookTests.get_body', self.get_unknown_event):
self.send_and_test_stream_message('tag', expected_topic, expected_message,
content_type="application/json")
def get_body(self, fixture_name: str) -> str:
return self.webhook_fixture_data("semaphore", fixture_name, file_type="json")
def get_unknown_event(self, fixture_name: str) -> str:
"""Return modified payload with revision.reference_type changed"""
fixture_data = ujson.loads(self.webhook_fixture_data("semaphore", fixture_name, file_type="json"))
fixture_data['revision']['reference_type'] = 'unknown'
return fixture_data

View File

@ -1,5 +1,7 @@
# Webhooks for external integrations.
from typing import Any, Dict
from typing import Any, Dict, Tuple, Optional
from urllib.parse import urlparse
from django.http import HttpRequest, HttpResponse
@ -9,6 +11,8 @@ from zerver.lib.response import json_success
from zerver.lib.webhooks.common import check_send_webhook_message
from zerver.models import UserProfile
# Semaphore Classic Templates
BUILD_TEMPLATE = """
[Build {build_number}]({build_url}) {status}:
* **Commit**: [{commit_hash}: {commit_message}]({commit_url})
@ -22,13 +26,83 @@ DEPLOY_TEMPLATE = """
* **Server**: {server_name}
""".strip()
# Semaphore 2.0 Templates
# Currently, Semaphore 2.0 only supports GitHub, while Semaphore Classic
# supports Bitbucket too. The payload does not have URLs for commits, tags,
# pull requests, etc. So, we use separate templates for GitHub and construct
# the URLs ourselves. For any other repository hosting services we use
# templates that don't have any links in them.
GH_PUSH_TEMPLATE = """
[{pipeline_name}]({workflow_url}) pipeline **{pipeline_result}**:
* **Commit**: [({commit_hash})]({commit_url}) {commit_message}
* **Branch**: {branch_name}
* **Author**: [{author_name}]({author_url})
""".strip()
PUSH_TEMPLATE = """
[{pipeline_name}]({workflow_url}) pipeline **{pipeline_result}**:
* **Commit**: ({commit_hash}) {commit_message}
* **Branch**: {branch_name}
* **Author**: {author_name}
""".strip()
GH_PULL_REQUEST_TEMPLATE = """
[{pipeline_name}]({workflow_url}) pipeline **{pipeline_result}**:
* **Pull Request**: [{pull_request_title}]({pull_request_url})
* **Branch**: {branch_name}
* **Author**: [{author_name}]({author_url})
""".strip()
PULL_REQUEST_TEMPLATE = """
[{pipeline_name}]({workflow_url}) pipeline **{pipeline_result}**:
* **Pull Request**: {pull_request_title} (#{pull_request_number})
* **Branch**: {branch_name}
* **Author**: {author_name}
""".strip()
GH_TAG_TEMPLATE = """
[{pipeline_name}]({workflow_url}) pipeline **{pipeline_result}**:
* **Tag**: [{tag_name}]({tag_url})
* **Author**: [{author_name}]({author_url})
""".strip()
TAG_TEMPLATE = """
[{pipeline_name}]({workflow_url}) pipeline **{pipeline_result}**:
* **Tag**: {tag_name}
* **Author**: {author_name}
""".strip()
DEFAULT_TEMPLATE = """
[{pipeline_name}]({workflow_url}) pipeline **{pipeline_result}** for {event_name} event
""".strip()
TOPIC_TEMPLATE = "{project}/{branch}"
GITHUB_URL_TEMPLATES = {
'commit': '{repo_url}/commit/{commit_id}',
'pull_request': '{repo_url}/pull/{pr_number}',
'tag': '{repo_url}/tree/{tag_name}',
'user': 'https://github.com/{username}',
}
@api_key_only_webhook_view('Semaphore')
@has_request_variables
def api_semaphore_webhook(request: HttpRequest, user_profile: UserProfile,
payload: Dict[str, Any]=REQ(argument_type='body')) -> HttpResponse:
content, project_name, branch_name = (
semaphore_classic(payload) if 'event' in payload else semaphore_2(payload)
)
subject = (
TOPIC_TEMPLATE.format(project=project_name, branch=branch_name) if branch_name else project_name
)
check_send_webhook_message(request, user_profile, subject, content)
return json_success()
def semaphore_classic(payload: Dict[str, Any]) -> Tuple[str, str, str]:
# semaphore only gives the last commit, even if there were multiple commits
# since the last build
branch_name = payload["branch_name"]
@ -76,10 +150,65 @@ def api_semaphore_webhook(request: HttpRequest, user_profile: UserProfile,
content = "{event}: {result}".format(
event=event, result=result)
subject = TOPIC_TEMPLATE.format(
project=project_name,
branch=branch_name
return content, project_name, branch_name
def semaphore_2(payload: Dict[str, Any]) -> Tuple[str, str, Optional[str]]:
repo_url = payload["repository"]["url"]
project_name = payload["project"]["name"]
organization_name = payload["organization"]["name"]
author_name = payload["revision"]["sender"]["login"]
workflow_id = payload['workflow']['id']
context = dict(
author_name=author_name,
author_url=GITHUB_URL_TEMPLATES['user'].format(repo_url=repo_url, username=author_name),
pipeline_name=payload["pipeline"]["name"],
pipeline_result=payload["pipeline"]["result"],
workflow_url='https://{org}.semaphoreci.com/workflows/{id}'.format(
org=organization_name, id=workflow_id)
)
check_send_webhook_message(request, user_profile, subject, content)
return json_success()
if payload["revision"]["reference_type"] == "branch": # push event
commit_id = payload["revision"]["commit_sha"]
branch_name = payload["revision"]["branch"]["name"]
context.update(
branch_name=branch_name,
commit_id=commit_id,
commit_hash=commit_id[:7],
commit_message=payload["revision"]["commit_message"],
commit_url=GITHUB_URL_TEMPLATES['commit'].format(repo_url=repo_url, commit_id=commit_id),
)
template = GH_PUSH_TEMPLATE if is_github_repo(repo_url) else PUSH_TEMPLATE
content = template.format(**context)
elif payload["revision"]["reference_type"] == "pull_request":
pull_request = payload["revision"]["pull_request"]
branch_name = pull_request["branch_name"]
pull_request_title = pull_request["name"]
pull_request_number = pull_request["number"]
pull_request_url = GITHUB_URL_TEMPLATES['pull_request'].format(
repo_url=repo_url, pr_number=pull_request_number)
context.update(
branch_name=branch_name,
pull_request_title=pull_request_title,
pull_request_url=pull_request_url,
pull_request_number=pull_request_number,
)
template = GH_PULL_REQUEST_TEMPLATE if is_github_repo(repo_url) else PULL_REQUEST_TEMPLATE
content = template.format(**context)
elif payload["revision"]["reference_type"] == "tag":
branch_name = ''
tag_name = payload["revision"]["tag"]["name"]
tag_url = GITHUB_URL_TEMPLATES['tag'].format(repo_url=repo_url, tag_name=tag_name)
context.update(
tag_name=tag_name,
tag_url=tag_url,
)
template = GH_TAG_TEMPLATE if is_github_repo(repo_url) else TAG_TEMPLATE
content = template.format(**context)
else: # should never get here: unknown event
branch_name = ''
context.update(event_name=payload["revision"]["reference_type"])
content = DEFAULT_TEMPLATE.format(**context)
return content, project_name, branch_name
def is_github_repo(repo_url: str) -> bool:
return urlparse(repo_url).hostname == 'github.com'