diff --git a/static/images/integrations/gocd/001.png b/static/images/integrations/gocd/001.png index 5e86b36b33..7bdbd01058 100644 Binary files a/static/images/integrations/gocd/001.png and b/static/images/integrations/gocd/001.png differ diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py index a8274eb413..00f53293a1 100644 --- a/zerver/lib/integrations.py +++ b/zerver/lib/integrations.py @@ -765,7 +765,7 @@ DOC_SCREENSHOT_CONFIG: dict[str, list[BaseScreenshotConfig]] = { "github": [ScreenshotConfig("push__1_commit.json")], "githubsponsors": [ScreenshotConfig("created.json")], "gitlab": [ScreenshotConfig("push_hook__push_local_branch_without_commits.json")], - "gocd": [ScreenshotConfig("pipeline.json")], + "gocd": [ScreenshotConfig("pipeline_with_mixed_job_result.json")], "gogs": [ScreenshotConfig("pull_request__opened.json")], "gosquared": [ScreenshotConfig("traffic_spike.json", image_name="000.png")], "grafana": [ScreenshotConfig("alert_values_v11.json")], diff --git a/zerver/webhooks/gocd/doc.md b/zerver/webhooks/gocd/doc.md index d38cf915c7..29a6729488 100644 --- a/zerver/webhooks/gocd/doc.md +++ b/zerver/webhooks/gocd/doc.md @@ -1,24 +1,41 @@ -Zulip supports integration with GoCD and can notify you of -your build statuses. +# Zulip GoCD integration + +Get GoCD notifications in Zulip! + +{start_tabs} 1. {!create-channel.md!} 1. {!create-an-incoming-webhook.md!} -1. {!generate-integration-url.md!} +1. {!generate-webhook-url-basic.md!} -1. Add the following to your `Config.XML` file. +1. [Download][1] and [install][2] Sentry's **GoCD WebHook Notification + plugin**. - ``` - - - ... - - ``` + !!! warn "" - Push this change to your repository. For further information, - see [GoCD's documentation](https://docs.gocd.org/current/integration/). + **Note**: the GoCD WebHook Notification plugin will only send + webhook payloads over HTTPS. + +1. In your GoCD server, go to **Admin > Server Configuration > Plugins**, + and click on the gear icon beside the **GoCD WebHook Notification + plugin** that you installed. + +1. Set **WebHook URL** to the URL generated above, and click **Save**. + +{end_tabs} {!congrats.md!} ![](/static/images/integrations/gocd/001.png) + +### Related Branches + +- [GoCD plugin user guide][3] + +{!webhooks-url-specification.md!} + +[1]: https://github.com/getsentry/gocd-webhook-notification-plugin/releases +[2]: https://docs.gocd.org/current/extension_points/plugin_user_guide.html#installing-and-uninstalling-of-plugins +[3]: https://docs.gocd.org/current/extension_points/plugin_user_guide.html diff --git a/zerver/webhooks/gocd/fixtures/build_details.json b/zerver/webhooks/gocd/fixtures/build_details.json deleted file mode 100644 index b178832a96..0000000000 --- a/zerver/webhooks/gocd/fixtures/build_details.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "build_details": { - "_links": { - "job": { - "href": "https://ci.example.com/go/tab/build/detail/pipelineName/pipelineCounter/stageName/stageCounter/jobName" - }, - "stage": { - "href": "https://ci.example.com/go/pipelines/pipelineName/pipelineCounter/stageName/stageCounter" - }, - "pipeline": { - "href": "https://ci.example.com/go/tab/pipeline/history/pipelineName" - } - }, - "pipeline_name": "pipelineName", - "stage_name": "stageName", - "job_name": "jobName" - } -} diff --git a/zerver/webhooks/gocd/fixtures/pipeline.json b/zerver/webhooks/gocd/fixtures/pipeline.json deleted file mode 100644 index 9a5969aa0f..0000000000 --- a/zerver/webhooks/gocd/fixtures/pipeline.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "build_cause": { - "approver": "", - "material_revisions": [ - { - "modifications": [ - { - "email_address": null, - "id": 7225, - "modified_time": 1435728005000, - "user_name": "Balaji B ", - "comment": "my hola mundo changes", - "revision": "a788f1876e2e1f6e5a1e91006e75cd1d467a0edb" - } - ], - "material": { - "description": "URL: https://github.com/gocd/gocd, Branch: master", - "fingerprint": "61e2da369d0207a7ef61f326eed837f964471b35072340a03f8f55d993afe01d", - "type": "Git", - "id": 4 - }, - "changed": true - } - ], - "trigger_forced": false, - "trigger_message": "modified by Balaji " - }, - "name": "PipelineName", - "natural_order": 1, - "can_run": false, - "comment": null, - "stages": [ - { - "name": "stage1", - "approved_by": "changes", - "jobs": [ - { - "name": "jsunit", - "result": "Passed", - "state": "Completed", - "id": 1, - "scheduled_date": 1398332981981 - } - ], - "can_run": false, - "result": "Passed", - "approval_type": "success", - "counter": "1", - "id": 1, - "operate_permission": false, - "rerun_of_counter": null, - "scheduled": true - } - ], - "counter": 1, - "id": 1, - "preparing_to_schedule": false, - "label": "14.1.0.1-b14a81825d081411993853ea5ea45266ced578b4" -} diff --git a/zerver/webhooks/gocd/fixtures/pipeline_building.json b/zerver/webhooks/gocd/fixtures/pipeline_building.json new file mode 100644 index 0000000000..853c1aa030 --- /dev/null +++ b/zerver/webhooks/gocd/fixtures/pipeline_building.json @@ -0,0 +1,47 @@ +{ + "data": { + "pipeline": { + "name": "Pipeline", + "counter": "90", + "group": "defaultGroup", + "build-cause": [ + { + "material": { + "git-configuration": { + "shallow-clone": false, + "branch": "main", + "url": "https://github.com/swayam0322/Test" + }, + "type": "git" + }, + "changed": false, + "modifications": [ + { + "revision": "59f3c6e4540b6a89ad5505790e5efdf964e7b837", + "modified-time": "Jan 31, 2024, 1:56:14 AM", + "data": {} + } + ] + } + ], + "stage": { + "name": "Stage", + "counter": "1", + "approval-type": "success", + "approved-by": "anonymous", + "state": "Building", + "result": "Unknown", + "create-time": "Feb 1, 2024, 1:58:13 AM", + "jobs": [ + { + "name": "Job", + "schedule-time": "Feb 1, 2024, 1:58:13 AM", + "state": "Scheduled", + "result": "Unknown" + } + ] + } + } + }, + "type": "stage" +} diff --git a/zerver/webhooks/gocd/fixtures/pipeline_failed.json b/zerver/webhooks/gocd/fixtures/pipeline_failed.json index 17c33d94d6..63dae64ebf 100644 --- a/zerver/webhooks/gocd/fixtures/pipeline_failed.json +++ b/zerver/webhooks/gocd/fixtures/pipeline_failed.json @@ -1,59 +1,50 @@ { - "build_cause": { - "approver": "anonymous", - "material_revisions": [ - { - "modifications": [ + "data": { + "pipeline": { + "name": "pipeline-one", + "counter": "7", + "group": "defaultGroup", + "build-cause": [ { - "email_address": null, - "id": 1998, - "modified_time": 1434957613000, - "user_name": "User Name ", - "comment": "my hola mundo changes", - "revision": "f6e7a3899c55e1682ffb00383bdf8f882bcee2141e79a8728254190a1fddcf4f" + "material": { + "git-configuration": { + "shallow-clone": false, + "branch": "master", + "url": "https://github.com/PieterCK/getting-started-repo.git" + }, + "type": "git" + }, + "changed": false, + "modifications": [ + { + "revision": "963eb239c7b91eac2e15c727e06e908fd334e6f9", + "modified-time": "Dec 14, 2016, 5:09:14 AM", + "data": {} + } + ] } ], - "material": { - "description": "URL: https://github.com/gocd/gocd, Branch: master", - "fingerprint": "61e2da369d0207a7ef61f326eed837f964471b35072340a03f8f55d993afe01d", - "type": "Git", - "id": 4 - }, - "changed": true - } - ], - "trigger_forced": false, - "trigger_message": "modified by User Name " - }, - "name": "PipelineName", - "natural_order": 8, - "can_run": false, - "comment": null, - "stages": [ - { - "name": "stage1", - "approved_by": "changes", - "jobs": [ - { - "name": "job123", + "stage": { + "name": "stage-two", + "counter": "1", + "approval-type": "success", + "approved-by": "anonymous", + "state": "Failed", "result": "Failed", - "state": "Completed", - "id": 21, - "scheduled_date": 1436172201081 + "create-time": "Aug 28, 2024, 9:30:19 PM", + "last-transition-time": "Aug 28, 2024, 9:31:00 PM", + "jobs": [ + { + "name": "task-two", + "schedule-time": "Aug 28, 2024, 9:30:19 PM", + "complete-time": "Aug 28, 2024, 9:31:00 PM", + "state": "Completed", + "result": "Failed", + "agent-uuid": "a35cf64c-7746-46ae-ba65-597d70898d50" + } + ] } - ], - "can_run": false, - "result": "Failed", - "approval_type": "success", - "counter": "1", - "id": 21, - "operate_permission": false, - "rerun_of_counter": null, - "scheduled": true - } - ], - "counter": 1, - "id": 21, - "preparing_to_schedule": false, - "label": "6" -} + } + }, + "type": "stage" + } diff --git a/zerver/webhooks/gocd/fixtures/pipeline_passed.json b/zerver/webhooks/gocd/fixtures/pipeline_passed.json new file mode 100644 index 0000000000..8d94e8dd95 --- /dev/null +++ b/zerver/webhooks/gocd/fixtures/pipeline_passed.json @@ -0,0 +1,50 @@ +{ + "data": { + "pipeline": { + "name": "Pipeline", + "counter": "90", + "group": "defaultGroup", + "build-cause": [ + { + "material": { + "git-configuration": { + "shallow-clone": false, + "branch": "main", + "url": "https://github.com/swayam0322/Test" + }, + "type": "git" + }, + "changed": false, + "modifications": [ + { + "revision": "59f3c6e4540b6a89ad5505790e5efdf964e7b837", + "modified-time": "Jan 31, 2024, 1:56:14 AM", + "data": {} + } + ] + } + ], + "stage": { + "name": "Stage", + "counter": "1", + "approval-type": "success", + "approved-by": "anonymous", + "state": "Passed", + "result": "Passed", + "create-time": "Feb 1, 2024, 1:58:13 AM", + "last-transition-time": "Feb 1, 2024, 1:58:40 AM", + "jobs": [ + { + "name": "Job", + "schedule-time": "Feb 1, 2024, 1:58:13 AM", + "complete-time": "Feb 1, 2024, 1:58:40 AM", + "state": "Completed", + "result": "Passed", + "agent-uuid": "95dde0d9-8da7-48ae-8572-5c7de18bff88" + } + ] + } + } + }, + "type": "stage" +} diff --git a/zerver/webhooks/gocd/fixtures/pipeline_with_mixed_job_result.json b/zerver/webhooks/gocd/fixtures/pipeline_with_mixed_job_result.json new file mode 100644 index 0000000000..2ac2a6eefe --- /dev/null +++ b/zerver/webhooks/gocd/fixtures/pipeline_with_mixed_job_result.json @@ -0,0 +1,75 @@ + +{ + "data": { + "pipeline": { + "name": "test-pipeline", + "counter": "6", + "group": "defaultGroup", + "build-cause": [ + { + "material": { + "git-configuration": { + "shallow-clone": false, + "branch": "master", + "url": "https://github.com/PieterCK/getting-started-repo.git" + }, + "type": "git" + }, + "changed": false, + "modifications": [ + { + "revision": "963eb239c7b91eac2e15c727e06e908fd334e6f9", + "modified-time": "Dec 14, 2016, 5:09:14 AM", + "data": {} + } + ] + } + ], + "stage": { + "name": "backend-tests", + "counter": "1", + "approval-type": "success", + "approved-by": "changes", + "state": "Failed", + "result": "Failed", + "create-time": "Aug 29, 2024, 3:59:18 PM", + "last-transition-time": "Aug 29, 2024, 4:00:15 PM", + "jobs": [ + { + "name": "check-backend-lints", + "schedule-time": "Aug 29, 2024, 3:59:18 PM", + "complete-time": "Aug 29, 2024, 3:59:23 PM", + "state": "Completed", + "result": "Failed", + "agent-uuid": "a35cf64c-7746-46ae-ba65-597d70898d50" + }, + { + "name": "check-backend-tests", + "schedule-time": "Aug 29, 2024, 3:59:18 PM", + "complete-time": "Aug 29, 2024, 3:59:47 PM", + "state": "Completed", + "result": "Passed", + "agent-uuid": "a35cf64c-7746-46ae-ba65-597d70898d50" + }, + { + "name": "test-frontend-js", + "schedule-time": "Aug 29, 2024, 3:59:18 PM", + "complete-time": "Aug 29, 2024, 4:00:15 PM", + "state": "Completed", + "result": "Failed", + "agent-uuid": "a35cf64c-7746-46ae-ba65-597d70898d50" + }, + { + "name": "zulip-ci-debian-12", + "schedule-time": "Aug 29, 2024, 3:59:18 PM", + "complete-time": "Aug 29, 2024, 4:00:11 PM", + "state": "Completed", + "result": "Passed", + "agent-uuid": "a35cf64c-7746-46ae-ba65-597d70898d50" + } + ] + } + } + }, + "type": "stage" +} diff --git a/zerver/webhooks/gocd/tests.py b/zerver/webhooks/gocd/tests.py index 739be052dd..07f24af0d4 100644 --- a/zerver/webhooks/gocd/tests.py +++ b/zerver/webhooks/gocd/tests.py @@ -5,36 +5,44 @@ class GocdHookTests(WebhookTestCase): CHANNEL_NAME = "gocd" URL_TEMPLATE = "/api/v1/external/gocd?stream={stream}&api_key={api_key}" WEBHOOK_DIR_NAME = "gocd" - TOPIC_NAME = "https://github.com/gocd/gocd" - def test_gocd_message(self) -> None: - expected_message = ( - "Author: Balaji B \n" - "Build status: Passed :thumbs_up:\n" - "Details: [build log](https://ci.example.com" - "/go/tab/pipeline/history/pipelineName)\n" - "Comment: my hola mundo changes" - ) + def test_building_pipeline(self) -> None: + expected_topic = "Pipeline / Stage" + expected_message = """**Pipeline building**: Pipeline / Stage +- **Commit**: [`59f3c6e4540`](https://github.com/swayam0322/Test/commit/59f3c6e4540) on branch `main` +- **Started**: Feb 1, 2024, 1:58:13 AM""" self.check_webhook( - "pipeline", - self.TOPIC_NAME, + "pipeline_building", + expected_topic, expected_message, - content_type="application/x-www-form-urlencoded", ) - def test_failed_message(self) -> None: - expected_message = ( - "Author: User Name \n" - "Build status: Failed :thumbs_down:\n" - "Details: [build log](https://ci.example.com" - "/go/tab/pipeline/history/pipelineName)\n" - "Comment: my hola mundo changes" - ) + def test_completed_pipeline_success(self) -> None: + expected_topic = "Pipeline / Stage" + expected_message = """:green_circle: **Build passed**: Pipeline / Stage +- **Commit**: [`59f3c6e4540`](https://github.com/swayam0322/Test/commit/59f3c6e4540) on branch `main` +- **Started**: Feb 1, 2024, 1:58:13 AM +- **Finished**: Feb 1, 2024, 1:58:40 AM +- **Passed**: `Job`""" - self.check_webhook( - "pipeline_failed", - self.TOPIC_NAME, - expected_message, - content_type="application/x-www-form-urlencoded", - ) + self.check_webhook("pipeline_passed", expected_topic, expected_message) + + def test_completed_pipeline_fail(self) -> None: + expected_topic = "pipeline-one / stage-two" + expected_message = """:red_circle: **Build failed**: pipeline-one / stage-two +- **Commit**: [`963eb239c7b`](https://github.com/PieterCK/getting-started-repo.git/commit/963eb239c7b) on branch `master` +- **Started**: Aug 28, 2024, 9:30:19 PM +- **Finished**: Aug 28, 2024, 9:31:00 PM +- **Failed**: `task-two`""" + self.check_webhook("pipeline_failed", expected_topic, expected_message) + + def test_completed_pipeline_with_mixed_result(self) -> None: + expected_topic = "test-pipeline / backend-tests" + expected_message = """:red_circle: **Build failed**: test-pipeline / backend-tests +- **Commit**: [`963eb239c7b`](https://github.com/PieterCK/getting-started-repo.git/commit/963eb239c7b) on branch `master` +- **Started**: Aug 29, 2024, 3:59:18 PM +- **Finished**: Aug 29, 2024, 4:00:15 PM +- **Failed**: `check-backend-lints`, `test-frontend-js` +- **Passed**: `check-backend-tests`, `zulip-ci-debian-12`""" + self.check_webhook("pipeline_with_mixed_job_result", expected_topic, expected_message) diff --git a/zerver/webhooks/gocd/view.py b/zerver/webhooks/gocd/view.py index a6c49b3acd..9364af0133 100644 --- a/zerver/webhooks/gocd/view.py +++ b/zerver/webhooks/gocd/view.py @@ -1,6 +1,6 @@ # Webhooks for external integrations. -import json -import os + +from collections import defaultdict from django.http import HttpRequest, HttpResponse @@ -9,13 +9,24 @@ from zerver.lib.response import json_success from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint from zerver.lib.validator import WildValue, check_string from zerver.lib.webhooks.common import check_send_webhook_message +from zerver.lib.webhooks.git import get_short_sha from zerver.models import UserProfile -MESSAGE_TEMPLATE = """\ -Author: {} -Build status: {} {} -Details: [build log]({}) -Comment: {}""" +COMMIT_INFO_TEMPLATE = """[`{commit_details}`]({commit_link}) on branch `{branch_name}`""" +TOPIC_TEMPLATE = "{pipeline} / {stage}" + +SCHEDULED_BODY_TEMPLATE = """ +**Pipeline {status}**: {pipeline} / {stage} +- **Commit**: {commit_details} +- **Started**: {start_time} +""" + +COMPLETED_BODY_TEMPLATE = """ +{emoji} **Build {status}**: {pipeline} / {stage} +- **Commit**: {commit_details} +- **Started**: {start_time} +- **Finished**: {end_time} +""" @webhook_view("Gocd") @@ -26,31 +37,75 @@ def api_gocd_webhook( *, payload: JsonBodyPayload[WildValue], ) -> HttpResponse: - modifications = payload["build_cause"]["material_revisions"][0]["modifications"][0] - result = payload["stages"][0]["result"].tame(check_string) - material = payload["build_cause"]["material_revisions"][0]["material"] - - if result == "Passed": - emoji = ":thumbs_up:" - elif result == "Failed": - emoji = ":thumbs_down:" - - build_details_file = os.path.join(os.path.dirname(__file__), "fixtures/build_details.json") - - with open(build_details_file) as f: - contents = json.load(f) - build_link = contents["build_details"]["_links"]["pipeline"]["href"] - - body = MESSAGE_TEMPLATE.format( - modifications["user_name"].tame(check_string), - result, - emoji, - build_link, - modifications["comment"].tame(check_string), - ) - branch = material["description"].tame(check_string).split(",") - topic_name = branch[0].split(" ")[1] - - check_send_webhook_message(request, user_profile, topic_name, body) - + type = payload["type"].tame(check_string) + if type == "stage": + body = get_body(payload) + topic_name = get_topic(payload) + check_send_webhook_message(request, user_profile, topic_name, body) return json_success(request) + + +def get_topic(payload: WildValue) -> str: + return TOPIC_TEMPLATE.format( + pipeline=payload["data"]["pipeline"]["name"].tame(check_string), + stage=payload["data"]["pipeline"]["stage"]["name"].tame(check_string), + ) + + +def get_commit_details(payload: WildValue) -> str: + build = payload["data"]["pipeline"]["build-cause"][0] + material = build["material"] + url_base = material["git-configuration"]["url"].tame(check_string) + revision = build["modifications"][0]["revision"].tame(check_string) + commit_sha = get_short_sha(revision) + url = f"{url_base}/commit/{commit_sha}" + branch = material["git-configuration"]["branch"].tame(check_string) + return COMMIT_INFO_TEMPLATE.format( + commit_details=commit_sha, + commit_link=url, + branch_name=branch, + ) + + +def get_jobs_details(pipeline_data: WildValue) -> str: + job_dict_list = pipeline_data["stage"]["jobs"] + formatted_job_dict = defaultdict(list) + job_details_template = "" + + for job in job_dict_list: + job_name = job["name"].tame(check_string) + job_result = job["result"].tame(check_string) + formatted_job_dict[job_result].append(f"`{job_name}`") + + for key in formatted_job_dict: + formatted_job_list = ", ".join(formatted_job_dict[key]) + job_details_template += f"- **{key}**: {formatted_job_list}\n" + + return job_details_template + + +def get_body(payload: WildValue) -> str: + pipeline_data = payload["data"]["pipeline"] + body_details = { + "commit_details": get_commit_details(payload), + "status": pipeline_data["stage"]["state"].tame(check_string).lower(), + "pipeline": pipeline_data["name"].tame(check_string), + "stage": pipeline_data["stage"]["name"].tame(check_string), + "start_time": pipeline_data["stage"]["create-time"].tame(check_string), + } + + if body_details["status"] == "building": + return SCHEDULED_BODY_TEMPLATE.format(**body_details) + + result = pipeline_data["stage"]["result"].tame(check_string) + body_details.update( + { + "result": result, + "emoji": ":green_circle:" if result == "Passed" else ":red_circle:", + "end_time": pipeline_data["stage"]["last-transition-time"].tame(check_string), + } + ) + body = COMPLETED_BODY_TEMPLATE.format(**body_details) + + body += get_jobs_details(pipeline_data) + return body