Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 130 additions & 20 deletions .github/workflows/handle_potential_conflicts.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,59 +19,169 @@
"""

import sys
import os
import json
import uuid
import requests

# need to install via pip
import hjson
try:
import hjson
except ImportError:
print("Error: hjson module not found. Please install it with: pip install hjson", file=sys.stderr)
sys.exit(1)

def get_pr_json(pr_num):
return requests.get(f'https://api.github.com/repos/dashpay/dash/pulls/{pr_num}').json()
# Get repository from environment or default to dashpay/dash
repo = os.environ.get('GITHUB_REPOSITORY', 'dashpay/dash')

try:
response = requests.get(f'https://api.github.com/repos/{repo}/pulls/{pr_num}')
response.raise_for_status()
pr_data = response.json()

# Check if we got an error response
if 'message' in pr_data and 'head' not in pr_data:
print(f"Warning: GitHub API error for PR {pr_num}: {pr_data.get('message', 'Unknown error')}", file=sys.stderr)
return None

return pr_data
except requests.RequestException as e:
print(f"Warning: Error fetching PR {pr_num}: {e}", file=sys.stderr)
return None
except json.JSONDecodeError as e:
print(f"Warning: Error parsing JSON for PR {pr_num}: {e}", file=sys.stderr)
return None

def set_github_output(name, value):
"""Set GitHub Actions output"""
if 'GITHUB_OUTPUT' not in os.environ:
print(f"Warning: GITHUB_OUTPUT not set, skipping output: {name}={value}", file=sys.stderr)
return

try:
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
# For multiline values, use the delimiter syntax
if '\n' in str(value):
delimiter = f"EOF_{uuid.uuid4()}"
f.write(f"{name}<<{delimiter}\n{value}\n{delimiter}\n")
else:
f.write(f"{name}={value}\n")
except IOError as e:
print(f"Error writing to GITHUB_OUTPUT: {e}", file=sys.stderr)

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

input = sys.argv[1]
print(input)
j_input = hjson.loads(input)
print(j_input)
conflict_input = sys.argv[1]
print(f"Debug: Input received: {conflict_input}", file=sys.stderr)

try:
j_input = hjson.loads(conflict_input)
except Exception as e:
print(f"Error parsing input JSON: {e}", file=sys.stderr)
sys.exit(1)

print(f"Debug: Parsed input: {j_input}", file=sys.stderr)

# Validate required fields
if 'pull_number' not in j_input:
print("Error: 'pull_number' field missing from input", file=sys.stderr)
sys.exit(1)
if 'conflictPrs' not in j_input:
print("Error: 'conflictPrs' field missing from input", file=sys.stderr)
sys.exit(1)

our_pr_num = j_input['pull_number']
our_pr_label = get_pr_json(our_pr_num)['head']['label']
conflictPrs = j_input['conflictPrs']
our_pr_json = get_pr_json(our_pr_num)

if our_pr_json is None:
print(f"Error: Failed to fetch PR {our_pr_num}", file=sys.stderr)
sys.exit(1)

if 'head' not in our_pr_json or 'label' not in our_pr_json['head']:
print(f"Error: Invalid PR data structure for PR {our_pr_num}", file=sys.stderr)
sys.exit(1)

our_pr_label = our_pr_json['head']['label']
conflict_prs = j_input['conflictPrs']

good = []
bad = []
conflict_details = []

for conflict in conflict_prs:
if 'number' not in conflict:
print("Warning: Skipping conflict entry without 'number' field", file=sys.stderr)
continue

for conflict in conflictPrs:
conflict_pr_num = conflict['number']
print(conflict_pr_num)
print(f"Debug: Checking PR #{conflict_pr_num}", file=sys.stderr)

conflict_pr_json = get_pr_json(conflict_pr_num)

if conflict_pr_json is None:
print(f"Warning: Failed to fetch PR {conflict_pr_num}, skipping", file=sys.stderr)
continue

if 'head' not in conflict_pr_json or 'label' not in conflict_pr_json['head']:
print(f"Warning: Invalid PR data structure for PR {conflict_pr_num}, skipping", file=sys.stderr)
continue

conflict_pr_label = conflict_pr_json['head']['label']
print(conflict_pr_label)
print(f"Debug: PR #{conflict_pr_num} label: {conflict_pr_label}", file=sys.stderr)

if conflict_pr_json['mergeable_state'] == "dirty":
print(f'{conflict_pr_num} needs rebase. Skipping conflict check')
if conflict_pr_json.get('mergeable_state') == "dirty":
print(f'PR #{conflict_pr_num} needs rebase. Skipping conflict check', file=sys.stderr)
continue

if conflict_pr_json['draft']:
print(f'{conflict_pr_num} is a draft. Skipping conflict check')
if conflict_pr_json.get('draft', False):
print(f'PR #{conflict_pr_num} is a draft. Skipping conflict check', file=sys.stderr)
continue

# Get repository from environment
repo = os.environ.get('GITHUB_REPOSITORY', 'dashpay/dash')

try:
pre_mergeable = requests.get(f'https://github.com/{repo}/branches/pre_mergeable/{our_pr_label}...{conflict_pr_label}')
pre_mergeable.raise_for_status()
except requests.RequestException as e:
print(f"Error checking mergeability for PR {conflict_pr_num}: {e}", file=sys.stderr)
continue

pre_mergeable = requests.get(f'https://github.com/dashpay/dash/branches/pre_mergeable/{our_pr_label}...{conflict_pr_label}')
if "These branches can be automatically merged." in pre_mergeable.text:
good.append(conflict_pr_num)
elif "Cant automatically merge" in pre_mergeable.text:
elif "Can't automatically merge" in pre_mergeable.text:
bad.append(conflict_pr_num)
conflict_details.append({
'number': conflict_pr_num,
'title': conflict_pr_json.get('title', 'Unknown'),
'url': conflict_pr_json.get('html_url', f'https://github.com/dashpay/dash/pull/{conflict_pr_num}')
})
else:
print(f"Warning: Unexpected response for PR {conflict_pr_num} mergeability check. Response snippet: {pre_mergeable.text[:200]}", file=sys.stderr)

print(f"Not conflicting PRs: {good}", file=sys.stderr)
print(f"Conflicting PRs: {bad}", file=sys.stderr)

# Set GitHub Actions outputs
if 'GITHUB_OUTPUT' in os.environ:
set_github_output('has_conflicts', 'true' if len(bad) > 0 else 'false')

# Format conflict details as markdown list
if conflict_details:
markdown_list = []
for conflict in conflict_details:
markdown_list.append(f"- #{conflict['number']} - [{conflict['title']}]({conflict['url']})")
conflict_markdown = '\n'.join(markdown_list)
set_github_output('conflict_details', conflict_markdown)
else:
raise Exception("not mergeable or unmergable!")
set_github_output('conflict_details', '')

print("Not conflicting PRs: ", good)
set_github_output('conflicting_prs', ','.join(map(str, bad)))

print("Conflicting PRs: ", bad)
if len(bad) > 0:
sys.exit(1)

Expand Down
30 changes: 29 additions & 1 deletion .github/workflows/predict-conflicts.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: "Check Potential Conflicts"
name: "Check Potential Conflicts - Test PR 2 Different Change"

on:
pull_request_target:
Expand All @@ -23,10 +23,38 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: check for potential conflicts
id: check_conflicts
uses: PastaPastaPasta/potential-conflicts-checker-action@v0.1.10
with:
ghToken: "${{ secrets.GITHUB_TOKEN }}"
- name: Checkout
uses: actions/checkout@v3
- name: validate potential conflicts
id: validate_conflicts
run: pip3 install hjson && .github/workflows/handle_potential_conflicts.py "$conflicts"
continue-on-error: true
- name: Post conflict comment
if: steps.validate_conflicts.outputs.has_conflicts == 'true'
uses: mshick/add-pr-comment@v2
with:
message-id: conflict-prediction
message: |
## ⚠️ Potential Merge Conflicts Detected

This PR has potential conflicts with the following open PRs:

${{ steps.validate_conflicts.outputs.conflict_details }}

Please coordinate with the authors of these PRs to avoid merge conflicts.
- name: Remove conflict comment if no conflicts
if: steps.validate_conflicts.outputs.has_conflicts == 'false'
uses: mshick/add-pr-comment@v2
with:
message-id: conflict-prediction
message: |
## ✅ No Merge Conflicts Detected

This PR currently has no conflicts with other open PRs.
- name: Fail if conflicts exist
if: steps.validate_conflicts.outputs.has_conflicts == 'true'
run: exit 1
Loading