Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update automated commit status comment #54441

Merged
merged 15 commits into from
Sep 12, 2023
102 changes: 72 additions & 30 deletions tests/ci/commit_status_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import os
import time
from typing import Dict, List, Optional, Union
from collections import defaultdict
import logging

from github import Github
from github.GithubObject import _NotSetType, NotSet as NotSet
from github.Commit import Commit
from github.CommitStatus import CommitStatus
from github.IssueComment import IssueComment
from github.PullRequest import PullRequest
from github.Repository import Repository

from ci_config import CI_CONFIG, REQUIRED_CHECKS, CHECK_DESCRIPTIONS, CheckDescription
Expand Down Expand Up @@ -128,6 +130,27 @@ def post_commit_status(
logging.error("Failed to update the status comment, continue anyway")


STATUS_ICON_MAP = defaultdict(
str,
{
ERROR: "❌",
FAILURE: "❌",
PENDING: "⏳",
SUCCESS: "✅",
},
)


def update_pr_status_label(pr: PullRequest, status: str) -> None:
new_label = "pr-status-" + STATUS_ICON_MAP[status]
for label in pr.get_labels():
if label.name == new_label:
return
if label.name.startswith("pr-status-"):
pr.remove_from_labels(label.name)
pr.add_to_labels(new_label)


def set_status_comment(commit: Commit, pr_info: PRInfo) -> None:
"""It adds or updates the comment status to all Pull Requests but for release
one, so the method does nothing for simple pushes and pull requests with
Expand Down Expand Up @@ -167,6 +190,8 @@ def set_status_comment(commit: Commit, pr_info: PRInfo) -> None:
comment = ic
break

update_pr_status_label(pr, get_worst_state(statuses))

if comment is None:
pr.create_issue_comment(comment_body)
return
Expand All @@ -180,33 +205,16 @@ def set_status_comment(commit: Commit, pr_info: PRInfo) -> None:
def generate_status_comment(pr_info: PRInfo, statuses: CommitStatuses) -> str:
"""The method generates the comment body, as well it updates the CI report"""

def beauty_state(state: str) -> str:
if state == SUCCESS:
return f"🟢 {state}"
if state == PENDING:
return f"🟡 {state}"
if state in [ERROR, FAILURE]:
return f"🔴 {state}"
return state

report_url = create_ci_report(pr_info, statuses)
worst_state = get_worst_state(statuses)
if not worst_state:
# Theoretically possible, although
# the function should not be used on empty statuses
worst_state = "The commit doesn't have the statuses yet"
else:
worst_state = f"The overall status of the commit is {beauty_state(worst_state)}"

comment_body = (
f"<!-- automatic status comment for PR #{pr_info.number} "
f"from {pr_info.head_name}:{pr_info.head_ref} -->\n"
f"This is an automated comment for commit {pr_info.sha} with "
f"description of existing statuses. It's updated for the latest CI running\n"
f"The full report is available [here]({report_url})\n"
f"{worst_state}\n\n<table>"
"<thead><tr><th>Check name</th><th>Description</th><th>Status</th></tr></thead>\n"
"<tbody>"
f"*This is an automated comment for commit {pr_info.sha} with "
f"description of existing statuses. It's updated for the latest CI running*\n\n"
f"[{STATUS_ICON_MAP[worst_state]} Click here]({report_url}) to open a full report in a separate page\n"
f"\n"
)
# group checks by the name to get the worst one per each
grouped_statuses = {} # type: Dict[CheckDescription, CommitStatuses]
Expand All @@ -230,17 +238,46 @@ def beauty_state(state: str) -> str:
else:
grouped_statuses[cd] = [status]

table_rows = [] # type: List[str]
table_header = (
"<table>\n"
"<thead><tr><th>Check name</th><th>Description</th><th>Status</th></tr></thead>\n"
"<tbody>\n"
)
table_footer = "<tbody>\n</table>\n"

details_header = "<details><summary>Successful checks</summary>\n"
details_footer = "</details>\n"

visible_table_rows = [] # type: List[str]
hidden_table_rows = [] # type: List[str]
for desc, gs in grouped_statuses.items():
table_rows.append(
state = get_worst_state(gs)
table_row = (
f"<tr><td>{desc.name}</td><td>{desc.description}</td>"
f"<td>{beauty_state(get_worst_state(gs))}</td></tr>\n"
f"<td>{STATUS_ICON_MAP[state]} {state}</td></tr>\n"
)
if state == SUCCESS:
hidden_table_rows.append(table_row)
else:
visible_table_rows.append(table_row)

table_rows.sort()
result = [comment_body]

comment_footer = "</table>"
return "".join([comment_body, *table_rows, comment_footer])
if hidden_table_rows:
hidden_table_rows.sort()
result.append(details_header)
result.append(table_header)
result.extend(hidden_table_rows)
result.append(table_footer)
result.append(details_footer)

if visible_table_rows:
visible_table_rows.sort()
result.append(table_header)
result.extend(visible_table_rows)
result.append(table_footer)

return "".join(result)


def get_worst_state(statuses: CommitStatuses) -> str:
Expand All @@ -252,10 +289,15 @@ def create_ci_report(pr_info: PRInfo, statuses: CommitStatuses) -> str:
to S3 tests bucket. Then it returns the URL"""
test_results = [] # type: TestResults
for status in statuses:
log_urls = None
log_urls = []
if status.target_url is not None:
log_urls = [status.target_url]
test_results.append(TestResult(status.context, status.state, log_urls=log_urls))
log_urls.append(status.target_url)
raw_logs = status.description or None
test_results.append(
TestResult(
status.context, status.state, log_urls=log_urls, raw_logs=raw_logs
Felixoid marked this conversation as resolved.
Show resolved Hide resolved
)
)
return upload_results(
S3Helper(), pr_info.number, pr_info.sha, test_results, [], CI_STATUS_NAME
)
Expand Down
20 changes: 10 additions & 10 deletions tests/ci/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ def get_worst_status(statuses: Iterable[str]) -> str:
p.links a:hover {{ background: var(--menu-hover-background); color: var(--menu-hover-color); }}
th {{ cursor: pointer; }}
tr:hover {{ filter: var(--tr-hover-filter); }}
.failed {{ cursor: pointer; }}
.failed-content {{ display: none; }}
.expandable {{ cursor: pointer; }}
.expandable-content {{ display: none; }}
#fish {{ display: none; float: right; position: relative; top: -20em; right: 2vw; margin-bottom: -20em; width: 30vw; filter: brightness(7%); z-index: -1; }}

.themes {{
Expand Down Expand Up @@ -148,7 +148,7 @@ def get_worst_status(statuses: Iterable[str]) -> str:
const getCellValue = (tr, idx) => {{
var classes = tr.classList;
var elem = tr;
if (classes.contains("failed-content") || classes.contains("failed-content.open"))
if (classes.contains("expandable-content") || classes.contains("expandable-content.open"))
elem = tr.previousElementSibling;
return elem.children[idx].innerText || elem.children[idx].textContent;
}}
Expand All @@ -164,9 +164,9 @@ def get_worst_status(statuses: Iterable[str]) -> str:
.forEach(tr => table.appendChild(tr) );
}})));

