diff --git a/.gitlab/pipelines/daily-pipeline.yml b/.gitlab/pipelines/daily-pipeline.yml index 0fb2971cc7b35935619a88ecb3d62d2a7e8886a9..12ac64cdac2b13f62ed329de2284f041368dd4c3 100644 --- a/.gitlab/pipelines/daily-pipeline.yml +++ b/.gitlab/pipelines/daily-pipeline.yml @@ -25,6 +25,15 @@ include: - local: .gitlab/templates/platform-test.yml - local: .gitlab/templates/merge-report.yml - local: .gitlab/templates/test-count.yml + - local: .gitlab/templates/status-report.yml + +notify-success: + stage: .post + extends: .notify-success + +notify-failure: + stage: .post + extends: .notify-failure check-lint: extends: .check-lint diff --git a/.gitlab/pipelines/deployment-pipeline.yml b/.gitlab/pipelines/deployment-pipeline.yml index 61475b6e344faf41eaed3e14534fc44c8edfad77..8cc3bf6f8a739c33ce8cad13ed54edd3c77a1597 100644 --- a/.gitlab/pipelines/deployment-pipeline.yml +++ b/.gitlab/pipelines/deployment-pipeline.yml @@ -44,6 +44,14 @@ report-pipeline-failure: stage: .post extends: .report-pipeline-failure +notify-success: + stage: .post + extends: .notify-success + +notify-failure: + stage: .post + extends: .notify-failure + check-lint: extends: - .check-lint diff --git a/.gitlab/templates/setup-workspace.yml b/.gitlab/templates/setup-workspace.yml index 58117562ea8c000893d8ae59d2637ad9722d1a7f..146ba07c31154924c9832a6639671af5c8c80f2a 100644 --- a/.gitlab/templates/setup-workspace.yml +++ b/.gitlab/templates/setup-workspace.yml @@ -32,6 +32,11 @@ echo "CI_MERGE_REQUEST_DIFF_BASE_SHA=$CI_MERGE_REQUEST_DIFF_BASE_SHA" \ >> .env/workspace.env + + MR_TITLE=$(echo "$MR_INFO" | jq -r .title) + echo "EXTERNAL_MR_TITLE=$MR_TITLE" \ + >> .env/workspace.env + artifacts: reports: dotenv: .env/workspace.env diff --git a/.gitlab/templates/status-report.yml b/.gitlab/templates/status-report.yml index 8b402ae1c72da75f41b6b5b146a10319cccb26a2..09ca248033c505b8c81054f7a5fc57b1110c6461 100644 --- a/.gitlab/templates/status-report.yml +++ b/.gitlab/templates/status-report.yml @@ -49,3 +49,21 @@ when: on_success rules: - if: $FETCH_PUBLIC_MR == "true" + +.notify-success: + extends: .report-status + script: + - pip install requests + - python3 tools/pipeline_notification.py --status "success" + when: on_success + rules: + - if: '$PIPELINE_TYPE == "daily-pipeline" || $FETCH_PUBLIC_MR == "true"' + +.notify-failure: + extends: .report-status + script: + - pip install requests + - python3 tools/pipeline_notification.py --status "failure" + when: on_failure + rules: + - if: '$PIPELINE_TYPE == "daily-pipeline" || $FETCH_PUBLIC_MR == "true"' diff --git a/tools/config/slack_notifications/generate_slack_message.py b/tools/config/slack_notifications/generate_slack_message.py new file mode 100755 index 0000000000000000000000000000000000000000..6a79cc89c585dba08f01ea196491c5c7d28a4076 --- /dev/null +++ b/tools/config/slack_notifications/generate_slack_message.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +# +# Arm SCP/MCP Software +# Copyright (c) 2024, Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +from dataclasses import dataclass +from enum import Enum + + +@dataclass +class SlackMessageParams: + pipeline_type: str + job_status: str + project_path: str + merge_request_iid: str + merge_request_title: str + merge_request_event_user: str + pipeline_id: str + pipeline_iid: str + pipeline_duration: str + gitlab_user_name: str + public_repo: str + pipeline_repo: str + + +class JobStatus(Enum): + SUCCESS = ("#36A64F", "Passed") # Green + FAILURE = ("#E01E5A", "Failed") # Red + + @property + def color(self): + return self.value[0] + + @property + def status(self): + return self.value[1] + + +def generate_slack_message(params: SlackMessageParams): + job_status_enum = JobStatus[params.job_status.upper()] + + color = job_status_enum.color + status = job_status_enum.status + + if params.pipeline_type == "deployment-pipeline": + url = params.public_repo.removesuffix(".git") + clean_url = url.replace("git.", "", 1) + title = "*External MR Development Pipeline*" + mr_text = ( + f"<{clean_url}/-/merge_requests/{params.merge_request_iid}|" + f"MR Title: {params.merge_request_title}>" + ) + button_text = "View Pipeline" + button_url = params.pipeline_repo + elif params.pipeline_type == "daily-pipeline": + title = "*Daily Pipeline*" + mr_text = f"Pipeline: {params.pipeline_id}" + button_text = "View Results" + button_url = params.pipeline_repo + else: + print("*** No pipeline populated ***") + print(f"Pipeline_type: '{params.pipeline_type}'") + + message = { + "attachments": [ + { + "color": color, + "blocks": [ + { + "type": "section", + "text": + { + "type": "mrkdwn", + "text": title + }, + }, + { + "type": "section", + "text": + { + "type": "mrkdwn", + "text": mr_text + }, + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": f"*Status:*\n{status}" + }, + { + "type": "mrkdwn", + "text": ( + f"*Duration:*\n" + f"{params.pipeline_duration}" + ) + }, + ], + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": + { + "type": "plain_text", + "text": button_text, + "emoji": True + }, + "url": button_url, + } + ], + }, + ], + } + ] + } + # Only add who triggered the job if it's a deployement pipeline + if params.pipeline_type == "deployment-pipeline": + message["attachments"][0]["blocks"].append( + { + "type": "context", + "elements": [ + { + "type": "plain_text", + "text": ( + f"Triggered by " + f"{params.gitlab_user_name}" + ), + "emoji": True + }, + ], + } + ) + return message diff --git a/tools/pipeline_notification.py b/tools/pipeline_notification.py new file mode 100755 index 0000000000000000000000000000000000000000..d3253bd0dcd6d2f0ab4c28f2743e5ebd5327bc45 --- /dev/null +++ b/tools/pipeline_notification.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +# +# Arm SCP/MCP Software +# Copyright (c) 2024, Arm Limited and Contributors. All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause +# + +import argparse +import os +import requests +from datetime import datetime +from config.slack_notifications.generate_slack_message import ( + SlackMessageParams, + generate_slack_message +) + + +def post_to_slack(webhook_url, message): + headers = {'Content-Type': 'application/json'} + response = requests.post(webhook_url, json=message, headers=headers) + if response.status_code != 200: + raise ValueError(f"Request to Slack returned an \ + error {response.status_code}, \ + the response is:\n{response.text}") + + +def get_pipeline_duration(start_time_env_var): + start = os.getenv(start_time_env_var) + if start: + start_datetime = datetime.strptime(start, "%Y-%m-%dT%H:%M:%SZ") + duration_s = (datetime.utcnow() - start_datetime).seconds + return format_duration(duration_s) + return "N/A" + + +def format_duration(seconds): + hours, remainder = divmod(seconds, 3600) + minutes, seconds = divmod(remainder, 60) + # Format as a human-readable string for the slack message + if hours > 0: + return f"{hours}h {minutes}m {seconds}s" + elif minutes > 0: + return f"{minutes}m {seconds}s" + else: + return f"{seconds}s" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--status", required=True, help="Pipeline status: success or failure" + ) + args = parser.parse_args() + + duration = get_pipeline_duration("CI_PIPELINE_CREATED_AT") + + # Populate the environment variables for generating the message + params = SlackMessageParams( + pipeline_type=os.getenv("PIPELINE_TYPE"), + job_status=args.status, + project_path=os.getenv("CI_PROJECT_PATH"), + merge_request_iid=os.getenv("FETCH_PUBLIC_MR_NUMBER", ""), + merge_request_title=os.getenv("EXTERNAL_MR_TITLE", ""), + merge_request_event_user=os.getenv("CI_MERGE_REQUEST_EVENT_USER", ""), + pipeline_id=os.getenv("CI_PIPELINE_ID"), + pipeline_iid=os.getenv("CI_PIPELINE_IID"), + pipeline_duration=duration, + gitlab_user_name=os.getenv("GITLAB_USER_NAME"), + public_repo=os.getenv("PUBLIC_REPO_URL"), + pipeline_repo=os.getenv("CI_PIPELINE_URL"), + ) + slack_webhook_url = os.getenv("SLACK_HOOK_URL") + + slack_message = generate_slack_message(params) + post_to_slack(slack_webhook_url, slack_message)