Repository: zulip/zulip

Issue:
integrations: Add Sonar Qube.
Suggest adding Sonar Qube integrations for real-time notifications of code coverage, `#` of bugs ant etc.. I can also work on this since I have made a PR adding grafana integrations.

Sonar Qube webhook
https://docs.sonarqube.org/latest/project-administration/webhooks/

instructions for adding a new webhook integration
https://zulipchat.com/api/incoming-webhooks-overview


Discussion:
Feel free to work on adding an integration for it, thanks @Zhenye-Na!
Hello @zulip/server-integrations members, this issue was labeled with the "area: integrations" label, so you may want to check it out!

<!-- areaLabelAddition -->
Thank you for reaching out, @timabbott !

Definitely would love to make contributions to this project. Could the core developing team or reviewers review my last PR concerning grafana integrations?

Just wanna make sure the last PR is on the right way so that I can follow the same procedure on this issue.

Thanks!

Yeah, can you post a link?
of course, why not!

here is the link: https://github.com/zulip/zulip/pull/13384
Hi @Zhenye-Na are you working on this or planning to? If not can I take this issue?  
Go ahead, checkout my working branch and make the changes. Thanks!


On Dec 4, 2019, at 01:28, kostekIV <notifications@github.com> wrote:

﻿

Hi @Zhenye-Na<https://github.com/Zhenye-Na> are you working on this or planning to? If not can I take this issue?

—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub<https://github.com/zulip/zulip/issues/13395?email_source=notifications&email_token=AHWBFZLNMZPAN4FQMBSZFUTQW5Z4BA5CNFSM4JMFWFEKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEF4J6QA#issuecomment-561553216>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AHWBFZL2MXQHQ2YQY6Z6G3TQW5Z4BANCNFSM4JMFWFEA>.

Thank you, @zulipbot claim 
Hello @kostekIV, you have been unassigned from this issue because you have not updated this issue or any referenced pull requests for over 14 days.

You can reclaim this issue or claim any other issue by commenting `@zulipbot claim` on that issue.

Thanks for your contributions, and hope to see you again soon!
@kostekIV do you wish to continue your PR or I can take it up.

Resolving patch:
diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py
--- a/zerver/lib/integrations.py
+++ b/zerver/lib/integrations.py
@@ -444,6 +444,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
     ),
     WebhookIntegration("slack", ["communication"]),
     WebhookIntegration("solano", ["continuous-integration"], display_name="Solano Labs"),
+    WebhookIntegration("sonarqube", ["continuous-integration"], display_name="SonarQube"),
     WebhookIntegration("sonarr", ["entertainment"], display_name="Sonarr"),
     WebhookIntegration("splunk", ["monitoring"], display_name="Splunk"),
     WebhookIntegration("statuspage", ["customer-support"], display_name="Statuspage"),
@@ -780,6 +781,7 @@ def __init__(self, name: str, *args: Any, **kwargs: Any) -> None:
     ],
     "slack": [ScreenshotConfig("message_info.txt")],
     "solano": [ScreenshotConfig("build_001.json")],
+    "sonarqube": [ScreenshotConfig("error.json")],
     "sonarr": [ScreenshotConfig("sonarr_episode_grabbed.json")],
     "splunk": [ScreenshotConfig("search_one_result.json")],
     "statuspage": [ScreenshotConfig("incident_created.json")],
