-
Notifications
You must be signed in to change notification settings - Fork 8.2k
[release/v7.6] Fix merge conflict checker for empty file lists and filter *.cs files #26556
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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: <<<<<<<, =======, >>>>>>> | ||||||||||||||||||
|
Comment on lines
+70
to
+73
|
||||||||||||||||||
| - src/example.cs | |
| Markers found: <<<<<<<, =======, >>>>>>> | |
| - README.md | |
| Markers found: <<<<<<<, =======, >>>>>>> | |
| - README.md | |
| Markers found: <<<<<<<, =======, >>>>>>> | |
| - src/example.txt | |
| Markers found: <<<<<<<, =======, >>>>>>> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
| } | ||
| } | ||
|
Comment on lines
+138
to
+169
|
||
|
|
||
| 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 | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README doesn't explain why *.cs files are excluded from merge conflict checking. Based on the PR description mentioning "exclude *.cs files from merge conflict checking to avoid false positives during C# builds", this rationale should be documented.
Suggestion:
This helps users understand the reasoning behind this design decision.