Skip to content

Commit a59a72a

Browse files
Merge #6758: feat: enhance conflict prediction workflow with PR comments
538d88d fix: add UTF-8 encoding to file open operation (pasta) e0515b9 fix: use dynamic repository reference and update checkout action (pasta) 2758785 cleanup: remove debug logging now that issues are resolved (pasta) 8b3c439 fix: improve conflict detection by checking for partial text or X icon (pasta) 5a853f2 debug: add more diagnostics for response text analysis (pasta) a7be4bb debug: add more detailed logging for merge check failures (pasta) 75c21e6 fix: make repository configurable using GITHUB_REPOSITORY env var (pasta) 4668cb7 fix: handle missing PR data gracefully in conflict checker (pasta) 3180c2d feat: enhance conflict prediction workflow with PR comments (pasta) Pull request description: ## Summary This PR enhances the conflict prediction workflow to automatically post comments on pull requests when potential merge conflicts are detected with other open PRs. ## Changes ### 1. Enhanced Python Script (`handle_potential_conflicts.py`) - Added GitHub Actions output functionality to pass conflict data to the workflow - Made repository configurable using `GITHUB_REPOSITORY` environment variable (supports testing on forks) - Improved error handling to gracefully skip PRs that fail to fetch - Fixed conflict detection to work with current GitHub HTML responses - Added formatted markdown output for PR comments ### 2. Updated Workflow (`predict-conflicts.yml`) - Added PR comment posting using `mshick/add-pr-comment@v2` - Comments are updateable (sticky) using `message-id` to prevent duplicates - Shows list of conflicting PRs with links when conflicts exist - Removes/updates comment when conflicts are resolved - Maintains original failure behavior ### 3. Bug Fixes - Fixed KeyError when PR API responses don't have expected fields - Improved HTML response parsing for merge conflict detection - Added support for partial text matching ("Can't automatic") and icon detection ("octicon octicon-x") ## Example Comment Output When conflicts are detected: ``` ## ⚠️ Potential Merge Conflicts Detected This PR has potential conflicts with the following open PRs: - #1234 - [PR Title](#1234) - #5678 - [Another PR Title](#5678) Please coordinate with the authors of these PRs to avoid merge conflicts. ``` When no conflicts exist: ``` ## ✅ No Merge Conflicts Detected This PR currently has no conflicts with other open PRs. ``` ## Testing This has been tested on the PastaPastaPasta/dash fork with multiple test PRs to verify: - Conflict detection works correctly - Comments are posted and updated properly - No duplicate comments are created - Works with forked repositories ## Benefits - Better visibility of potential merge conflicts early in the review process - Helps maintainers and contributors coordinate on conflicting changes - Reduces merge conflicts and rebasing work - Automatic updates keep the information current ACKs for top commit: UdjinM6: utACK 538d88d Tree-SHA512: d687f3aba8c8a48a404257fa39ddd0826a806616f3bd4e374f797ebe8061eccb3b80012d64405f2635acffea04101754446f378a02a887956e0aa78ca01a2873
2 parents d5fc8bf + 538d88d commit a59a72a

File tree

2 files changed

+156
-21
lines changed

2 files changed

+156
-21
lines changed

.github/workflows/handle_potential_conflicts.py