diff --git a/zerver/webhooks/sonarqube/__init__.py b/zerver/webhooks/sonarqube/__init__.py
new file mode 100644
diff --git a/zerver/webhooks/sonarqube/view.py b/zerver/webhooks/sonarqube/view.py
new file mode 100644
--- /dev/null
+++ b/zerver/webhooks/sonarqube/view.py
@@ -0,0 +1,132 @@
+# Webhooks for external integrations.
+
+from typing import Any, Dict, List, Mapping
+
+from django.http import HttpRequest, HttpResponse
+
+from zerver.decorator import webhook_view
+from zerver.lib.request import REQ, has_request_variables
+from zerver.lib.response import json_success
+from zerver.lib.webhooks.common import check_send_webhook_message
+from zerver.models import UserProfile
+
+TOPIC_WITH_BRANCH = "{} / {}"
+
+MESSAGE_WITH_BRANCH_AND_CONDITIONS = "Project [{}]({}) analysis of branch {} resulted in {}:\n"
+MESSAGE_WITH_BRANCH_AND_WITHOUT_CONDITIONS = (
+    "Project [{}]({}) analysis of branch {} resulted in {}."
+)
+MESSAGE_WITHOUT_BRANCH_AND_WITH_CONDITIONS = "Project [{}]({}) analysis resulted in {}:\n"
+MESSAGE_WITHOUT_BRANCH_AND_CONDITIONS = "Project [{}]({}) analysis resulted in {}."
+
+INVERSE_OPERATORS = {
+    "WORSE_THAN": "should be better or equal to",
+    "GREATER_THAN": "should be less than or equal to",
+    "LESS_THAN": "should be greater than or equal to",
+}
+
+TEMPLATES = {
+    "default": "* {}: **{}** {} {} {}.",
+    "no_value": "* {}: **{}**.",
+}
+
+
+def parse_metric_name(metric_name: str) -> str:
+    return " ".join(metric_name.split("_"))
+
+
+def parse_condition(condition: Mapping[str, Any]) -> str:
+    metric = condition["metric"]
+
+    metric_name = parse_metric_name(metric)
+    operator = condition["operator"]
+    operator = INVERSE_OPERATORS.get(operator, operator)
+    value = condition.get("value", "no value")
+    status = condition["status"].lower()
+    threshold = condition["errorThreshold"]
+
+    if value == "no value":
+        return TEMPLATES["no_value"].format(metric_name, status)
+
+    template = TEMPLATES["default"]
+
+    return template.format(metric_name, status, value, operator, threshold)
+
+
+def parse_conditions(conditions: List[Mapping[str, Any]]) -> str:
+    return "\n".join(
+        [
+            parse_condition(condition)
+            for condition in conditions
+            if condition["status"].lower() != "ok" and condition["status"].lower() != "no_value"
+        ]
+    )
+
+
+def render_body_with_branch(payload: Mapping[str, Any]) -> str:
+    project_name = payload["project"]["name"]
+    project_url = payload["project"]["url"]
+    quality_gate_status = payload["qualityGate"]["status"].lower()
+    if quality_gate_status == "ok":
+        quality_gate_status = "success"
+    else:
+        quality_gate_status = "error"
+    branch = payload["branch"]["name"]
+
+    conditions = payload["qualityGate"]["conditions"]
+    conditions = parse_conditions(conditions)
+
+    if not conditions:
+        return MESSAGE_WITH_BRANCH_AND_WITHOUT_CONDITIONS.format(
+            project_name, project_url, branch, quality_gate_status
+        )
+    msg = MESSAGE_WITH_BRANCH_AND_CONDITIONS.format(
+        project_name, project_url, branch, quality_gate_status
+    )
+    msg += conditions
+
+    return msg
+
+
+def render_body_without_branch(payload: Mapping[str, Any]) -> str:
+    project_name = payload["project"]["name"]
+    project_url = payload["project"]["url"]
+    quality_gate_status = payload["qualityGate"]["status"].lower()
+    if quality_gate_status == "ok":
+        quality_gate_status = "success"
+    else:
+        quality_gate_status = "error"
+    conditions = payload["qualityGate"]["conditions"]
+    conditions = parse_conditions(conditions)
+
+    if not conditions:
+        return MESSAGE_WITHOUT_BRANCH_AND_CONDITIONS.format(
+            project_name, project_url, quality_gate_status
+        )
+    msg = MESSAGE_WITHOUT_BRANCH_AND_WITH_CONDITIONS.format(
+        project_name, project_url, quality_gate_status
+    )
+    msg += conditions
+
+    return msg
+
+
+@webhook_view("Sonarqube")
+@has_request_variables
+def api_sonarqube_webhook(
+    request: HttpRequest,
+    user_profile: UserProfile,
+    payload: Dict[str, Any] = REQ(argument_type="body"),
+) -> HttpResponse:
+    project = payload["project"]["name"]
+    branch = None
+    if "branch" in payload.keys():
+        branch = payload["branch"].get("name", None)
+    if branch:
+        topic = TOPIC_WITH_BRANCH.format(project, branch)
+        message = render_body_with_branch(payload)
+    else:
+        topic = project
+        message = render_body_without_branch(payload)
+    check_send_webhook_message(request, user_profile, topic, message)
+    return json_success()


