[release/v7.6] Fix merge conflict checker for empty file lists and filter *.cs files (#26556)

This commit is contained in:
Aditya Patwardhan
2025-12-02 15:56:04 -08:00
committed by GitHub
parent 0b313f7111
commit c63af4e2c9
4 changed files with 592 additions and 1 deletions
@@ -0,0 +1,86 @@
# Merge Conflict Checker
This composite GitHub Action checks for Git merge conflict markers in files changed in pull requests.
## Purpose
Automatically detects leftover merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) in pull request files to prevent them from being merged into the codebase.
## Usage
### In a Workflow
```yaml
- name: Check for merge conflict markers
uses: "./.github/actions/infrastructure/merge-conflict-checker"
```
### Complete Example
```yaml
jobs:
merge_conflict_check:
name: Check for Merge Conflict Markers
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
permissions:
pull-requests: read
contents: read
steps:
- name: checkout
uses: actions/checkout@v5
- name: Check for merge conflict markers
uses: "./.github/actions/infrastructure/merge-conflict-checker"
```
## How It Works
1. **File Detection**: Uses GitHub's API to get the list of files changed in the pull request
2. **Marker Scanning**: Reads each changed file and searches for the following markers:
- `<<<<<<<` (conflict start marker)
- `=======` (conflict separator)
- `>>>>>>>` (conflict end marker)
3. **Result Reporting**:
- If markers are found, the action fails and lists all affected files
- If no markers are found, the action succeeds
## Outputs
- `files-checked`: Number of files that were checked
- `conflicts-found`: Number of files containing merge conflict markers
## Behavior
- **Event Support**: Only works with `pull_request` events
- **File Handling**:
- Checks only files that were added, modified, or renamed
- Skips deleted files
- **Filters out `*.cs` files** (C# files are excluded from merge conflict checking)
- Skips binary/unreadable files
- Skips directories
- **Empty File List**: Gracefully handles cases where no files need checking (e.g., PRs that only delete files)
## Example Output
When conflict markers are detected:
```
❌ Merge conflict markers detected in the following files:
- src/example.cs
Markers found: <<<<<<<, =======, >>>>>>>
- README.md
Markers found: <<<<<<<, =======, >>>>>>>
Please resolve these conflicts before merging.
```
When no markers are found:
```
✅ No merge conflict markers found
```
## Integration
This action is integrated into the `linux-ci.yml` workflow and runs automatically on all pull requests to ensure code quality before merging.
@@ -0,0 +1,37 @@
name: 'Check for Merge Conflict Markers'
description: 'Checks for Git merge conflict markers in changed files for pull requests'
author: 'PowerShell Team'
outputs:
files-checked:
description: 'Number of files checked for merge conflict markers'
value: ${{ steps.check.outputs.files-checked }}
conflicts-found:
description: 'Number of files with merge conflict markers'
value: ${{ steps.check.outputs.conflicts-found }}
runs:
using: 'composite'
steps:
- name: Get changed files
id: changed-files
uses: "./.github/actions/infrastructure/get-changed-files"
- name: Check for merge conflict markers
id: check
shell: pwsh
env:
CHANGED_FILES_JSON: ${{ steps.changed-files.outputs.files }}
run: |
# Get changed files from environment variable (secure against injection)
$changedFilesJson = $env:CHANGED_FILES_JSON
# Ensure we always have an array (ConvertFrom-Json returns null for empty JSON arrays)
$changedFiles = @($changedFilesJson | ConvertFrom-Json)
# Import ci.psm1 and run the check
Import-Module "$env:GITHUB_WORKSPACE/tools/ci.psm1" -Force
Test-MergeConflictMarker -File $changedFiles -WorkspacePath $env:GITHUB_WORKSPACE
branding:
icon: 'alert-triangle'
color: 'red'
+246
View File
@@ -0,0 +1,246 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# NOTE: This test file tests the Test-MergeConflictMarker function which detects Git merge conflict markers.
# IMPORTANT: Do NOT use here-strings or literal conflict markers (e.g., "<<<<<<<", "=======", ">>>>>>>")
# in this file, as they will trigger conflict marker detection in CI pipelines.
# Instead, use string multiplication (e.g., '<' * 7) to dynamically generate these markers at runtime.
Describe "Test-MergeConflictMarker" {
BeforeAll {
# Import the module
Import-Module "$PSScriptRoot/../../tools/ci.psm1" -Force
# Create a temporary test workspace
$script:testWorkspace = Join-Path $TestDrive "workspace"
New-Item -ItemType Directory -Path $script:testWorkspace -Force | Out-Null
# Create temporary output files
$script:testOutputPath = Join-Path $TestDrive "outputs.txt"
$script:testSummaryPath = Join-Path $TestDrive "summary.md"
}
AfterEach {
# Clean up test files after each test
if (Test-Path $script:testWorkspace) {
Get-ChildItem $script:testWorkspace -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
}
Remove-Item $script:testOutputPath -Force -ErrorAction SilentlyContinue
Remove-Item $script:testSummaryPath -Force -ErrorAction SilentlyContinue
}
Context "When no files are provided" {
It "Should handle empty file array gracefully" {
# The function now accepts empty arrays to handle cases like delete-only PRs
$emptyArray = @()
Test-MergeConflictMarker -File $emptyArray -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "files-checked=0"
$outputs | Should -Contain "conflicts-found=0"
$summary = Get-Content $script:testSummaryPath -Raw
$summary | Should -Match "No Files to Check"
}
}
Context "When files have no conflicts" {
It "Should pass for clean files" {
$testFile = Join-Path $script:testWorkspace "clean.txt"
"This is a clean file" | Out-File $testFile -Encoding utf8
Test-MergeConflictMarker -File @("clean.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "files-checked=1"
$outputs | Should -Contain "conflicts-found=0"
$summary = Get-Content $script:testSummaryPath -Raw
$summary | Should -Match "No Conflicts Found"
}
}
Context "When files have conflict markers" {
It "Should detect <<<<<<< marker" {
$testFile = Join-Path $script:testWorkspace "conflict1.txt"
"Some content`n" + ('<' * 7) + " HEAD`nConflicting content" | Out-File $testFile -Encoding utf8
{ Test-MergeConflictMarker -File @("conflict1.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "files-checked=1"
$outputs | Should -Contain "conflicts-found=1"
}
It "Should detect ======= marker" {
$testFile = Join-Path $script:testWorkspace "conflict2.txt"
"Some content`n" + ('=' * 7) + "`nMore content" | Out-File $testFile -Encoding utf8
{ Test-MergeConflictMarker -File @("conflict2.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
}
It "Should detect >>>>>>> marker" {
$testFile = Join-Path $script:testWorkspace "conflict3.txt"
"Some content`n" + ('>' * 7) + " branch-name`nMore content" | Out-File $testFile -Encoding utf8
{ Test-MergeConflictMarker -File @("conflict3.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
}
It "Should detect multiple markers in one file" {
$testFile = Join-Path $script:testWorkspace "conflict4.txt"
$content = "Some content`n" + ('<' * 7) + " HEAD`nContent A`n" + ('=' * 7) + "`nContent B`n" + ('>' * 7) + " branch`nMore content"
$content | Out-File $testFile -Encoding utf8
{ Test-MergeConflictMarker -File @("conflict4.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
$summary = Get-Content $script:testSummaryPath -Raw
$summary | Should -Match "Conflicts Detected"
$summary | Should -Match "conflict4.txt"
}
It "Should detect conflicts in multiple files" {
$testFile1 = Join-Path $script:testWorkspace "conflict5.txt"
('<' * 7) + " HEAD" | Out-File $testFile1 -Encoding utf8
$testFile2 = Join-Path $script:testWorkspace "conflict6.txt"
('=' * 7) | Out-File $testFile2 -Encoding utf8
{ Test-MergeConflictMarker -File @("conflict5.txt", "conflict6.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "files-checked=2"
$outputs | Should -Contain "conflicts-found=2"
}
}
Context "When markers are not at line start" {
It "Should not detect markers in middle of line" {
$testFile = Join-Path $script:testWorkspace "notconflict.txt"
"This line has <<<<<<< in the middle" | Out-File $testFile -Encoding utf8
Test-MergeConflictMarker -File @("notconflict.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "conflicts-found=0"
}
It "Should not detect markers with wrong number of characters" {
$testFile = Join-Path $script:testWorkspace "wrongcount.txt"
('<' * 6) + " Only 6`n" + ('<' * 8) + " 8 characters" | Out-File $testFile -Encoding utf8
Test-MergeConflictMarker -File @("wrongcount.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "conflicts-found=0"
}
}
Context "When handling special file scenarios" {
It "Should skip non-existent files" {
Test-MergeConflictMarker -File @("nonexistent.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "files-checked=0"
}
It "Should handle absolute paths" {
$testFile = Join-Path $script:testWorkspace "absolute.txt"
"Clean content" | Out-File $testFile -Encoding utf8
Test-MergeConflictMarker -File @($testFile) -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "conflicts-found=0"
}
It "Should handle mixed relative and absolute paths" {
$testFile1 = Join-Path $script:testWorkspace "relative.txt"
"Clean" | Out-File $testFile1 -Encoding utf8
$testFile2 = Join-Path $script:testWorkspace "absolute.txt"
"Clean" | Out-File $testFile2 -Encoding utf8
Test-MergeConflictMarker -File @("relative.txt", $testFile2) -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Should -Contain "files-checked=2"
$outputs | Should -Contain "conflicts-found=0"
}
}
Context "When summary and output generation" {
It "Should generate proper GitHub Actions outputs format" {
$testFile = Join-Path $script:testWorkspace "test.txt"
"Clean file" | Out-File $testFile -Encoding utf8
Test-MergeConflictMarker -File @("test.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
$outputs = Get-Content $script:testOutputPath
$outputs | Where-Object {$_ -match "^files-checked=\d+$"} | Should -Not -BeNullOrEmpty
$outputs | Where-Object {$_ -match "^conflicts-found=\d+$"} | Should -Not -BeNullOrEmpty
}
It "Should generate markdown summary with conflict details" {
$testFile = Join-Path $script:testWorkspace "marked.txt"
$content = "Line 1`n" + ('<' * 7) + " HEAD`nLine 3`n" + ('=' * 7) + "`nLine 5"
$content | Out-File $testFile -Encoding utf8
{ Test-MergeConflictMarker -File @("marked.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
$summary = Get-Content $script:testSummaryPath -Raw
$summary | Should -Match "# Merge Conflict Marker Check Results"
$summary | Should -Match "marked.txt"
$summary | Should -Match "\| Line \| Marker \|"
}
}
}
Describe "Install-CIPester" {
BeforeAll {
# Import the module
Import-Module "$PSScriptRoot/../../tools/ci.psm1" -Force
}
Context "When checking function exists" {
It "Should export Install-CIPester function" {
$function = Get-Command Install-CIPester -ErrorAction SilentlyContinue
$function | Should -Not -BeNullOrEmpty
$function.ModuleName | Should -Be 'ci'
}
It "Should have expected parameters" {
$function = Get-Command Install-CIPester
$function.Parameters.Keys | Should -Contain 'MinimumVersion'
$function.Parameters.Keys | Should -Contain 'MaximumVersion'
$function.Parameters.Keys | Should -Contain 'Force'
}
It "Should accept version parameters" {
$function = Get-Command Install-CIPester
$function.Parameters['MinimumVersion'].ParameterType.Name | Should -Be 'String'
$function.Parameters['MaximumVersion'].ParameterType.Name | Should -Be 'String'
$function.Parameters['Force'].ParameterType.Name | Should -Be 'SwitchParameter'
}
}
Context "When validating real execution" {
# These tests only run in CI where we can safely install/test Pester
It "Should successfully run without errors when Pester exists" {
if (!$env:CI) {
Set-ItResult -Skipped -Because "Test requires CI environment to safely install Pester"
}
{ Install-CIPester -ErrorAction Stop } | Should -Not -Throw
}
It "Should accept custom version parameters" {
if (!$env:CI) {
Set-ItResult -Skipped -Because "Test requires CI environment to safely install Pester"
}
{ Install-CIPester -MinimumVersion '4.0.0' -MaximumVersion '5.99.99' -ErrorAction Stop } | Should -Not -Throw
}
}
}
+223 -1
View File
@@ -972,8 +972,230 @@ function Invoke-InitializeContainerStage {
Write-Host "##vso[build.updatebuildnumber]PR-${env:SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-$($selectedImage.JobName)-$((get-date).ToString("yyyyMMddhhmmss"))"
} else {
Write-Host "##vso[build.updatebuildnumber]${env:BUILD_SOURCEBRANCHNAME}-$($selectedImage.JobName)-${env:BUILD_SOURCEVERSION}-$((get-date).ToString("yyyyMMddhhmmss"))"
# Cannot do this for a PR
Write-Host "##vso[build.addbuildtag]$($selectedImage.JobName)"
}
}
Function Test-MergeConflictMarker
{
<#
.SYNOPSIS
Checks files for Git merge conflict markers and outputs results for GitHub Actions.
.DESCRIPTION
Scans the specified files for Git merge conflict markers (<<<<<<<, =======, >>>>>>>)
and generates console output, GitHub Actions outputs, and job summary.
Designed for use in GitHub Actions workflows.
.PARAMETER File
Array of file paths (relative or absolute) to check for merge conflict markers.
.PARAMETER WorkspacePath
Base workspace path for resolving relative paths. Defaults to current directory.
.PARAMETER OutputPath
Path to write GitHub Actions outputs. Defaults to $env:GITHUB_OUTPUT.
.PARAMETER SummaryPath
Path to write GitHub Actions job summary. Defaults to $env:GITHUB_STEP_SUMMARY.
.EXAMPLE
Test-MergeConflictMarker -File @('file1.txt', 'file2.cs') -WorkspacePath $env:GITHUB_WORKSPACE
#>
[CmdletBinding()]
param(
[Parameter()]
[AllowEmptyCollection()]
[string[]] $File = @(),
[Parameter()]
[string] $WorkspacePath = $PWD,
[Parameter()]
[string] $OutputPath = $env:GITHUB_OUTPUT,
[Parameter()]
[string] $SummaryPath = $env:GITHUB_STEP_SUMMARY
)
Write-Host "Starting merge conflict marker check..." -ForegroundColor Cyan
# Helper function to write outputs when no files to check
function Write-NoFilesOutput {
param(
[string]$Message,
[string]$OutputPath,
[string]$SummaryPath
)
# Output results to GitHub Actions
if ($OutputPath) {
"files-checked=0" | Out-File -FilePath $OutputPath -Append -Encoding utf8
"conflicts-found=0" | Out-File -FilePath $OutputPath -Append -Encoding utf8
}
# Create GitHub Actions job summary
if ($SummaryPath) {
$summaryContent = @"
# Merge Conflict Marker Check Results
## Summary
- **Files Checked:** 0
- **Files with Conflicts:** 0
## ℹ️ No Files to Check
$Message
"@
$summaryContent | Out-File -FilePath $SummaryPath -Encoding utf8
}
}
# Handle empty file list (e.g., when PR only deletes files)
if ($File.Count -eq 0) {
Write-Host "No files to check (empty file list)" -ForegroundColor Yellow
Write-NoFilesOutput -Message "No files were provided for checking (this can happen when a PR only deletes files)." -OutputPath $OutputPath -SummaryPath $SummaryPath
return
}
# Filter out *.cs files from merge conflict checking
$filesToCheck = @($File | Where-Object { $_ -notlike "*.cs" })
$filteredCount = $File.Count - $filesToCheck.Count
if ($filteredCount -gt 0) {
Write-Host "Filtered out $filteredCount *.cs file(s) from merge conflict checking" -ForegroundColor Yellow
}
if ($filesToCheck.Count -eq 0) {
Write-Host "No files to check after filtering (all files were *.cs)" -ForegroundColor Yellow
Write-NoFilesOutput -Message "All $filteredCount file(s) were filtered out (*.cs files are excluded from merge conflict checking)." -OutputPath $OutputPath -SummaryPath $SummaryPath
return
}
Write-Host "Checking $($filesToCheck.Count) changed files for merge conflict markers" -ForegroundColor Cyan
# Convert relative paths to absolute paths for processing
$absolutePaths = $filesToCheck | ForEach-Object {
if ([System.IO.Path]::IsPathRooted($_)) {
$_
} else {
Join-Path $WorkspacePath $_
}
}
$filesWithConflicts = @()
$filesChecked = 0
foreach ($filePath in $absolutePaths) {
# Check if file exists (might be deleted)
if (-not (Test-Path $filePath)) {
Write-Verbose " Skipping deleted file: $filePath"
continue
}
# Skip binary files and directories
if ((Get-Item $filePath) -is [System.IO.DirectoryInfo]) {
continue
}
$filesChecked++
# Get relative path for display
$relativePath = if ($WorkspacePath -and $filePath.StartsWith($WorkspacePath)) {
$filePath.Substring($WorkspacePath.Length).TrimStart([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)
} else {
$filePath
}
Write-Host " Checking: $relativePath" -ForegroundColor Gray
# Search for conflict markers using Select-String
try {
# Git conflict markers are 7 characters followed by a space or end of line
# Regex pattern breakdown:
# ^ - Matches the start of a line
# (<{7}|={7}|>{7}) - Matches exactly 7 consecutive '<', '=', or '>' characters (Git conflict markers)
# (\s|$) - Ensures the marker is followed by whitespace or end of line
$pattern = '^(<{7}|={7}|>{7})(\s|$)'
$matchedLines = Select-String -Path $filePath -Pattern $pattern -AllMatches -ErrorAction Stop
if ($matchedLines) {
# Collect marker details with line numbers (Select-String provides LineNumber automatically)
$markerDetails = @()
foreach ($match in $matchedLines) {
$markerDetails += [PSCustomObject]@{
Marker = $match.Matches[0].Groups[1].Value
Line = $match.LineNumber
}
}
$filesWithConflicts += [PSCustomObject]@{
File = $relativePath
MarkerDetails = $markerDetails
}
Write-Host " ❌ CONFLICT MARKERS FOUND in $relativePath" -ForegroundColor Red
foreach ($detail in $markerDetails) {
Write-Host " Line $($detail.Line): $($detail.Marker)" -ForegroundColor Red
}
}
}
catch {
# Skip files that can't be read (likely binary)
Write-Verbose " Skipping unreadable file: $relativePath"
}
}
# Output results to GitHub Actions
if ($OutputPath) {
"files-checked=$filesChecked" | Out-File -FilePath $OutputPath -Append -Encoding utf8
"conflicts-found=$($filesWithConflicts.Count)" | Out-File -FilePath $OutputPath -Append -Encoding utf8
}
Write-Host "`nSummary:" -ForegroundColor Cyan
Write-Host " Files checked: $filesChecked" -ForegroundColor Cyan
Write-Host " Files with conflicts: $($filesWithConflicts.Count)" -ForegroundColor Cyan
# Create GitHub Actions job summary
if ($SummaryPath) {
$summaryContent = @"
# Merge Conflict Marker Check Results
## Summary
- **Files Checked:** $filesChecked
- **Files with Conflicts:** $($filesWithConflicts.Count)
"@
if ($filesWithConflicts.Count -gt 0) {
Write-Host "`n❌ Merge conflict markers detected in the following files:" -ForegroundColor Red
$summaryContent += "`n## ❌ Conflicts Detected`n`n"
$summaryContent += "The following files contain merge conflict markers:`n`n"
foreach ($fileInfo in $filesWithConflicts) {
Write-Host " - $($fileInfo.File)" -ForegroundColor Red
$summaryContent += "### 📄 ``$($fileInfo.File)```n`n"
$summaryContent += "| Line | Marker |`n"
$summaryContent += "|------|--------|`n"
foreach ($detail in $fileInfo.MarkerDetails) {
Write-Host " Line $($detail.Line): $($detail.Marker)" -ForegroundColor Red
$summaryContent += "| $($detail.Line) | ``$($detail.Marker)`` |`n"
}
$summaryContent += "`n"
}
$summaryContent += "`n**Action Required:** Please resolve these conflicts before merging.`n"
Write-Host "`nPlease resolve these conflicts before merging." -ForegroundColor Red
} else {
Write-Host "`n✅ No merge conflict markers found" -ForegroundColor Green
$summaryContent += "`n## ✅ No Conflicts Found`n`nAll checked files are free of merge conflict markers.`n"
}
$summaryContent | Out-File -FilePath $SummaryPath -Encoding utf8
}
# Exit with error if conflicts found
if ($filesWithConflicts.Count -gt 0) {
throw "Merge conflict markers detected in $($filesWithConflicts.Count) file(s)"
}
}