Files
sglang/scripts/ci_monitor/post_bisect_to_slack.py
2026-04-04 18:58:18 -07:00

254 lines
8.6 KiB
Python
Executable File

#!/usr/bin/env python3
"""
Post CI auto-bisect results to Slack.
Reads the bisect_results.json produced by ci_auto_bisect.py and posts
a summary message with threaded details to the CI failures Slack channel.
"""
import argparse
import json
import logging
import os
import sys
from datetime import datetime
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# CI failures channel (same as post_ci_failures_to_slack.py)
SLACK_CHANNEL_ID = "C0A2DG0R7CJ"
CLASSIFICATION_EMOJI = {
"code_regression": "🔴",
"hardware_issue": "🟠",
"environment_change": "🟡",
"flaky_test": "🔵",
"unknown": "",
}
CLASSIFICATION_LABELS = {
"code_regression": "Code Regression",
"hardware_issue": "Hardware Issue",
"environment_change": "Environment Change",
"flaky_test": "Flaky Test",
"unknown": "Unknown",
}
def post_bisect_to_slack(report_file: str) -> bool:
"""Post bisect results to Slack with threaded details."""
from slack_sdk import WebClient
token = os.environ.get("SGLANG_DIFFUSION_SLACK_TOKEN")
if not token:
logger.info("Slack post skipped: no token")
return False
with open(report_file) as f:
report = json.load(f)
try:
results = report.get("results", [])
summary = report.get("summary", {})
total_analyzed = report.get("total_failures_analyzed", 0)
total_tokens = report.get("total_tokens_used", 0)
error_msg = report.get("error")
client = WebClient(token=token)
run_id = os.environ.get("GITHUB_RUN_ID", "")
workflow_url = ""
if run_id:
workflow_url = (
f"https://github.com/sgl-project/sglang/actions/runs/{run_id}"
)
# Build summary message
if error_msg:
mentions = "<@U09R55D8EAY> <@U09ABMCKQPM>"
summary_text = (
f"{mentions} 🚨 *CI Auto Bisect Failed*\n" f"Error: `{error_msg[:200]}`"
)
if workflow_url:
summary_text += f"\n<{workflow_url}|View logs>"
color = "danger"
elif total_analyzed == 0:
summary_text = "✅ *CI Auto Bisect*: No failures requiring analysis"
if workflow_url:
summary_text += f"\n<{workflow_url}|View run>"
color = "good"
else:
mentions = "<@U09R55D8EAY> <@U09ABMCKQPM>"
lines = [f"{mentions} 🔍 *CI Auto Bisect Results*"]
lines.append(f"Analyzed {total_analyzed} failures:\n")
# Order: regressions first (most actionable)
class_order = [
"code_regressions",
"hardware_issues",
"environment_changes",
"flaky_tests",
"unknown",
]
class_to_key = {
"code_regressions": "code_regression",
"hardware_issues": "hardware_issue",
"environment_changes": "environment_change",
"flaky_tests": "flaky_test",
"unknown": "unknown",
}
for cls_key in class_order:
count = summary.get(cls_key, 0)
if count > 0:
cls = class_to_key[cls_key]
emoji = CLASSIFICATION_EMOJI.get(cls, "")
label = CLASSIFICATION_LABELS.get(cls, cls)
lines.append(f" {emoji} *{label}*: {count}")
if workflow_url:
lines.append(f"\n<{workflow_url}|View full bisect report>")
summary_text = "\n".join(lines)
# Color based on most severe classification
if summary.get("code_regressions", 0) > 0:
color = "danger"
elif (
summary.get("hardware_issues", 0) > 0
or summary.get("environment_changes", 0) > 0
):
color = "warning"
else:
color = "#439FE0" # Blue for flaky only
# Post parent message
response = client.chat_postMessage(
channel=SLACK_CHANNEL_ID,
text=summary_text,
attachments=[
{
"color": color,
"footer": f"SGLang CI Auto Bisect | {total_tokens} tokens used",
"footer_icon": "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png",
"ts": int(datetime.now().timestamp()),
}
],
)
thread_ts = response.get("ts")
if not thread_ts:
logger.warning("Slack response missing 'ts', cannot post thread")
return True
# Post detailed breakdown in thread if there are results
if results:
detail_lines = ["*Detailed Bisect Results*\n"]
# Group by classification for organized display
by_class = {}
for r in results:
cls = r.get("classification", "unknown")
by_class.setdefault(cls, []).append(r)
class_display_order = [
"code_regression",
"hardware_issue",
"environment_change",
"flaky_test",
"unknown",
]
for cls in class_display_order:
cls_results = by_class.get(cls, [])
if not cls_results:
continue
emoji = CLASSIFICATION_EMOJI.get(cls, "")
label = CLASSIFICATION_LABELS.get(cls, cls)
detail_lines.append(f"\n*━━━ {emoji} {label} ━━━*\n")
for r in cls_results:
target = r.get("target", {})
test_file = target.get("test_file", "unknown")
job_name = target.get("job_name", "unknown")
confidence = r.get("confidence", "unknown")
evidence = r.get("evidence_summary", "N/A")
fix = r.get("recommended_fix", "N/A")
suspected = r.get("suspected_commit")
suspected_pr = r.get("suspected_pr")
job_url = target.get("last_failure_job_url", "")
streak = target.get("current_streak", 0)
detail_lines.append(f"• *`{test_file}`* in `{job_name}`")
detail_lines.append(
f" Streak: {streak} | Confidence: {confidence}"
)
if suspected:
cause_str = f"`{suspected}`"
if suspected_pr:
cause_str += f" (PR #{suspected_pr})"
detail_lines.append(f" Suspected: {cause_str}")
detail_lines.append(f" Evidence: {evidence}")
detail_lines.append(f" Fix: {fix}")
if job_url:
detail_lines.append(f" <{job_url}|View failing job>")
detail_lines.append("")
detail_text = "\n".join(detail_lines)
# Slack has a 4000 char limit per message; split if needed
if len(detail_text) > 3900:
chunks = []
current_chunk = ""
for line in detail_lines:
if len(current_chunk) + len(line) + 1 > 3900:
chunks.append(current_chunk)
current_chunk = line
else:
current_chunk += "\n" + line if current_chunk else line
if current_chunk:
chunks.append(current_chunk)
for chunk in chunks:
client.chat_postMessage(
channel=SLACK_CHANNEL_ID,
thread_ts=thread_ts,
text=chunk,
)
else:
client.chat_postMessage(
channel=SLACK_CHANNEL_ID,
thread_ts=thread_ts,
text=detail_text,
)
logger.info("Bisect results posted to Slack successfully")
return True
except Exception:
logger.exception("Failed to post bisect results to Slack")
return False
def main():
parser = argparse.ArgumentParser(description="Post CI auto-bisect results to Slack")
parser.add_argument(
"--report-file",
type=str,
required=True,
help="Path to bisect_results.json",
)
args = parser.parse_args()
success = post_bisect_to_slack(args.report_file)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()