2024-07-22 13:46:27 +00:00
|
|
|
import argparse
|
2024-07-11 11:37:26 +00:00
|
|
|
import json
|
|
|
|
import os
|
2024-10-17 15:06:42 +00:00
|
|
|
from typing import Dict, List, Union
|
2024-07-11 11:37:26 +00:00
|
|
|
|
|
|
|
import boto3
|
|
|
|
import requests
|
|
|
|
from botocore.exceptions import ClientError
|
|
|
|
|
2024-08-02 07:23:40 +00:00
|
|
|
from ci_config import CI
|
2024-08-06 14:55:04 +00:00
|
|
|
from ci_utils import WithIter
|
2024-10-17 15:06:42 +00:00
|
|
|
from commit_status_helper import get_commit_filtered_statuses, get_repo
|
|
|
|
from get_robot_token import get_best_robot_token
|
|
|
|
from github_helper import GitHub
|
|
|
|
from pr_info import PRInfo
|
2024-08-06 14:55:04 +00:00
|
|
|
|
|
|
|
|
|
|
|
class Channels(metaclass=WithIter):
|
|
|
|
# Channel names must match json keys in ParameterStore
|
|
|
|
ALERTS = "alerts-channel"
|
|
|
|
INFO = "info-channel"
|
|
|
|
DRY_RUN = "dry-ryn-channel"
|
|
|
|
DEFAULT = "default"
|
2024-07-11 11:37:26 +00:00
|
|
|
|
|
|
|
|
|
|
|
class CIBuddy:
|
2024-08-06 14:55:04 +00:00
|
|
|
Channels = Channels
|
2024-07-11 11:37:26 +00:00
|
|
|
_HEADERS = {"Content-Type": "application/json"}
|
|
|
|
|
|
|
|
def __init__(self, dry_run=False):
|
|
|
|
self.repo = os.getenv("GITHUB_REPOSITORY", "")
|
|
|
|
self.dry_run = dry_run
|
|
|
|
res = self._get_webhooks()
|
2024-08-06 14:55:04 +00:00
|
|
|
self.channels = {}
|
2024-07-11 11:37:26 +00:00
|
|
|
if res:
|
2024-08-06 14:55:04 +00:00
|
|
|
channels = json.loads(res)
|
|
|
|
for channel in Channels:
|
|
|
|
if channel in channels:
|
|
|
|
self.channels[channel] = channels[channel]
|
|
|
|
|
|
|
|
for channel in Channels:
|
|
|
|
if channel not in self.channels:
|
|
|
|
if Channels.DEFAULT in self.channels:
|
|
|
|
print(
|
|
|
|
f"ERROR: missing config for channel [{channel}] - will use default channel instead"
|
|
|
|
)
|
|
|
|
self.channels[channel] = self.channels[Channels.DEFAULT]
|
|
|
|
else:
|
|
|
|
print(
|
|
|
|
f"ERROR: missing config for channel [{channel}] - will disable notification"
|
|
|
|
)
|
|
|
|
self.channels[channel] = ""
|
2024-07-11 11:37:26 +00:00
|
|
|
self.job_name = os.getenv("CHECK_NAME", "unknown")
|
|
|
|
pr_info = PRInfo()
|
|
|
|
self.pr_number = pr_info.number
|
|
|
|
self.head_ref = pr_info.head_ref
|
|
|
|
self.commit_url = pr_info.commit_html_url
|
2024-10-17 15:06:42 +00:00
|
|
|
self.sha_full = pr_info.sha
|
|
|
|
self.sha = self.sha_full[:10]
|
2024-07-11 11:37:26 +00:00
|
|
|
|
2024-07-22 13:46:27 +00:00
|
|
|
def check_workflow(self):
|
2024-08-02 07:23:40 +00:00
|
|
|
CI.GH.print_workflow_results()
|
|
|
|
if CI.Envs.GITHUB_WORKFLOW == CI.WorkFlowNames.CreateRelease:
|
|
|
|
if not CI.GH.is_workflow_ok():
|
|
|
|
self.post_job_error(
|
|
|
|
f"{CI.Envs.GITHUB_WORKFLOW} Workflow Failed", critical=True
|
|
|
|
)
|
2024-10-17 15:06:42 +00:00
|
|
|
return
|
|
|
|
|
|
|
|
res = CI.GH.get_workflow_job_result(CI.GH.ActionsNames.RunConfig)
|
|
|
|
if res == CI.GH.ActionStatuses.SUCCESS:
|
|
|
|
# the normal case
|
|
|
|
return
|
|
|
|
|
|
|
|
gh = GitHub(get_best_robot_token())
|
|
|
|
commit = get_repo(gh).get_commit(self.sha_full)
|
|
|
|
statuses = get_commit_filtered_statuses(commit)
|
|
|
|
if any(True for st in statuses if st.context == CI.StatusNames.PR_CHECK):
|
|
|
|
print(
|
|
|
|
f"INFO: RunConfig status is [{res}], but it "
|
|
|
|
f'contains "{CI.StatusNames.PR_CHECK}" status, do not report error'
|
|
|
|
)
|
|
|
|
return
|
|
|
|
|
|
|
|
print(f"ERROR: RunConfig status is [{res}] - post report to slack")
|
|
|
|
self.post_job_error(f"{CI.Envs.GITHUB_WORKFLOW} Workflow Failed", critical=True)
|
2024-07-22 13:46:27 +00:00
|
|
|
|
2024-07-11 11:37:26 +00:00
|
|
|
@staticmethod
|
|
|
|
def _get_webhooks():
|
|
|
|
name = "ci_buddy_web_hooks"
|
|
|
|
|
|
|
|
session = boto3.Session(region_name="us-east-1") # Replace with your region
|
|
|
|
ssm_client = session.client("ssm")
|
|
|
|
json_string = None
|
|
|
|
try:
|
|
|
|
response = ssm_client.get_parameter(
|
|
|
|
Name=name,
|
|
|
|
WithDecryption=True, # Set to True if the parameter is a SecureString
|
|
|
|
)
|
|
|
|
json_string = response["Parameter"]["Value"]
|
|
|
|
except ClientError as e:
|
|
|
|
print(f"An error occurred: {e}")
|
|
|
|
|
|
|
|
return json_string
|
|
|
|
|
2024-08-06 14:55:04 +00:00
|
|
|
def post(self, message: str, channels: List[str]) -> None:
|
|
|
|
print(f"Posting slack message, dry_run [{self.dry_run}]")
|
|
|
|
if self.dry_run:
|
|
|
|
urls = [self.channels[Channels.DRY_RUN]]
|
2024-07-11 11:37:26 +00:00
|
|
|
else:
|
2024-08-06 14:55:04 +00:00
|
|
|
urls = []
|
|
|
|
for channel in channels:
|
|
|
|
url = self.channels[channel]
|
|
|
|
if url:
|
|
|
|
urls.append(url)
|
|
|
|
else:
|
|
|
|
print(f"WARNING: no channel config for [{channel}] - skip")
|
2024-07-11 11:37:26 +00:00
|
|
|
data = {"text": message}
|
|
|
|
try:
|
2024-08-06 14:55:04 +00:00
|
|
|
for url in urls:
|
|
|
|
requests.post(
|
|
|
|
url, headers=self._HEADERS, data=json.dumps(data), timeout=10
|
|
|
|
)
|
2024-07-11 11:37:26 +00:00
|
|
|
except Exception as e:
|
|
|
|
print(f"ERROR: Failed to post message, ex {e}")
|
|
|
|
|
2024-07-16 15:07:49 +00:00
|
|
|
def _post_formatted(
|
2024-08-06 14:55:04 +00:00
|
|
|
self,
|
|
|
|
title: str,
|
|
|
|
body: Union[Dict, str],
|
|
|
|
with_wf_link: bool,
|
|
|
|
channels: Union[List[str], str],
|
2024-07-16 15:07:49 +00:00
|
|
|
) -> None:
|
|
|
|
message = title
|
|
|
|
if isinstance(body, dict):
|
|
|
|
for name, value in body.items():
|
2024-08-02 07:23:40 +00:00
|
|
|
if "sha" in name and value and len(value) == 40:
|
2024-07-16 15:07:49 +00:00
|
|
|
value = (
|
|
|
|
f"<https://github.com/{self.repo}/commit/{value}|{value[:8]}>"
|
|
|
|
)
|
2024-08-02 07:23:40 +00:00
|
|
|
elif isinstance(value, str) and value.startswith("https://github.com/"):
|
|
|
|
value_shorten = value.split("/")[-1]
|
|
|
|
value = f"<{value}|{value_shorten}>"
|
2024-07-16 15:07:49 +00:00
|
|
|
message += f" *{name}*: {value}\n"
|
|
|
|
else:
|
|
|
|
message += body + "\n"
|
|
|
|
run_id = os.getenv("GITHUB_RUN_ID", "")
|
|
|
|
if with_wf_link and run_id:
|
|
|
|
message += f" *workflow*: <https://github.com/{self.repo}/actions/runs/{run_id}|{run_id}>\n"
|
2024-08-06 14:55:04 +00:00
|
|
|
self.post(
|
|
|
|
message, channels=[channels] if isinstance(channels, str) else channels
|
|
|
|
)
|
2024-07-16 15:07:49 +00:00
|
|
|
|
|
|
|
def post_info(
|
2024-08-06 14:55:04 +00:00
|
|
|
self,
|
|
|
|
title: str,
|
|
|
|
body: Union[Dict, str],
|
|
|
|
with_wf_link: bool = True,
|
|
|
|
channels: Union[List[str], str] = Channels.INFO,
|
2024-07-16 15:07:49 +00:00
|
|
|
) -> None:
|
|
|
|
title_extended = f":white_circle: *{title}*\n\n"
|
2024-08-06 14:55:04 +00:00
|
|
|
self._post_formatted(title_extended, body, with_wf_link, channels=channels)
|
2024-07-16 15:07:49 +00:00
|
|
|
|
|
|
|
def post_done(
|
2024-08-06 14:55:04 +00:00
|
|
|
self,
|
|
|
|
title: str,
|
|
|
|
body: Union[Dict, str],
|
|
|
|
with_wf_link: bool = True,
|
|
|
|
channels: Union[List[str], str] = Channels.INFO,
|
2024-07-16 15:07:49 +00:00
|
|
|
) -> None:
|
|
|
|
title_extended = f":white_check_mark: *{title}*\n\n"
|
2024-08-06 14:55:04 +00:00
|
|
|
self._post_formatted(title_extended, body, with_wf_link, channels=channels)
|
2024-07-16 15:07:49 +00:00
|
|
|
|
|
|
|
def post_warning(
|
2024-08-06 14:55:04 +00:00
|
|
|
self,
|
|
|
|
title: str,
|
|
|
|
body: Union[Dict, str],
|
|
|
|
with_wf_link: bool = True,
|
|
|
|
channels: Union[List[str], str] = Channels.ALERTS,
|
2024-07-16 15:07:49 +00:00
|
|
|
) -> None:
|
|
|
|
title_extended = f":warning: *{title}*\n\n"
|
2024-08-06 14:55:04 +00:00
|
|
|
self._post_formatted(title_extended, body, with_wf_link, channels=channels)
|
2024-07-16 15:07:49 +00:00
|
|
|
|
|
|
|
def post_critical(
|
2024-08-06 14:55:04 +00:00
|
|
|
self,
|
|
|
|
title: str,
|
|
|
|
body: Union[Dict, str],
|
|
|
|
with_wf_link: bool = True,
|
|
|
|
channels: Union[List[str], str] = Channels.ALERTS,
|
2024-07-16 15:07:49 +00:00
|
|
|
) -> None:
|
|
|
|
title_extended = f":black_circle: *{title}*\n\n"
|
2024-08-06 14:55:04 +00:00
|
|
|
self._post_formatted(title_extended, body, with_wf_link, channels=channels)
|
2024-07-16 15:07:49 +00:00
|
|
|
|
|
|
|
def post_job_error(
|
|
|
|
self,
|
2024-07-19 09:35:43 +00:00
|
|
|
error_description: str,
|
|
|
|
job_name: str = "",
|
|
|
|
with_instance_info: bool = True,
|
2024-07-16 15:07:49 +00:00
|
|
|
with_wf_link: bool = True,
|
2024-07-19 18:43:14 +00:00
|
|
|
critical: bool = False,
|
2024-08-06 14:55:04 +00:00
|
|
|
channel: Union[List[str], str] = Channels.ALERTS,
|
2024-07-19 09:35:43 +00:00
|
|
|
) -> None:
|
2024-07-11 11:37:26 +00:00
|
|
|
instance_id, instance_type = "unknown", "unknown"
|
|
|
|
if with_instance_info:
|
2024-08-02 07:23:40 +00:00
|
|
|
instance_id = (
|
|
|
|
CI.Shell.get_output("ec2metadata --instance-id") or instance_id
|
|
|
|
)
|
2024-08-01 09:57:54 +00:00
|
|
|
instance_type = (
|
2024-08-02 07:23:40 +00:00
|
|
|
CI.Shell.get_output("ec2metadata --instance-type") or instance_type
|
2024-08-01 09:57:54 +00:00
|
|
|
)
|
2024-07-11 11:37:26 +00:00
|
|
|
if not job_name:
|
|
|
|
job_name = os.getenv("CHECK_NAME", "unknown")
|
2024-07-19 18:43:14 +00:00
|
|
|
sign = ":red_circle:" if not critical else ":black_circle:"
|
|
|
|
line_err = f"{sign} *Error: {error_description}*\n\n"
|
2024-07-12 08:14:41 +00:00
|
|
|
line_ghr = f" *Runner:* `{instance_type}`, `{instance_id}`\n"
|
|
|
|
line_job = f" *Job:* `{job_name}`\n"
|
2024-07-15 12:14:44 +00:00
|
|
|
line_pr_ = f" *PR:* <https://github.com/{self.repo}/pull/{self.pr_number}|#{self.pr_number}>, <{self.commit_url}|{self.sha}>\n"
|
2024-07-15 12:39:29 +00:00
|
|
|
line_br_ = (
|
|
|
|
f" *Branch:* `{self.head_ref}`, <{self.commit_url}|{self.sha}>\n"
|
|
|
|
)
|
2024-07-11 11:37:26 +00:00
|
|
|
message = line_err
|
|
|
|
message += line_job
|
|
|
|
if with_instance_info:
|
|
|
|
message += line_ghr
|
|
|
|
if self.pr_number > 0:
|
|
|
|
message += line_pr_
|
|
|
|
else:
|
|
|
|
message += line_br_
|
2024-07-16 15:07:49 +00:00
|
|
|
run_id = os.getenv("GITHUB_RUN_ID", "")
|
|
|
|
if with_wf_link and run_id:
|
|
|
|
message += f" *workflow*: <https://github.com/{self.repo}/actions/runs/{run_id}|{run_id}>\n"
|
2024-08-06 14:55:04 +00:00
|
|
|
self.post(message, channels=[channel] if isinstance(channel, str) else channel)
|
2024-07-11 11:37:26 +00:00
|
|
|
|
|
|
|
|
2024-07-22 13:46:27 +00:00
|
|
|
def parse_args():
|
|
|
|
parser = argparse.ArgumentParser("CI Buddy bot notifies about CI events")
|
|
|
|
parser.add_argument(
|
|
|
|
"--check-wf-status",
|
|
|
|
action="store_true",
|
|
|
|
help="Checks workflow status",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--test",
|
|
|
|
action="store_true",
|
|
|
|
help="for test and debug",
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
"--dry-run",
|
|
|
|
action="store_true",
|
|
|
|
help="dry run mode",
|
|
|
|
)
|
|
|
|
return parser.parse_args(), parser
|
|
|
|
|
|
|
|
|
2024-07-11 11:37:26 +00:00
|
|
|
if __name__ == "__main__":
|
2024-07-22 13:46:27 +00:00
|
|
|
args, parser = parse_args()
|
|
|
|
|
|
|
|
if args.test:
|
|
|
|
CIBuddy(dry_run=True).post_job_error("TEst")
|
|
|
|
elif args.check_wf_status:
|
|
|
|
CIBuddy(dry_run=args.dry_run).check_workflow()
|