diff --git a/.github/workflows/handle_potential_conflicts.py b/.github/workflows/handle_potential_conflicts.py index ffdae440d85ba..bd0f5a128175d 100755 --- a/.github/workflows/handle_potential_conflicts.py +++ b/.github/workflows/handle_potential_conflicts.py @@ -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]} ', 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 "Can’t 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) diff --git a/.github/workflows/predict-conflicts.yml b/.github/workflows/predict-conflicts.yml index 186abfd4d1df1..d015270957603 100644 --- a/.github/workflows/predict-conflicts.yml +++ b/.github/workflows/predict-conflicts.yml @@ -1,4 +1,4 @@ -name: "Check Potential Conflicts" +name: "Check Potential Conflicts - Test PR 1" on: pull_request_target: @@ -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