From b897189446ecc7102a8fa543edc5931c224e34af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:34:18 +0000 Subject: [PATCH 1/3] Initial plan From b90891bf3e81a0aa67847099890cbc55c408cfe8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 19:38:05 +0000 Subject: [PATCH 2/3] Add GitHub Action to detect and label PRs with merge conflicts Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> --- .github/workflows/check-pr-conflicts.yml | 211 +++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 .github/workflows/check-pr-conflicts.yml diff --git a/.github/workflows/check-pr-conflicts.yml b/.github/workflows/check-pr-conflicts.yml new file mode 100644 index 0000000000..a0c20ce82c --- /dev/null +++ b/.github/workflows/check-pr-conflicts.yml @@ -0,0 +1,211 @@ +name: Check PR Conflicts + +# Uses pull_request_target so it runs with base repo permissions for forked PRs. +# SECURITY: We do NOT check out or execute PR code. We only use the GitHub API. +on: + pull_request_target: + types: + - opened + - synchronize + - reopened + - ready_for_review + # Also run when the base branch is updated + push: + branches: + - main + - master + - develop + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + check_conflicts: + runs-on: ubuntu-latest + steps: + - name: Check for Merge Conflicts + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + + // If triggered by push to base branch, check all open PRs + let pullRequests = []; + + if (context.eventName === 'push') { + const branch = context.ref.replace('refs/heads/', ''); + core.info(`Push event detected on branch: ${branch}`); + + // Get all open PRs targeting this branch + const { data: prs } = await github.rest.pulls.list({ + owner, + repo, + state: 'open', + base: branch, + per_page: 100, + }); + + pullRequests = prs; + core.info(`Found ${pullRequests.length} open PR(s) targeting ${branch}`); + } else { + // For pull_request_target events, process the current PR + const pr = context.payload.pull_request; + if (!pr) { + core.info('No pull_request in context. Skipping.'); + return; + } + pullRequests = [pr]; + } + + // Process each PR + for (const pr of pullRequests) { + const pull_number = pr.number; + core.info(`Processing PR #${pull_number}`); + + // Get the latest PR data to check mergeable state + const { data: prData } = await github.rest.pulls.get({ + owner, + repo, + pull_number, + }); + + const hasConflicts = prData.mergeable === false; + const isDraft = prData.draft; + const prAuthor = prData.user.login; + + core.info(`PR #${pull_number}: mergeable=${prData.mergeable}, draft=${isDraft}, conflicts=${hasConflicts}`); + + // Define the conflict label + const conflictLabel = 'has-conflicts'; + const conflictLabelColor = 'e74c3c'; // Red (project's preferred red color) + const conflictLabelDescription = 'PR has merge conflicts that need to be resolved'; + + // Get current labels on the PR + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pull_number, + per_page: 100, + }); + const currentLabelNames = new Set(currentLabels.map(l => l.name)); + const hasConflictLabel = currentLabelNames.has(conflictLabel); + + // Ensure the conflict label exists in the repo + async function ensureLabelExists() { + try { + await github.rest.issues.getLabel({ owner, repo, name: conflictLabel }); + } catch (e) { + if (e.status === 404) { + await github.rest.issues.createLabel({ + owner, + repo, + name: conflictLabel, + color: conflictLabelColor, + description: conflictLabelDescription, + }); + core.info(`Created label: ${conflictLabel}`); + } else { + throw e; + } + } + } + + await ensureLabelExists(); + + // Get existing comments to check if we already commented + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pull_number, + per_page: 100, + }); + + // Find our conflict comment (using a unique marker) + const conflictCommentMarker = ''; + const existingConflictComment = comments.find(comment => + comment.body && comment.body.includes(conflictCommentMarker) + ); + + if (hasConflicts) { + // Add the conflict label if not present + if (!hasConflictLabel) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pull_number, + labels: [conflictLabel], + }); + core.info(`Added label "${conflictLabel}" to PR #${pull_number}`); + } else { + core.info(`Label "${conflictLabel}" already present on PR #${pull_number}`); + } + + // Add a comment if one doesn't exist + if (!existingConflictComment) { + const commentBody = conflictCommentMarker + '\n' + + '⚠️ **Merge Conflicts Detected**\n\n' + + 'Hi @' + prAuthor + '!\n\n' + + 'This pull request has merge conflicts with the base branch that need to be resolved before it can be merged.\n\n' + + '**To resolve the conflicts:**\n\n' + + '1. Sync your branch with the base branch:\n' + + ' ```bash\n' + + ' git fetch origin\n' + + ' git merge origin/' + prData.base.ref + '\n' + + ' ```\n\n' + + '2. Resolve any conflicts in your editor\n\n' + + '3. Commit the changes:\n' + + ' ```bash\n' + + ' git add .\n' + + ' git commit -m "Resolve merge conflicts"\n' + + ' git push\n' + + ' ```\n\n' + + 'Once you push the resolved conflicts, this label and comment will be automatically updated.\n\n' + + 'Thank you! 🙏'; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pull_number, + body: commentBody, + }); + core.info(`Added conflict comment to PR #${pull_number}`); + } else { + core.info(`Conflict comment already exists on PR #${pull_number}`); + } + } else { + // No conflicts - remove label and comment if present + if (hasConflictLabel) { + try { + await github.rest.issues.removeLabel({ + owner, + repo, + issue_number: pull_number, + name: conflictLabel, + }); + core.info(`Removed label "${conflictLabel}" from PR #${pull_number}`); + } catch (err) { + core.warning(`Failed to remove label "${conflictLabel}": ${err.message}`); + } + } + + // Remove the conflict comment if it exists + if (existingConflictComment) { + try { + await github.rest.issues.deleteComment({ + owner, + repo, + comment_id: existingConflictComment.id, + }); + core.info(`Removed conflict comment from PR #${pull_number}`); + } catch (err) { + core.warning(`Failed to remove comment: ${err.message}`); + } + } + + core.info(`PR #${pull_number} has no conflicts`); + } + } From 26a257522853acb9062d77cbe1b45c84708be412 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:54:30 +0000 Subject: [PATCH 3/3] Simplify workflow to only handle PR events Co-authored-by: DonnieBLT <128622481+DonnieBLT@users.noreply.github.com> --- .github/workflows/check-pr-conflicts.yml | 300 ++++++++++------------- 1 file changed, 135 insertions(+), 165 deletions(-) diff --git a/.github/workflows/check-pr-conflicts.yml b/.github/workflows/check-pr-conflicts.yml index a0c20ce82c..d56e41895a 100644 --- a/.github/workflows/check-pr-conflicts.yml +++ b/.github/workflows/check-pr-conflicts.yml @@ -9,12 +9,6 @@ on: - synchronize - reopened - ready_for_review - # Also run when the base branch is updated - push: - branches: - - main - - master - - develop permissions: contents: write @@ -33,179 +27,155 @@ jobs: const owner = context.repo.owner; const repo = context.repo.repo; - // If triggered by push to base branch, check all open PRs - let pullRequests = []; + // Get the current PR from context + const pr = context.payload.pull_request; + if (!pr) { + core.info('No pull_request in context. Skipping.'); + return; + } - if (context.eventName === 'push') { - const branch = context.ref.replace('refs/heads/', ''); - core.info(`Push event detected on branch: ${branch}`); - - // Get all open PRs targeting this branch - const { data: prs } = await github.rest.pulls.list({ - owner, - repo, - state: 'open', - base: branch, - per_page: 100, - }); - - pullRequests = prs; - core.info(`Found ${pullRequests.length} open PR(s) targeting ${branch}`); - } else { - // For pull_request_target events, process the current PR - const pr = context.payload.pull_request; - if (!pr) { - core.info('No pull_request in context. Skipping.'); - return; + const pull_number = pr.number; + core.info(`Processing PR #${pull_number}`); + + // Get the latest PR data to check mergeable state + const { data: prData } = await github.rest.pulls.get({ + owner, + repo, + pull_number, + }); + + const hasConflicts = prData.mergeable === false; + const isDraft = prData.draft; + const prAuthor = prData.user.login; + + core.info(`PR #${pull_number}: mergeable=${prData.mergeable}, draft=${isDraft}, conflicts=${hasConflicts}`); + + // Define the conflict label + const conflictLabel = 'has-conflicts'; + const conflictLabelColor = 'e74c3c'; // Red (project's preferred red color) + const conflictLabelDescription = 'PR has merge conflicts that need to be resolved'; + + // Get current labels on the PR + const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ + owner, + repo, + issue_number: pull_number, + per_page: 100, + }); + const currentLabelNames = new Set(currentLabels.map(l => l.name)); + const hasConflictLabel = currentLabelNames.has(conflictLabel); + + // Ensure the conflict label exists in the repo + async function ensureLabelExists() { + try { + await github.rest.issues.getLabel({ owner, repo, name: conflictLabel }); + } catch (e) { + if (e.status === 404) { + await github.rest.issues.createLabel({ + owner, + repo, + name: conflictLabel, + color: conflictLabelColor, + description: conflictLabelDescription, + }); + core.info(`Created label: ${conflictLabel}`); + } else { + throw e; + } } - pullRequests = [pr]; } - // Process each PR - for (const pr of pullRequests) { - const pull_number = pr.number; - core.info(`Processing PR #${pull_number}`); - - // Get the latest PR data to check mergeable state - const { data: prData } = await github.rest.pulls.get({ - owner, - repo, - pull_number, - }); - - const hasConflicts = prData.mergeable === false; - const isDraft = prData.draft; - const prAuthor = prData.user.login; - - core.info(`PR #${pull_number}: mergeable=${prData.mergeable}, draft=${isDraft}, conflicts=${hasConflicts}`); - - // Define the conflict label - const conflictLabel = 'has-conflicts'; - const conflictLabelColor = 'e74c3c'; // Red (project's preferred red color) - const conflictLabelDescription = 'PR has merge conflicts that need to be resolved'; - - // Get current labels on the PR - const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({ - owner, - repo, - issue_number: pull_number, - per_page: 100, - }); - const currentLabelNames = new Set(currentLabels.map(l => l.name)); - const hasConflictLabel = currentLabelNames.has(conflictLabel); - - // Ensure the conflict label exists in the repo - async function ensureLabelExists() { - try { - await github.rest.issues.getLabel({ owner, repo, name: conflictLabel }); - } catch (e) { - if (e.status === 404) { - await github.rest.issues.createLabel({ - owner, - repo, - name: conflictLabel, - color: conflictLabelColor, - description: conflictLabelDescription, - }); - core.info(`Created label: ${conflictLabel}`); - } else { - throw e; - } - } + await ensureLabelExists(); + + // Get existing comments to check if we already commented + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number: pull_number, + per_page: 100, + }); + + // Find our conflict comment (using a unique marker) + const conflictCommentMarker = ''; + const existingConflictComment = comments.find(comment => + comment.body && comment.body.includes(conflictCommentMarker) + ); + + if (hasConflicts) { + // Add the conflict label if not present + if (!hasConflictLabel) { + await github.rest.issues.addLabels({ + owner, + repo, + issue_number: pull_number, + labels: [conflictLabel], + }); + core.info(`Added label "${conflictLabel}" to PR #${pull_number}`); + } else { + core.info(`Label "${conflictLabel}" already present on PR #${pull_number}`); } - await ensureLabelExists(); - - // Get existing comments to check if we already commented - const { data: comments } = await github.rest.issues.listComments({ - owner, - repo, - issue_number: pull_number, - per_page: 100, - }); - - // Find our conflict comment (using a unique marker) - const conflictCommentMarker = ''; - const existingConflictComment = comments.find(comment => - comment.body && comment.body.includes(conflictCommentMarker) - ); - - if (hasConflicts) { - // Add the conflict label if not present - if (!hasConflictLabel) { - await github.rest.issues.addLabels({ + // Add a comment if one doesn't exist + if (!existingConflictComment) { + const commentBody = conflictCommentMarker + '\n' + + '⚠️ **Merge Conflicts Detected**\n\n' + + 'Hi @' + prAuthor + '!\n\n' + + 'This pull request has merge conflicts with the base branch that need to be resolved before it can be merged.\n\n' + + '**To resolve the conflicts:**\n\n' + + '1. Sync your branch with the base branch:\n' + + ' ```bash\n' + + ' git fetch origin\n' + + ' git merge origin/' + prData.base.ref + '\n' + + ' ```\n\n' + + '2. Resolve any conflicts in your editor\n\n' + + '3. Commit the changes:\n' + + ' ```bash\n' + + ' git add .\n' + + ' git commit -m "Resolve merge conflicts"\n' + + ' git push\n' + + ' ```\n\n' + + 'Once you push the resolved conflicts, this label and comment will be automatically updated.\n\n' + + 'Thank you! 🙏'; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pull_number, + body: commentBody, + }); + core.info(`Added conflict comment to PR #${pull_number}`); + } else { + core.info(`Conflict comment already exists on PR #${pull_number}`); + } + } else { + // No conflicts - remove label and comment if present + if (hasConflictLabel) { + try { + await github.rest.issues.removeLabel({ owner, repo, issue_number: pull_number, - labels: [conflictLabel], + name: conflictLabel, }); - core.info(`Added label "${conflictLabel}" to PR #${pull_number}`); - } else { - core.info(`Label "${conflictLabel}" already present on PR #${pull_number}`); + core.info(`Removed label "${conflictLabel}" from PR #${pull_number}`); + } catch (err) { + core.warning(`Failed to remove label "${conflictLabel}": ${err.message}`); } - - // Add a comment if one doesn't exist - if (!existingConflictComment) { - const commentBody = conflictCommentMarker + '\n' + - '⚠️ **Merge Conflicts Detected**\n\n' + - 'Hi @' + prAuthor + '!\n\n' + - 'This pull request has merge conflicts with the base branch that need to be resolved before it can be merged.\n\n' + - '**To resolve the conflicts:**\n\n' + - '1. Sync your branch with the base branch:\n' + - ' ```bash\n' + - ' git fetch origin\n' + - ' git merge origin/' + prData.base.ref + '\n' + - ' ```\n\n' + - '2. Resolve any conflicts in your editor\n\n' + - '3. Commit the changes:\n' + - ' ```bash\n' + - ' git add .\n' + - ' git commit -m "Resolve merge conflicts"\n' + - ' git push\n' + - ' ```\n\n' + - 'Once you push the resolved conflicts, this label and comment will be automatically updated.\n\n' + - 'Thank you! 🙏'; - - await github.rest.issues.createComment({ + } + + // Remove the conflict comment if it exists + if (existingConflictComment) { + try { + await github.rest.issues.deleteComment({ owner, repo, - issue_number: pull_number, - body: commentBody, + comment_id: existingConflictComment.id, }); - core.info(`Added conflict comment to PR #${pull_number}`); - } else { - core.info(`Conflict comment already exists on PR #${pull_number}`); - } - } else { - // No conflicts - remove label and comment if present - if (hasConflictLabel) { - try { - await github.rest.issues.removeLabel({ - owner, - repo, - issue_number: pull_number, - name: conflictLabel, - }); - core.info(`Removed label "${conflictLabel}" from PR #${pull_number}`); - } catch (err) { - core.warning(`Failed to remove label "${conflictLabel}": ${err.message}`); - } + core.info(`Removed conflict comment from PR #${pull_number}`); + } catch (err) { + core.warning(`Failed to remove comment: ${err.message}`); } - - // Remove the conflict comment if it exists - if (existingConflictComment) { - try { - await github.rest.issues.deleteComment({ - owner, - repo, - comment_id: existingConflictComment.id, - }); - core.info(`Removed conflict comment from PR #${pull_number}`); - } catch (err) { - core.warning(`Failed to remove comment: ${err.message}`); - } - } - - core.info(`PR #${pull_number} has no conflicts`); } + + core.info(`PR #${pull_number} has no conflicts`); }