Lines changed: 127 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,59 +19,166 @@
1919
"""
2020

2121
import sys
22+
import os
23+
import json
24+
import uuid
2225
import requests
2326

2427
# need to install via pip
25-
import hjson
28+
try:
29+
import hjson
30+
except ImportError:
31+
print("Error: hjson module not found. Please install it with: pip install hjson", file=sys.stderr)
32+
sys.exit(1)
2633

2734
def get_pr_json(pr_num):
28-
return requests.get(f'https://api.github.com/repos/dashpay/dash/pulls/{pr_num}').json()
35+
# Get repository from environment or default to dashpay/dash
36+
repo = os.environ.get('GITHUB_REPOSITORY', 'dashpay/dash')
37+
38+
try:
39+
response = requests.get(f'https://api.github.com/repos/{repo}/pulls/{pr_num}')
40+
response.raise_for_status()
41+
pr_data = response.json()
42+
43+
# Check if we got an error response
44+
if 'message' in pr_data and 'head' not in pr_data:
45+
print(f"Warning: GitHub API error for PR {pr_num}: {pr_data.get('message', 'Unknown error')}", file=sys.stderr)
46+
return None
47+
48+
return pr_data
49+
except requests.RequestException as e:
50+
print(f"Warning: Error fetching PR {pr_num}: {e}", file=sys.stderr)
51+
return None
52+
except json.JSONDecodeError as e:
53+
print(f"Warning: Error parsing JSON for PR {pr_num}: {e}", file=sys.stderr)
54+
return None
55+
56+
def set_github_output(name, value):
57+
"""Set GitHub Actions output"""
58+
if 'GITHUB_OUTPUT' not in os.environ:
59+
print(f"Warning: GITHUB_OUTPUT not set, skipping output: {name}={value}", file=sys.stderr)
60+
return
61+
62+
try:
63+
with open(os.environ['GITHUB_OUTPUT'], 'a', encoding='utf8') as f:
64+
# For multiline values, use the delimiter syntax
65+
if '\n' in str(value):
66+
delimiter = f"EOF_{uuid.uuid4()}"
67+
f.write(f"{name}<<{delimiter}\n{value}\n{delimiter}\n")
68+
else:
69+
f.write(f"{name}={value}\n")
70+
except IOError as e:
71+
print(f"Error writing to GITHUB_OUTPUT: {e}", file=sys.stderr)
2972

3073
def main():
3174
if len(sys.argv) != 2:
3275
print(f'Usage: {sys.argv[0]} <conflicts>', file=sys.stderr)
3376
sys.exit(1)
3477

35-
input = sys.argv[1]
36-
print(input)
37-
j_input = hjson.loads(input)
38-
print(j_input)
78+
conflict_input = sys.argv[1]
3979

80+
try:
81+
j_input = hjson.loads(conflict_input)
82+
except Exception as e:
83+
print(f"Error parsing input JSON: {e}", file=sys.stderr)
84+
sys.exit(1)
85+
86+
# Validate required fields
87+
if 'pull_number' not in j_input:
88+
print("Error: 'pull_number' field missing from input", file=sys.stderr)
89+
sys.exit(1)
90+
if 'conflictPrs' not in j_input:
91+
print("Error: 'conflictPrs' field missing from input", file=sys.stderr)
92+
sys.exit(1)
4093

4194
our_pr_num = j_input['pull_number']
42-
our_pr_label = get_pr_json(our_pr_num)['head']['label']
43-
conflictPrs = j_input['conflictPrs']
95+
our_pr_json = get_pr_json(our_pr_num)
96+
97+
if our_pr_json is None:
98+
print(f"Error: Failed to fetch PR {our_pr_num}", file=sys.stderr)
99+
sys.exit(1)
100+
101+
if 'head' not in our_pr_json or 'label' not in our_pr_json['head']:
102+
print(f"Error: Invalid PR data structure for PR {our_pr_num}", file=sys.stderr)
103+
sys.exit(1)
104+
105+
our_pr_label = our_pr_json['head']['label']
106+
conflict_prs = j_input['conflictPrs']
44107

45108
good = []
46109
bad = []
110+
conflict_details = []
111+
112+
for conflict in conflict_prs:
113+
if 'number' not in conflict:
114+
print("Warning: Skipping conflict entry without 'number' field", file=sys.stderr)
115+
continue
47116

48-
for conflict in conflictPrs:
49117
conflict_pr_num = conflict['number']
50-
print(conflict_pr_num)
51118

52119
conflict_pr_json = get_pr_json(conflict_pr_num)
120+
121+
if conflict_pr_json is None:
122+
print(f"Warning: Failed to fetch PR {conflict_pr_num}, skipping", file=sys.stderr)
123+
continue
124+
125+
if 'head' not in conflict_pr_json or 'label' not in conflict_pr_json['head']:
126+
print(f"Warning: Invalid PR data structure for PR {conflict_pr_num}, skipping", file=sys.stderr)
127+
continue
128+
53129
conflict_pr_label = conflict_pr_json['head']['label']
54-
print(conflict_pr_label)
55130

56-
if conflict_pr_json['mergeable_state'] == "dirty":
57-
print(f'{conflict_pr_num} needs rebase. Skipping conflict check')
131+
if conflict_pr_json.get('mergeable_state') == "dirty":
132+
print(f'PR #{conflict_pr_num} needs rebase. Skipping conflict check', file=sys.stderr)
58133
continue
59134

60-
if conflict_pr_json['draft']:
61-
print(f'{conflict_pr_num} is a draft. Skipping conflict check')
135+
if conflict_pr_json.get('draft', False):
136+
print(f'PR #{conflict_pr_num} is a draft. Skipping conflict check', file=sys.stderr)
137+
continue
138+
139+
# Get repository from environment
140+
repo = os.environ.get('GITHUB_REPOSITORY', 'dashpay/dash')
141+
merge_check_url = f'https://github.com/{repo}/branches/pre_mergeable/{our_pr_label}...{conflict_pr_label}'
142+
143+
try:
144+
pre_mergeable = requests.get(merge_check_url)
145+
pre_mergeable.raise_for_status()
146+
except requests.RequestException as e:
147+
print(f"Error checking mergeability for PR {conflict_pr_num}: {e}", file=sys.stderr)
62148
continue
63149

64-
pre_mergeable = requests.get(f'https://github.com/dashpay/dash/branches/pre_mergeable/{our_pr_label}...{conflict_pr_label}')
65150
if "These branches can be automatically merged." in pre_mergeable.text:
66151
good.append(conflict_pr_num)
67-
elif "Can’t automatically merge" in pre_mergeable.text:
152+
elif "Can't automatic" in pre_mergeable.text or "octicon octicon-x" in pre_mergeable.text:
153+
# Check for partial text or the X icon which indicates conflicts
68154
bad.append(conflict_pr_num)
155+
conflict_details.append({
156+
'number': conflict_pr_num,
157+
'title': conflict_pr_json.get('title', 'Unknown'),
158+
'url': conflict_pr_json.get('html_url', f'https://github.com/{repo}/pull/{conflict_pr_num}')
159+
})
160+
else:
161+
print(f"Warning: Unexpected response for PR {conflict_pr_num} mergeability check. URL: {pre_mergeable.url}", file=sys.stderr)
162+
163+
print(f"Not conflicting PRs: {good}", file=sys.stderr)
164+
print(f"Conflicting PRs: {bad}", file=sys.stderr)
165+
166+
# Set GitHub Actions outputs
167+
if 'GITHUB_OUTPUT' in os.environ:
168+
set_github_output('has_conflicts', 'true' if len(bad) > 0 else 'false')
169+
170+
# Format conflict details as markdown list
171+
if conflict_details:
172+
markdown_list = []
173+
for conflict in conflict_details:
174+
markdown_list.append(f"- #{conflict['number']} - [{conflict['title']}]({conflict['url']})")
175+
conflict_markdown = '\n'.join(markdown_list)
176+
set_github_output('conflict_details', conflict_markdown)
69177
else:
70-
raise Exception("not mergeable or unmergable!")
178+
set_github_output('conflict_details', '')
71179

72-
print("Not conflicting PRs: ", good)
180+
set_github_output('conflicting_prs', ','.join(map(str, bad)))
73181

74-
print("Conflicting PRs: ", bad)
75182
if len(bad) > 0:
76183
sys.exit(1)
77184

.github/workflows/predict-conflicts.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,38 @@ jobs:
2323
runs-on: ubuntu-latest
2424
steps:
2525
- name: check for potential conflicts
26+
id: check_conflicts
2627
uses: PastaPastaPasta/potential-conflicts-checker-action@v0.1.10
2728
with:
2829
ghToken: "${{ secrets.GITHUB_TOKEN }}"
2930
- name: Checkout
30-
uses: actions/checkout@v3
31+
uses: actions/checkout@v4
3132
- name: validate potential conflicts
33+
id: validate_conflicts
3234
run: pip3 install hjson && .github/workflows/handle_potential_conflicts.py "$conflicts"
35+
continue-on-error: true
36+
- name: Post conflict comment
37+
if: steps.validate_conflicts.outputs.has_conflicts == 'true'
38+
uses: mshick/add-pr-comment@v2
39+
with:
40+
message-id: conflict-prediction
41+
message: |
42+
## ⚠️ Potential Merge Conflicts Detected
43+
44+
This PR has potential conflicts with the following open PRs:
45+
46+
${{ steps.validate_conflicts.outputs.conflict_details }}
47+
48+
Please coordinate with the authors of these PRs to avoid merge conflicts.
49+
- name: Remove conflict comment if no conflicts
50+
if: steps.validate_conflicts.outputs.has_conflicts == 'false'
51+
uses: mshick/add-pr-comment@v2
52+
with:
53+
message-id: conflict-prediction
54+
message: |
55+
## ✅ No Merge Conflicts Detected
56+
57+
This PR currently has no conflicts with other open PRs.
58+
- name: Fail if conflicts exist
59+
if: steps.validate_conflicts.outputs.has_conflicts == 'true'
60+
run: exit 1

0 commit comments

Comments
 (0)