Test patch:
diff --git a/zerver/webhooks/sonarqube/tests.py b/zerver/webhooks/sonarqube/tests.py
new file mode 100644
--- /dev/null
+++ b/zerver/webhooks/sonarqube/tests.py
@@ -0,0 +1,84 @@
+from zerver.lib.test_classes import WebhookTestCase
+
+
+class SonarqubeHookTests(WebhookTestCase):
+    STREAM_NAME = "SonarQube"
+    URL_TEMPLATE = "/api/v1/external/sonarqube?api_key={api_key}&stream={stream}"
+    FIXTURE_DIR_NAME = "sonarqube"
+    WEBHOOK_DIR_NAME = "sonarqube"
+
+    def test_analysis_success(self) -> None:
+        expected_topic = "test-sonar / master"
+
+        expected_message = """
+Project [test-sonar](http://localhost:9000/dashboard?id=test-sonar) analysis of branch master resulted in success.
+        """.strip()
+
+        self.check_webhook(
+            "success",
+            expected_topic,
+            expected_message,
+            content_type="application/x-www-form-urlencoded",
+        )
+
+    def test_analysis_error(self) -> None:
+        expected_topic = "test-sonar / master"
+
+        expected_message = """
+Project [test-sonar](http://localhost:9000/dashboard?id=test-sonar) analysis of branch master resulted in error:
+* coverage: **error** 0.0 should be greater than or equal to 80.
+* duplicated lines density: **error** 89.39828080229226 should be less than or equal to 3.
+        """.strip()
+
+        self.check_webhook(
+            "error",
+            expected_topic,
+            expected_message,
+            content_type="application/x-www-form-urlencoded",
+        )
+
+    def test_analysis_error_no_value(self) -> None:
+        expected_topic = "test-sonar / master"
+
+        expected_message = """
+Project [test-sonar](http://localhost:9000/dashboard?id=test-sonar) analysis of branch master resulted in error:
+* coverage: **error** 0.0 should be greater than or equal to 80.
+* duplicated lines density: **error**.
+        """.strip()
+
+        self.check_webhook(
+            "error_no_value",
+            expected_topic,
+            expected_message,
+            content_type="application/x-www-form-urlencoded",
+        )
+
+    def test_analysis_success_no_branch(self) -> None:
+        expected_topic = "test-sonar"
+
+        expected_message = """
+Project [test-sonar](http://localhost:9000/dashboard?id=test-sonar) analysis resulted in success.
+        """.strip()
+
+        self.check_webhook(
+            "success_no_branch",
+            expected_topic,
+            expected_message,
+            content_type="application/x-www-form-urlencoded",
+        )
+
+    def test_analysis_error_no_branch(self) -> None:
+        expected_topic = "test-sonar"
+
+        expected_message = """
+Project [test-sonar](http://localhost:9000/dashboard?id=test-sonar) analysis resulted in error:
+* coverage: **error** 0.0 should be greater than or equal to 80.
+* duplicated lines density: **error** 89.39828080229226 should be less than or equal to 3.
+        """.strip()
+
+        self.check_webhook(
+            "error_no_branch",
+            expected_topic,
+            expected_message,
+            content_type="application/x-www-form-urlencoded",
+        )