-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathslack_notify.py
More file actions
142 lines (111 loc) · 4.29 KB
/
slack_notify.py
File metadata and controls
142 lines (111 loc) · 4.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""Slack webhook notifications for PR conflict detection results."""
import json
import logging
import requests
from conflict_detector import ConflictCluster, cluster_conflicts
logger = logging.getLogger(__name__)
def send_slack_notification(
webhook_url: str,
conflicts_by_repo: dict, # {repo_full_name: list[ConflictResult]}
channel: str = "",
dry_run: bool = False,
) -> bool:
"""Send Slack notifications about detected PR conflicts.
Sends one message per conflict/cluster with @mentions for authors.
Args:
webhook_url: Slack incoming webhook URL
conflicts_by_repo: Dict mapping repo names to their conflict results
channel: Optional channel override
dry_run: If True, log the messages but don't send
Returns:
True if all notifications sent successfully, False otherwise
"""
if not webhook_url:
logger.info("No Slack webhook URL configured, skipping notification")
return False
if not conflicts_by_repo or all(len(c) == 0 for c in conflicts_by_repo.values()):
logger.info("No conflicts to notify about")
return True
all_success = True
for repo_name, conflicts in conflicts_by_repo.items():
if not conflicts:
continue
clusters = cluster_conflicts(conflicts)
for cluster in clusters:
message = build_cluster_message(repo_name, cluster)
if dry_run:
logger.info(
"DRY RUN: Would send Slack message: %s",
json.dumps(message, indent=2),
)
else:
success = post_to_slack(webhook_url, message, channel)
if not success:
all_success = False
return all_success
def build_cluster_message(repo_name: str, cluster: ConflictCluster) -> dict:
"""Build a Slack message for a single conflict cluster.
Args:
repo_name: Repository full name.
cluster: ConflictCluster containing one or more conflicts.
Returns:
Slack webhook payload dict.
"""
# Collect unique authors for @mentions
authors = sorted({pr.author for pr in cluster.prs if pr.author})
mentions = " ".join(f"<@{author}>" for author in authors)
if len(cluster.prs) == 2:
# Simple pair
conflict = cluster.conflicts[0]
file_details = _format_file_details(conflict.conflicting_files)
text = (
f"{mentions} Your PRs may conflict:\n\n"
f"*{repo_name}*\n"
f"<{conflict.pr_a.url}|#{conflict.pr_a.number}> ({conflict.pr_a.title}) "
f"↔ <{conflict.pr_b.url}|#{conflict.pr_b.number}> ({conflict.pr_b.title})\n\n"
f"{file_details}"
)
else:
# Multi-PR cluster
pr_list = "\n".join(
f" • <{pr.url}|#{pr.number}> {pr.title}" for pr in cluster.prs
)
files_str = ", ".join(f"`{f}`" for f in cluster.shared_files)
text = (
f"{mentions} Your PRs may conflict:\n\n"
f"*{repo_name} — Cluster: {len(cluster.prs)} PRs, "
f"{len(cluster.conflicts)} conflict pair(s)*\n\n"
f"PRs:\n{pr_list}\n\n"
f"Shared files: {files_str}"
)
return {"text": text}
def _format_file_details(file_overlaps: list) -> str:
"""Format file overlap details with line ranges.
Args:
file_overlaps: List of FileOverlap objects.
Returns:
Formatted string with file names and line ranges.
"""
lines = []
for fo in file_overlaps:
ranges = ", ".join(f"L{start}-L{end}" for start, end in fo.overlapping_ranges)
lines.append(f" • `{fo.filename}` ({ranges})")
return "Files:\n" + "\n".join(lines)
def post_to_slack(webhook_url: str, message: dict, channel: str = "") -> bool:
"""Post a message to Slack via webhook.
Returns True if successful, False otherwise.
"""
if channel:
message["channel"] = channel
try:
response = requests.post(
webhook_url,
json=message,
headers={"Content-Type": "application/json"},
timeout=30,
)
response.raise_for_status()
return True
except requests.RequestException as e:
logger.error("Failed to send Slack notification: %s", e)
return False