Skip to content

Admin - GitHub Action: Merge conflict detector in an existing pull request due to merging another pull request #6364

@palisadoes

Description

@palisadoes

Is your feature request related to a problem? Please describe.

  1. After merging a PR in this repository, other PRs will often inherit merge conflicts which prevents merges in the GitHub API
  2. The author of the PR is often unaware of this as there is no messaging to notify them.

Describe the solution you'd like

  1. A GitHub action to detect unresolved merge conflicts in an existing pull request due to merging another pull request in the same repository using a GitHub action
  2. It must fail when there are conflicts found
  3. It must pass when none are found
  4. The approach will probably need to use the pull_request_target event and the GitHub CLI to check for conflicts when an update is made to the base branch (e.g., develop is updated by another PR merge)
  5. When complete this solution will need to be transferred to the PalisadoesFoundation/.github repository so that it can be applied to all repositories.
  6. There needs to be ample logging for troubleshooting purposes

Testing

You will need to do the following:

  1. Test this in your personal GitHub repository before submitting a PR
  2. Provide a video of it working in your repository in the submitted PR

Describe alternatives you've considered

  • N/A

Approach to be followed (optional)

  • See above

Additional context

This sample code is only meant as a guide.

name: PR File-Level Merge Conflict Detection

# Trigger on pull request events for the current PR only
on:
  pull_request_target:
    types: [opened, reopened, synchronize, ready_for_review]

permissions:
  contents: read
  pull-requests: read