Array.from(document.getElementsByClassName("failed")).forEach(tr => tr.addEventListener('click', function() {{
Array.from(document.getElementsByClassName("expandable")).forEach(tr => tr.addEventListener('click', function() {{
var content = this.nextElementSibling;
content.classList.toggle("failed-content");
content.classList.toggle("expandable-content");
}}));

let theme = 'dark';
Expand Down Expand Up @@ -546,9 +546,8 @@ def create_test_html_report(
has_log_urls = True

row = []
has_error = test_result.status in ("FAIL", "NOT_FAILED")
if has_error and test_result.raw_logs is not None:
row.append('<tr class="failed">')
if test_result.raw_logs is not None:
row.append('<tr class="expandable">')
else:
row.append("<tr>")
row.append(f"<td>{test_result.name}</td>")
Expand All @@ -557,6 +556,7 @@ def create_test_html_report(

# Allow to quickly scroll to the first failure.
fail_id = ""
has_error = test_result.status in ("FAIL", "NOT_FAILED")
if has_error:
num_fails = num_fails + 1
fail_id = f'id="fail{num_fails}" '
Expand All @@ -578,11 +578,11 @@ def create_test_html_report(
colspan += 1

row.append("</tr>")
rows_part.append("".join(row))
rows_part.append("\n".join(row))
if test_result.raw_logs is not None:
raw_logs = escape(test_result.raw_logs)
row_raw_logs = (
'<tr class="failed-content">'
'<tr class="expandable-content">'
f'<td colspan="{colspan}"><pre>{raw_logs}</pre></td>'
"</tr>"
)
Expand Down