Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions .github/actions/infrastructure/merge-conflict-checker/README.md
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)
Copy link

Copilot AI Dec 2, 2025

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:

  - **Filters out `*.cs` files** (C# files are excluded from merge conflict checking to avoid false positives from C# build-generated files)

This helps users understand the reasoning behind this design decision.

Suggested change
- **Filters out `*.cs` files** (C# files are excluded from merge conflict checking)
- **Filters out `*.cs` files** (C# files are excluded from merge conflict checking to avoid false positives from C# build-generated files, which may contain patterns resembling merge conflict markers)

Copilot uses AI. Check for mistakes.
- 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
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example output shows src/example.cs with conflict markers, but this contradicts the documented behavior that *.cs files are filtered out. Since C# files are excluded from merge conflict checking (as stated in line 59), they should not appear in the example output.

Update the example to show only non-cs files, for instance:

❌ Merge conflict markers detected in the following files:
  - README.md
    Markers found: <<<<<<<, =======, >>>>>>>
  - src/example.txt
    Markers found: <<<<<<<, =======, >>>>>>>
Suggested change
- src/example.cs
Markers found: <<<<<<<, =======, >>>>>>>
- README.md
Markers found: <<<<<<<, =======, >>>>>>>
- README.md
Markers found: <<<<<<<, =======, >>>>>>>
- src/example.txt
Markers found: <<<<<<<, =======, >>>>>>>

Copilot uses AI. Check for mistakes.

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.
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'
246 changes: 246 additions & 0 deletions test/infrastructure/ciModule.Tests.ps1
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
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing test coverage for the *.cs file filtering feature. The PR description and implementation include logic to filter out *.cs files from merge conflict checking (lines 1057-1069 in ci.psm1), but there are no tests validating this behavior.

Suggested tests to add:

  1. Test that *.cs files are filtered out when provided
  2. Test that the filtered count is correctly reported
  3. Test that when all files are *.cs, the function handles it gracefully (already implemented in code but not tested)
  4. Test mixed scenarios with both *.cs and non-cs files

Example test:

It "Should filter out *.cs files from checking" {
    $testCsFile = Join-Path $script:testWorkspace "file.cs"
    $testTxtFile = Join-Path $script:testWorkspace "file.txt"
    "Clean content" | Out-File $testCsFile -Encoding utf8
    "Clean content" | Out-File $testTxtFile -Encoding utf8

    Test-MergeConflictMarker -File @("file.cs", "file.txt") -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath

    $outputs = Get-Content $script:testOutputPath
    $outputs | Should -Contain "files-checked=1"  # Only txt file checked
}

Copilot uses AI. Check for mistakes.

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
}
}
}

Loading
Loading