jobs:
  detect-pr-file-conflicts:
    name: Check Current PR Files for Conflicts
    runs-on: ubuntu-latest
    
    steps:
      # Step 1: Checkout repository with full git history
      # Full history is required for git merge-base and merge-tree operations
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Fetch all history for all branches
          ref: ${{ github.event.pull_request.head.sha }}
      
      - name: Setup - Log environment
        run: |
          echo "[SETUP] ========================================"
          echo "[SETUP] Starting PR file-level conflict detection"
          echo "[SETUP] PR Number: ${{ github.event.pull_request.number }}"
          echo "[SETUP] PR Title: ${{ github.event.pull_request.title }}"
          echo "[SETUP] Base Branch: ${{ github.event.pull_request.base.ref }}"
          echo "[SETUP] Base SHA: ${{ github.event.pull_request.base.sha }}"
          echo "[SETUP] Head Branch: ${{ github.event.pull_request.head.ref }}"
          echo "[SETUP] Head SHA: ${{ github.event.pull_request.head.sha }}"
          echo "[SETUP] Current Git Ref: $(git rev-parse HEAD)"
          echo "[SETUP] ========================================"
      
      # Step 2: Fetch base branch for merge operations
      - name: Fetch base branch
        run: |
          echo "[SETUP] Fetching base branch: ${{ github.event.pull_request.base.ref }}"
          git fetch origin ${{ github.event.pull_request.base.ref }}:refs/remotes/origin/${{ github.event.pull_request.base.ref }}
          echo "[SETUP] Base branch fetched successfully"
      
      # Step 3: Get list of files changed in the current PR
      - name: Fetch PR changed files
        id: pr-files
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          echo "[FETCH] ========================================"
          echo "[FETCH] Fetching list of files changed in PR #${{ github.event.pull_request.number }}"
          
          # Get list of changed files using GitHub CLI
          gh pr view ${{ github.event.pull_request.number }} \
            --json files \
            --jq '.files[].path' > pr_files.txt
          
          # Count files
          FILE_COUNT=$(wc -l < pr_files.txt | tr -d ' ')
          echo "file_count=${FILE_COUNT}" >> $GITHUB_OUTPUT
          
          echo "[FETCH] Total files changed in PR: ${FILE_COUNT}"
          
          if [ "${FILE_COUNT}" -eq 0 ]; then
            echo "[FETCH] No files changed in PR - exiting with success"
            echo "has_files=false" >> $GITHUB_OUTPUT
            exit 0
          fi
          
          echo "has_files=true" >> $GITHUB_OUTPUT
          
          echo "[FETCH] Changed files:"
          cat pr_files.txt | while read file; do
            echo "[FETCH]   - ${file}"
          done
          echo "[FETCH] ========================================"
      
      # Step 4: Detect conflicts using git merge-tree
      - name: Detect conflicts with git merge-tree
        id: detect-conflicts
        if: steps.pr-files.outputs.has_files == 'true'
        run: |
          echo "[DETECT] ========================================"
          echo "[DETECT] Calculating merge-base"
          
          # Calculate merge-base
          MERGE_BASE=$(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }})
          echo "[DETECT] Merge-base: ${MERGE_BASE}"
          
          # Run git merge-tree to simulate merge and detect conflicts
          echo "[DETECT] Running git merge-tree to detect conflicts"
          echo "[DETECT] Command: git merge-tree ${MERGE_BASE} ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}"
          
          # Capture merge-tree output and exit code
          set +e
          MERGE_OUTPUT=$(git merge-tree ${MERGE_BASE} ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }} 2>&1)
          MERGE_EXIT_CODE=$?
          set -e
          
          echo "[DETECT] Merge-tree exit code: ${MERGE_EXIT_CODE}"
          
          # Parse conflicted files from merge-tree output
          # Conflicts are indicated by lines starting with "changed in both"
          echo "${MERGE_OUTPUT}" | grep -E "^\+\+\+|^\-\-\-|^changed in both" | \
            grep -oP "(?<=^changed in both\s+)\S+" | \
            sort -u > all_conflicts.txt || touch all_conflicts.txt
          
          # Alternative: look for conflict markers in the merge output
          echo "${MERGE_OUTPUT}" | awk '/^CONFLICT \(content\):/ {
            for (i=3; i<=NF; i++) {
              if ($i ~ /^[^[:space:]]+$/ && $i !~ /^in$|^and$|^Merge$|^conflict$/) {
                print $i
              }
            }
          }' | sort -u >> all_conflicts.txt || true
          
          # Remove duplicates
          sort -u all_conflicts.txt > all_conflicts_unique.txt
          mv all_conflicts_unique.txt all_conflicts.txt
          
          TOTAL_CONFLICTS=$(wc -l < all_conflicts.txt | tr -d ' ')
          echo "[DETECT] Total files with conflicts (from merge-tree): ${TOTAL_CONFLICTS}"
          
          if [ "${TOTAL_CONFLICTS}" -gt 0 ]; then
            echo "[DETECT] All conflicted files:"
            cat all_conflicts.txt | while read file; do
              echo "[DETECT]   - ${file}"
            done
          else
            echo "[DETECT] No conflicts detected by merge-tree"
          fi
          
          echo "total_conflicts=${TOTAL_CONFLICTS}" >> $GITHUB_OUTPUT
          echo "[DETECT] ========================================"
      
      # Step 5: Filter conflicts to only PR's changed files
      - name: Filter conflicts to PR files
        id: filter-conflicts
        if: steps.pr-files.outputs.has_files == 'true'
        run: |
          echo "[DETECT] ========================================"
          echo "[DETECT] Filtering conflicts to only PR's changed files"
          
          # Initialize empty file for filtered conflicts
          touch pr_conflicts.txt
          
          # Cross-reference PR files with conflicted files
          if [ -f all_conflicts.txt ] && [ -s all_conflicts.txt ]; then
            while IFS= read -r conflict_file; do
              # Check if this conflicted file is in the PR's changed files
              if grep -Fxq "${conflict_file}" pr_files.txt; then
                echo "${conflict_file}" >> pr_conflicts.txt
              fi
            done < all_conflicts.txt
          fi
          
          # Count filtered conflicts
          if [ -f pr_conflicts.txt ] && [ -s pr_conflicts.txt ]; then
            FILTERED_COUNT=$(wc -l < pr_conflicts.txt | tr -d ' ')
          else
            FILTERED_COUNT=0
          fi
          
          echo "filtered_count=${FILTERED_COUNT}" >> $GITHUB_OUTPUT
          
          echo "[DETECT] Files with conflicts in this PR: ${FILTERED_COUNT}"
          
          if [ "${FILTERED_COUNT}" -gt 0 ]; then
            echo "[DETECT] Conflicted files in PR #${{ github.event.pull_request.number }}:"
            cat pr_conflicts.txt | while read file; do
              echo "[DETECT]   ⚠️  ${file}"
            done
          else
            echo "[DETECT] ✅ No conflicts found in PR's changed files"
          fi
          
          echo "[DETECT] ========================================"
      
      # Step 6: Generate detailed report and determine exit status
      - name: Report results
        if: steps.pr-files.outputs.has_files == 'true'
        run: |
          echo "[RESULT] ========================================"
          echo "[RESULT] CONFLICT DETECTION SUMMARY"
          echo "[RESULT] ========================================"
          echo "[RESULT] PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}"
          echo "[RESULT] Base: ${{ github.event.pull_request.base.ref }} @ ${{ github.event.pull_request.base.sha }}"
          echo "[RESULT] Head: ${{ github.event.pull_request.head.ref }} @ ${{ github.event.pull_request.head.sha }}"
          echo "[RESULT] ----------------------------------------"
          echo "[RESULT] Total files changed in PR: ${{ steps.pr-files.outputs.file_count }}"
          echo "[RESULT] Total files with conflicts (all): ${{ steps.detect-conflicts.outputs.total_conflicts || '0' }}"
          echo "[RESULT] Files with conflicts in PR: ${{ steps.filter-conflicts.outputs.filtered_count }}"
          echo "[RESULT] ========================================"
          
          FILTERED_COUNT=${{ steps.filter-conflicts.outputs.filtered_count }}
          
          if [ "${FILTERED_COUNT}" -eq 0 ]; then
            echo "[RESULT] ✅ STATUS: PASSED"
            echo "[RESULT] No merge conflicts detected in this PR's files"
            echo "[RESULT] This PR can be merged (subject to other checks)"
            echo "[RESULT] ========================================"
            exit 0
          else
            echo "[RESULT] ❌ STATUS: FAILED"
            echo "[RESULT] Merge conflicts detected in ${FILTERED_COUNT} file(s)"
            echo "[RESULT]"
            echo "[RESULT] The following files in this PR have merge conflicts:"
            cat pr_conflicts.txt | while read file; do
              echo "[RESULT]   ⚠️  ${file}"
            done
            echo "[RESULT]"
            echo "[RESULT] ACTION REQUIRED:"
            echo "[RESULT] Please update your branch to resolve these conflicts:"
            echo "[RESULT]   1. Merge the latest '${{ github.event.pull_request.base.ref }}' into your branch, OR"
            echo "[RESULT]   2. Rebase your branch on top of '${{ github.event.pull_request.base.ref }}'"
            echo "[RESULT]"
            echo "[RESULT] Commands:"
            echo "[RESULT]   git fetch origin"
            echo "[RESULT]   git checkout ${{ github.event.pull_request.head.ref }}"
            echo "[RESULT]   git merge origin/${{ github.event.pull_request.base.ref }}"
            echo "[RESULT]   # OR: git rebase origin/${{ github.event.pull_request.base.ref }}"
            echo "[RESULT] ========================================"
            exit 1
          fi
      
      - name: Success - No files in PR
        if: steps.pr-files.outputs.has_files == 'false'
        run: |
          echo "[RESULT] ✅ STATUS: PASSED"
          echo "[RESULT] No files changed in PR - nothing to check"

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions