Skip to content

Commit c63af4e

Browse files
[release/v7.6] Fix merge conflict checker for empty file lists and filter *.cs files (#26556)
1 parent 0b313f7 commit c63af4e

File tree

4 files changed

+592
-1
lines changed

4 files changed

+592
-1
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Merge Conflict Checker
2+
3+
This composite GitHub Action checks for Git merge conflict markers in files changed in pull requests.
4+
5+
## Purpose
6+
7+
Automatically detects leftover merge conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) in pull request files to prevent them from being merged into the codebase.
8+
9+
## Usage
10+
11+
### In a Workflow
12+
13+
```yaml
14+
- name: Check for merge conflict markers
15+
uses: "./.github/actions/infrastructure/merge-conflict-checker"
16+
```
17+
18+
### Complete Example
19+
20+
```yaml
21+
jobs:
22+
merge_conflict_check:
23+
name: Check for Merge Conflict Markers
24+
runs-on: ubuntu-latest
25+
if: github.event_name == 'pull_request'
26+
permissions:
27+
pull-requests: read
28+
contents: read
29+
steps:
30+
- name: checkout
31+
uses: actions/checkout@v5
32+
33+
- name: Check for merge conflict markers
34+
uses: "./.github/actions/infrastructure/merge-conflict-checker"
35+
```
36+
37+
## How It Works
38+
39+
1. **File Detection**: Uses GitHub's API to get the list of files changed in the pull request
40+
2. **Marker Scanning**: Reads each changed file and searches for the following markers:
41+
- `<<<<<<<` (conflict start marker)
42+
- `=======` (conflict separator)
43+
- `>>>>>>>` (conflict end marker)
44+
3. **Result Reporting**:
45+
- If markers are found, the action fails and lists all affected files
46+
- If no markers are found, the action succeeds
47+
48+
## Outputs
49+
50+
- `files-checked`: Number of files that were checked
51+
- `conflicts-found`: Number of files containing merge conflict markers
52+
53+
## Behavior
54+
55+
- **Event Support**: Only works with `pull_request` events
56+
- **File Handling**:
57+
- Checks only files that were added, modified, or renamed
58+
- Skips deleted files
59+
- **Filters out `*.cs` files** (C# files are excluded from merge conflict checking)
60+
- Skips binary/unreadable files
61+
- Skips directories
62+
- **Empty File List**: Gracefully handles cases where no files need checking (e.g., PRs that only delete files)
63+
64+
## Example Output
65+
66+
When conflict markers are detected:
67+
68+
```
69+
❌ Merge conflict markers detected in the following files:
70+
- src/example.cs
71+
Markers found: <<<<<<<, =======, >>>>>>>
72+
- README.md
73+
Markers found: <<<<<<<, =======, >>>>>>>
74+
75+
Please resolve these conflicts before merging.
76+
```
77+
78+
When no markers are found:
79+
80+
```
81+
✅ No merge conflict markers found
82+
```
83+
84+
## Integration
85+
86+
This action is integrated into the `linux-ci.yml` workflow and runs automatically on all pull requests to ensure code quality before merging.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: 'Check for Merge Conflict Markers'
2+
description: 'Checks for Git merge conflict markers in changed files for pull requests'
3+
author: 'PowerShell Team'
4+
5+
outputs:
6+
files-checked:
7+
description: 'Number of files checked for merge conflict markers'
8+
value: ${{ steps.check.outputs.files-checked }}
9+
conflicts-found:
10+
description: 'Number of files with merge conflict markers'
11+
value: ${{ steps.check.outputs.conflicts-found }}
12+
13+
runs:
14+
using: 'composite'
15+
steps:
16+
- name: Get changed files
17+
id: changed-files
18+
uses: "./.github/actions/infrastructure/get-changed-files"
19+
20+
- name: Check for merge conflict markers
21+
id: check
22+
shell: pwsh
23+
env:
24+
CHANGED_FILES_JSON: ${{ steps.changed-files.outputs.files }}
25+
run: |
26+
# Get changed files from environment variable (secure against injection)
27+
$changedFilesJson = $env:CHANGED_FILES_JSON
28+
# Ensure we always have an array (ConvertFrom-Json returns null for empty JSON arrays)
29+
$changedFiles = @($changedFilesJson | ConvertFrom-Json)
30+
31+
# Import ci.psm1 and run the check
32+
Import-Module "$env:GITHUB_WORKSPACE/tools/ci.psm1" -Force
33+
Test-MergeConflictMarker -File $changedFiles -WorkspacePath $env:GITHUB_WORKSPACE
34+
35+
branding:
36+
icon: 'alert-triangle'
37+
color: 'red'
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
4+
# NOTE: This test file tests the Test-MergeConflictMarker function which detects Git merge conflict markers.
5+
# IMPORTANT: Do NOT use here-strings or literal conflict markers (e.g., "<<<<<<<", "=======", ">>>>>>>")
6+
# in this file, as they will trigger conflict marker detection in CI pipelines.
7+
# Instead, use string multiplication (e.g., '<' * 7) to dynamically generate these markers at runtime.
8+
9+
Describe "Test-MergeConflictMarker" {
10+
BeforeAll {
11+
# Import the module
12+
Import-Module "$PSScriptRoot/../../tools/ci.psm1" -Force
13+
14+
# Create a temporary test workspace
15+
$script:testWorkspace = Join-Path $TestDrive "workspace"
16+
New-Item -ItemType Directory -Path $script:testWorkspace -Force | Out-Null
17+
18+
# Create temporary output files
19+
$script:testOutputPath = Join-Path $TestDrive "outputs.txt"
20+
$script:testSummaryPath = Join-Path $TestDrive "summary.md"
21+
}
22+
23+
AfterEach {
24+
# Clean up test files after each test
25+
if (Test-Path $script:testWorkspace) {
26+
Get-ChildItem $script:testWorkspace -File -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
27+
}
28+
Remove-Item $script:testOutputPath -Force -ErrorAction SilentlyContinue
29+
Remove-Item $script:testSummaryPath -Force -ErrorAction SilentlyContinue
30+
}
31+
32+
Context "When no files are provided" {
33+
It "Should handle empty file array gracefully" {
34+
# The function now accepts empty arrays to handle cases like delete-only PRs
35+
$emptyArray = @()
36+
Test-MergeConflictMarker -File $emptyArray -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
37+
38+
$outputs = Get-Content $script:testOutputPath
39+
$outputs | Should -Contain "files-checked=0"
40+
$outputs | Should -Contain "conflicts-found=0"
41+
42+
$summary = Get-Content $script:testSummaryPath -Raw
43+
$summary | Should -Match "No Files to Check"
44+
}
45+
}
46+
47+
Context "When files have no conflicts" {
48+
It "Should pass for clean files" {
49+
$testFile = Join-Path $script:testWorkspace "clean.txt"
50+
"This is a clean file" | Out-File $testFile -Encoding utf8
51+
52+
Test-MergeConflictMarker -File @("clean.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
53+
54+
$outputs = Get-Content $script:testOutputPath
55+
$outputs | Should -Contain "files-checked=1"
56+
$outputs | Should -Contain "conflicts-found=0"
57+
58+
$summary = Get-Content $script:testSummaryPath -Raw
59+
$summary | Should -Match "No Conflicts Found"
60+
}
61+
}
62+
63+
Context "When files have conflict markers" {
64+
It "Should detect <<<<<<< marker" {
65+
$testFile = Join-Path $script:testWorkspace "conflict1.txt"
66+
"Some content`n" + ('<' * 7) + " HEAD`nConflicting content" | Out-File $testFile -Encoding utf8
67+
68+
{ Test-MergeConflictMarker -File @("conflict1.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
69+
70+
$outputs = Get-Content $script:testOutputPath
71+
$outputs | Should -Contain "files-checked=1"
72+
$outputs | Should -Contain "conflicts-found=1"
73+
}
74+
75+
It "Should detect ======= marker" {
76+
$testFile = Join-Path $script:testWorkspace "conflict2.txt"
77+
"Some content`n" + ('=' * 7) + "`nMore content" | Out-File $testFile -Encoding utf8
78+
79+
{ Test-MergeConflictMarker -File @("conflict2.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
80+
}
81+
82+
It "Should detect >>>>>>> marker" {
83+
$testFile = Join-Path $script:testWorkspace "conflict3.txt"
84+
"Some content`n" + ('>' * 7) + " branch-name`nMore content" | Out-File $testFile -Encoding utf8
85+
86+
{ Test-MergeConflictMarker -File @("conflict3.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
87+
}
88+
89+
It "Should detect multiple markers in one file" {
90+
$testFile = Join-Path $script:testWorkspace "conflict4.txt"
91+
$content = "Some content`n" + ('<' * 7) + " HEAD`nContent A`n" + ('=' * 7) + "`nContent B`n" + ('>' * 7) + " branch`nMore content"
92+
$content | Out-File $testFile -Encoding utf8
93+
94+
{ Test-MergeConflictMarker -File @("conflict4.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
95+
96+
$summary = Get-Content $script:testSummaryPath -Raw
97+
$summary | Should -Match "Conflicts Detected"
98+
$summary | Should -Match "conflict4.txt"
99+
}
100+
101+
It "Should detect conflicts in multiple files" {
102+
$testFile1 = Join-Path $script:testWorkspace "conflict5.txt"
103+
('<' * 7) + " HEAD" | Out-File $testFile1 -Encoding utf8
104+
105+
$testFile2 = Join-Path $script:testWorkspace "conflict6.txt"
106+
('=' * 7) | Out-File $testFile2 -Encoding utf8
107+
108+
{ Test-MergeConflictMarker -File @("conflict5.txt", "conflict6.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
109+
110+
$outputs = Get-Content $script:testOutputPath
111+
$outputs | Should -Contain "files-checked=2"
112+
$outputs | Should -Contain "conflicts-found=2"
113+
}
114+
}
115+
116+
Context "When markers are not at line start" {
117+
It "Should not detect markers in middle of line" {
118+
$testFile = Join-Path $script:testWorkspace "notconflict.txt"
119+
"This line has <<<<<<< in the middle" | Out-File $testFile -Encoding utf8
120+
121+
Test-MergeConflictMarker -File @("notconflict.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
122+
123+
$outputs = Get-Content $script:testOutputPath
124+
$outputs | Should -Contain "conflicts-found=0"
125+
}
126+
127+
It "Should not detect markers with wrong number of characters" {
128+
$testFile = Join-Path $script:testWorkspace "wrongcount.txt"
129+
('<' * 6) + " Only 6`n" + ('<' * 8) + " 8 characters" | Out-File $testFile -Encoding utf8
130+
131+
Test-MergeConflictMarker -File @("wrongcount.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
132+
133+
$outputs = Get-Content $script:testOutputPath
134+
$outputs | Should -Contain "conflicts-found=0"
135+
}
136+
}
137+
138+
Context "When handling special file scenarios" {
139+
It "Should skip non-existent files" {
140+
Test-MergeConflictMarker -File @("nonexistent.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
141+
142+
$outputs = Get-Content $script:testOutputPath
143+
$outputs | Should -Contain "files-checked=0"
144+
}
145+
146+
It "Should handle absolute paths" {
147+
$testFile = Join-Path $script:testWorkspace "absolute.txt"
148+
"Clean content" | Out-File $testFile -Encoding utf8
149+
150+
Test-MergeConflictMarker -File @($testFile) -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
151+
152+
$outputs = Get-Content $script:testOutputPath
153+
$outputs | Should -Contain "conflicts-found=0"
154+
}
155+
156+
It "Should handle mixed relative and absolute paths" {
157+
$testFile1 = Join-Path $script:testWorkspace "relative.txt"
158+
"Clean" | Out-File $testFile1 -Encoding utf8
159+
160+
$testFile2 = Join-Path $script:testWorkspace "absolute.txt"
161+
"Clean" | Out-File $testFile2 -Encoding utf8
162+
163+
Test-MergeConflictMarker -File @("relative.txt", $testFile2) -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
164+
165+
$outputs = Get-Content $script:testOutputPath
166+
$outputs | Should -Contain "files-checked=2"
167+
$outputs | Should -Contain "conflicts-found=0"
168+
}
169+
}
170+
171+
Context "When summary and output generation" {
172+
It "Should generate proper GitHub Actions outputs format" {
173+
$testFile = Join-Path $script:testWorkspace "test.txt"
174+
"Clean file" | Out-File $testFile -Encoding utf8
175+
176+
Test-MergeConflictMarker -File @("test.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath
177+
178+
$outputs = Get-Content $script:testOutputPath
179+
$outputs | Where-Object {$_ -match "^files-checked=\d+$"} | Should -Not -BeNullOrEmpty
180+
$outputs | Where-Object {$_ -match "^conflicts-found=\d+$"} | Should -Not -BeNullOrEmpty
181+
}
182+
183+
It "Should generate markdown summary with conflict details" {
184+
$testFile = Join-Path $script:testWorkspace "marked.txt"
185+
$content = "Line 1`n" + ('<' * 7) + " HEAD`nLine 3`n" + ('=' * 7) + "`nLine 5"
186+
$content | Out-File $testFile -Encoding utf8
187+
188+
{ Test-MergeConflictMarker -File @("marked.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw
189+
190+
$summary = Get-Content $script:testSummaryPath -Raw
191+
$summary | Should -Match "# Merge Conflict Marker Check Results"
192+
$summary | Should -Match "marked.txt"
193+
$summary | Should -Match "\| Line \| Marker \|"
194+
}
195+
}
196+
}
197+
198+
Describe "Install-CIPester" {
199+
BeforeAll {
200+
# Import the module
201+
Import-Module "$PSScriptRoot/../../tools/ci.psm1" -Force
202+
}
203+
204+
Context "When checking function exists" {
205+
It "Should export Install-CIPester function" {
206+
$function = Get-Command Install-CIPester -ErrorAction SilentlyContinue
207+
$function | Should -Not -BeNullOrEmpty
208+
$function.ModuleName | Should -Be 'ci'
209+
}
210+
211+
It "Should have expected parameters" {
212+
$function = Get-Command Install-CIPester
213+
$function.Parameters.Keys | Should -Contain 'MinimumVersion'
214+
$function.Parameters.Keys | Should -Contain 'MaximumVersion'
215+
$function.Parameters.Keys | Should -Contain 'Force'
216+
}
217+
218+
It "Should accept version parameters" {
219+
$function = Get-Command Install-CIPester
220+
$function.Parameters['MinimumVersion'].ParameterType.Name | Should -Be 'String'
221+
$function.Parameters['MaximumVersion'].ParameterType.Name | Should -Be 'String'
222+
$function.Parameters['Force'].ParameterType.Name | Should -Be 'SwitchParameter'
223+
}
224+
}
225+
226+
Context "When validating real execution" {
227+
# These tests only run in CI where we can safely install/test Pester
228+
229+
It "Should successfully run without errors when Pester exists" {
230+
if (!$env:CI) {
231+
Set-ItResult -Skipped -Because "Test requires CI environment to safely install Pester"
232+
}
233+
234+
{ Install-CIPester -ErrorAction Stop } | Should -Not -Throw
235+
}
236+
237+
It "Should accept custom version parameters" {
238+
if (!$env:CI) {
239+
Set-ItResult -Skipped -Because "Test requires CI environment to safely install Pester"
240+
}
241+
242+
{ Install-CIPester -MinimumVersion '4.0.0' -MaximumVersion '5.99.99' -ErrorAction Stop } | Should -Not -Throw
243+
}
244+
}
245+
}
246+

0 commit comments

Comments
 (0)