-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Open
Labels
Description
Is your feature request related to a problem? Please describe.
- After merging a PR in this repository, other PRs will often inherit merge conflicts which prevents merges in the GitHub API
- The author of the PR is often unaware of this as there is no messaging to notify them.
Describe the solution you'd like
- 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
- It must fail when there are conflicts found
- It must pass when none are found
- The approach will probably need to use the
pull_request_targetevent and theGitHubCLI to check for conflicts when an update is made to the base branch (e.g.,developis updated by another PR merge) - When complete this solution will need to be transferred to the
PalisadoesFoundation/.githubrepository so that it can be applied to all repositories. - There needs to be ample logging for troubleshooting purposes
Testing
You will need to do the following:
- Test this in your personal GitHub repository before submitting a PR
- 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"