Add merge conflict marker detection to linux-ci workflow and refactor existing actions to use reusable get-changed-files action (#26350)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com>
Co-authored-by: Travis Plunk <travis.plunk@microsoft.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Travis Plunk (HE/HIM) <tplunk@ntdev.microsoft.com>
This commit is contained in:
Copilot
2025-10-31 18:00:55 +00:00
committed by GitHub
co-authored by TravisEz13 Travis Plunk Copilot Travis Plunk
parent c12322323d
commit 0c40a84abb
10 changed files with 1001 additions and 62 deletions
@@ -0,0 +1,84 @@
# 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
- Skips binary/unreadable files
- Skips directories
## 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,36 @@
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
$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'
@@ -31,6 +31,12 @@ runs:
Invoke-CIFinish
shell: pwsh
- name: Install Pester
run: |-
Import-Module ./tools/ci.psm1
Install-CIPester
shell: pwsh
- name: Validate Package Names
run: |-
# Run Pester tests to validate package names
@@ -0,0 +1,159 @@
---
applyTo:
- "**/*.ps1"
- "**/*.psm1"
---
# PowerShell Automatic Variables - Naming Guidelines
## Purpose
This instruction provides guidelines for avoiding conflicts with PowerShell's automatic variables when writing PowerShell scripts and modules.
## What Are Automatic Variables?
PowerShell has built-in automatic variables that are created and maintained by PowerShell itself. Assigning values to these variables can cause unexpected behavior and side effects.
## Common Automatic Variables to Avoid
### Critical Variables (Never Use)
- **`$matches`** - Contains the results of regular expression matches. Overwriting this can break regex operations.
- **`$_`** - Represents the current object in the pipeline. Only use within pipeline blocks.
- **`$PSItem`** - Alias for `$_`. Same rules apply.
- **`$args`** - Contains an array of undeclared parameters. Don't use as a regular variable.
- **`$input`** - Contains an enumerator of all input passed to a function. Don't reassign.
- **`$LastExitCode`** - Exit code of the last native command. Don't overwrite unless intentional.
- **`$?`** - Success status of the last command. Don't use as a variable name.
- **`$$`** - Last token in the last line received by the session. Don't use.
- **`$^`** - First token in the last line received by the session. Don't use.
### Context Variables (Use with Caution)
- **`$Error`** - Array of error objects. Don't replace, but can modify (e.g., `$Error.Clear()`).
- **`$PSBoundParameters`** - Parameters passed to the current function. Read-only.
- **`$MyInvocation`** - Information about the current command. Read-only.
- **`$PSCmdlet`** - Cmdlet object for advanced functions. Read-only.
### Other Common Automatic Variables
- `$true`, `$false`, `$null` - Boolean and null constants
- `$HOME`, `$PSHome`, `$PWD` - Path-related variables
- `$PID` - Process ID of the current PowerShell session
- `$Host` - Host application object
- `$PSVersionTable` - PowerShell version information
For a complete list, see: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_automatic_variables
## Best Practices
### ❌ Bad - Using Automatic Variable Names
```powershell
# Bad: $matches is an automatic variable used for regex capture groups
$matches = Select-String -Path $file -Pattern $pattern
# Bad: $args is an automatic variable for undeclared parameters
$args = Get-ChildItem
# Bad: $input is an automatic variable for pipeline input
$input = Read-Host "Enter value"
```
### ✅ Good - Using Descriptive Alternative Names
```powershell
# Good: Use descriptive names that avoid conflicts
$matchedLines = Select-String -Path $file -Pattern $pattern
# Good: Use specific names for arguments
$arguments = Get-ChildItem
# Good: Use specific names for user input
$userInput = Read-Host "Enter value"
```
## Naming Alternatives
When you encounter a situation where you might use an automatic variable name, use these alternatives:
| Avoid | Use Instead |
|-------|-------------|
| `$matches` | `$matchedLines`, `$matchResults`, `$regexMatches` |
| `$args` | `$arguments`, `$parameters`, `$commandArgs` |
| `$input` | `$userInput`, `$inputValue`, `$inputData` |
| `$_` (outside pipeline) | Use a named parameter or explicit variable |
| `$Error` (reassignment) | Don't reassign; use `$Error.Clear()` if needed |
## How to Check
### PSScriptAnalyzer Rule
PSScriptAnalyzer has a built-in rule that detects assignments to automatic variables:
```powershell
# This will trigger PSAvoidAssignmentToAutomaticVariable
$matches = Get-Something
```
**Rule ID**: PSAvoidAssignmentToAutomaticVariable
### Manual Review
When writing PowerShell code, always:
1. Avoid variable names that match PowerShell keywords or automatic variables
2. Use descriptive, specific names that clearly indicate the variable's purpose
3. Run PSScriptAnalyzer on your code before committing
4. Review code for variable naming during PR reviews
## Examples from the Codebase
### Example 1: Regex Matching
```powershell
# ❌ Bad - Overwrites automatic $matches variable
$matches = [regex]::Matches($content, $pattern)
# ✅ Good - Uses descriptive name
$regexMatches = [regex]::Matches($content, $pattern)
```
### Example 2: Select-String Results
```powershell
# ❌ Bad - Conflicts with automatic $matches
$matches = Select-String -Path $file -Pattern $pattern
# ✅ Good - Clear and specific
$matchedLines = Select-String -Path $file -Pattern $pattern
```
### Example 3: Collecting Arguments
```powershell
# ❌ Bad - Conflicts with automatic $args
function Process-Items {
$args = $MyItems
# ... process items
}
# ✅ Good - Descriptive parameter name
function Process-Items {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments)]
[string[]]$Items
)
# ... process items
}
```
## References
- [PowerShell Automatic Variables Documentation](https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_automatic_variables)
- [PSScriptAnalyzer Rules](https://github.com/PowerShell/PSScriptAnalyzer/blob/master/docs/Rules/README.md)
- [PowerShell Best Practices](https://learn.microsoft.com/powershell/scripting/developer/cmdlet/strongly-encouraged-development-guidelines)
## Summary
**Key Takeaway**: Always use descriptive, specific variable names that clearly indicate their purpose and avoid conflicts with PowerShell's automatic variables. When in doubt, choose a longer, more descriptive name over a short one that might conflict.
@@ -0,0 +1,201 @@
---
applyTo:
- "tools/ci.psm1"
- "build.psm1"
- "tools/packaging/**/*.psm1"
- ".github/**/*.yml"
- ".github/**/*.yaml"
---
# Guidelines for PowerShell Code Organization
## When to Move Code from YAML to PowerShell Modules
PowerShell code in GitHub Actions YAML files should be kept minimal. Move code to a module when:
### Size Threshold
- **More than ~30 lines** of PowerShell in a YAML file step
- **Any use of .NET types** like `[regex]`, `[System.IO.Path]`, etc.
- **Complex logic** requiring multiple nested loops or conditionals
- **Reusable functionality** that might be needed elsewhere
### Indicators to Move Code
1. Using .NET type accelerators (`[regex]`, `[PSCustomObject]`, etc.)
2. Complex string manipulation or parsing
3. File system operations beyond basic reads/writes
4. Logic that would benefit from unit testing
5. Code that's difficult to read/maintain in YAML format
## Which Module to Use
### ci.psm1 (`tools/ci.psm1`)
**Purpose**: CI/CD-specific operations and workflows
**Use for**:
- Build orchestration (invoking builds, tests, packaging)
- CI environment setup and configuration
- Test execution and result processing
- Artifact handling and publishing
- CI-specific validations and checks
- Environment variable management for CI
**Examples**:
- `Invoke-CIBuild` - Orchestrates build process
- `Invoke-CITest` - Runs Pester tests
- `Test-MergeConflictMarker` - Validates files for conflicts
- `Set-BuildVariable` - Manages CI variables
**When NOT to use**:
- Core build operations (use build.psm1)
- Package creation logic (use packaging.psm1)
- Platform-specific build steps
### build.psm1 (`build.psm1`)
**Purpose**: Core build operations and utilities
**Use for**:
- Compiling source code
- Resource generation
- Build configuration management
- Core build utilities (New-PSOptions, Get-PSOutput, etc.)
- Bootstrap operations
- Cross-platform build helpers
**Examples**:
- `Start-PSBuild` - Main build function
- `Start-PSBootstrap` - Bootstrap dependencies
- `New-PSOptions` - Create build configuration
- `Start-ResGen` - Generate resources
**When NOT to use**:
- CI workflow orchestration (use ci.psm1)
- Package creation (use packaging.psm1)
- Test execution
### packaging.psm1 (`tools/packaging/packaging.psm1`)
**Purpose**: Package creation and distribution
**Use for**:
- Creating distribution packages (MSI, RPM, DEB, etc.)
- Package-specific metadata generation
- Package signing operations
- Platform-specific packaging logic
**Examples**:
- `Start-PSPackage` - Create packages
- `New-MSIPackage` - Create Windows MSI
- `New-DotnetSdkContainerFxdPackage` - Create container packages
**When NOT to use**:
- Building binaries (use build.psm1)
- Running tests (use ci.psm1)
- General utilities
## Best Practices
### Keep YAML Minimal
```yaml
# ❌ Bad - too much logic in YAML
- name: Check files
shell: pwsh
run: |
$files = Get-ChildItem -Recurse
foreach ($file in $files) {
$content = Get-Content $file -Raw
if ($content -match $pattern) {
# ... complex processing ...
}
}
# ✅ Good - call function from module
- name: Check files
shell: pwsh
run: |
Import-Module ./tools/ci.psm1
Test-SomeCondition -Path ${{ github.workspace }}
```
### Document Functions
Always include comment-based help for functions:
```powershell
function Test-MyFunction
{
<#
.SYNOPSIS
Brief description
.DESCRIPTION
Detailed description
.PARAMETER ParameterName
Parameter description
.EXAMPLE
Test-MyFunction -ParameterName Value
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $ParameterName
)
# Implementation
}
```
### Error Handling
Use proper error handling in modules:
```powershell
try {
# Operation
}
catch {
Write-Error "Detailed error message: $_"
throw
}
```
### Verbose Output
Use `Write-Verbose` for debugging information:
```powershell
Write-Verbose "Processing file: $filePath"
```
## Module Dependencies
- **ci.psm1** imports both `build.psm1` and `packaging.psm1`
- **build.psm1** is standalone (minimal dependencies)
- **packaging.psm1** imports `build.psm1`
When adding new functions, consider these import relationships to avoid circular dependencies.
## Testing Modules
Functions in modules should be testable:
```powershell
# Test locally
Import-Module ./tools/ci.psm1 -Force
Test-MyFunction -Parameter Value
# Can be unit tested with Pester
Describe "Test-MyFunction" {
It "Should return expected result" {
# Test implementation
}
}
```
## Migration Checklist
When moving code from YAML to a module:
1. ✅ Determine which module is appropriate (ci, build, or packaging)
2. ✅ Create function with proper parameter validation
3. ✅ Add comment-based help documentation
4. ✅ Use `[CmdletBinding()]` for advanced function features
5. ✅ Include error handling
6. ✅ Add verbose output for debugging
7. ✅ Test the function independently
8. ✅ Update YAML to call the new function
9. ✅ Verify the workflow still works end-to-end
## References
- PowerShell Advanced Functions: https://learn.microsoft.com/powershell/module/microsoft.powershell.core/about/about_functions_advanced
- Comment-Based Help: https://learn.microsoft.com/powershell/scripting/developer/help/writing-help-for-windows-powershell-scripts-and-functions
+58
View File
@@ -67,6 +67,20 @@ jobs:
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
merge_conflict_check:
name: Check for Merge Conflict Markers
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' && (startsWith(github.repository_owner, 'azure') || github.repository_owner == 'PowerShell')
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"
ci_build:
name: Build PowerShell
runs-on: ubuntu-latest
@@ -158,6 +172,48 @@ jobs:
runner_os: ubuntu-latest
test_results_artifact_name: testResults-xunit
infrastructure_tests:
name: Infrastructure Tests
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v5
with:
fetch-depth: 1
- name: Install Pester
shell: pwsh
run: |
Import-Module ./tools/ci.psm1
Install-CIPester
- name: Run Infrastructure Tests
shell: pwsh
run: |
$testResultsFolder = Join-Path $PWD "testResults"
New-Item -ItemType Directory -Path $testResultsFolder -Force | Out-Null
$config = New-PesterConfiguration
$config.Run.Path = './test/infrastructure/'
$config.Run.PassThru = $true
$config.TestResult.Enabled = $true
$config.TestResult.OutputFormat = 'NUnitXml'
$config.TestResult.OutputPath = "$testResultsFolder/InfrastructureTests.xml"
$config.Output.Verbosity = 'Detailed'
$result = Invoke-Pester -Configuration $config
if ($result.FailedCount -gt 0 -or $result.Result -eq 'Failed') {
throw "Infrastructure tests failed"
}
- name: Publish Test Results
uses: "./.github/actions/test/process-pester-results"
if: always()
with:
name: "InfrastructureTests"
testResultsFolder: "${{ github.workspace }}/testResults"
## Temporarily disable the CodeQL analysis on Linux as it doesn't work for .NET SDK 10-rc.2.
# analyze:
# name: CodeQL Analysis
@@ -180,6 +236,8 @@ jobs:
- linux_test_unelevated_ci
- linux_test_unelevated_others
- linux_packaging
- merge_conflict_check
- infrastructure_tests
# - analyze
if: always()
uses: PowerShell/compliance/.github/workflows/ready-to-merge.yml@v1.0.0
+8
View File
@@ -187,6 +187,14 @@ jobs:
$macOSRuntime = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'osx-arm64' } else { 'osx-x64' }
Start-PSPackage -Type osxpkg -ReleaseTag $releaseTag -MacOSRuntime $macOSRuntime -SkipReleaseChecks
shell: pwsh
- name: Install Pester
if: success()
run: |-
Import-Module ./tools/ci.psm1
Install-CIPester
shell: pwsh
- name: Test package contents
if: success()
run: |-
-61
View File
@@ -1,61 +0,0 @@
parameters:
vmImage: 'windows-latest'
jobName: 'Nanoserver_Tests'
continueOnError: false
jobs:
- job: ${{ parameters.jobName }}
variables:
scriptName: ${{ parameters.scriptName }}
pool:
vmImage: ${{ parameters.vmImage }}
displayName: ${{ parameters.jobName }}
steps:
- script: |
set
displayName: Capture Environment
condition: succeededOrFailed()
- task: DownloadBuildArtifacts@0
displayName: 'Download Build Artifacts'
inputs:
downloadType: specific
itemPattern: |
build/**/*
downloadPath: '$(System.ArtifactsDirectory)'
- pwsh: |
Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse
displayName: 'Capture Artifacts Directory'
continueOnError: true
- pwsh: |
Install-module Pester -Scope CurrentUser -Force -MaximumVersion 4.99
displayName: 'Install Pester'
continueOnError: true
- pwsh: |
Import-Module .\tools\ci.psm1
Restore-PSOptions -PSOptionsPath '$(System.ArtifactsDirectory)\build\psoptions.json'
$options = (Get-PSOptions)
$path = split-path -path $options.Output
Write-Verbose "Path: '$path'" -Verbose
$rootPath = split-Path -path $path
Expand-Archive -Path '$(System.ArtifactsDirectory)\build\build.zip' -DestinationPath $rootPath -Force
Invoke-Pester -Path ./test/nanoserver -OutputFormat NUnitXml -OutputFile ./test-nanoserver.xml
displayName: Test
condition: succeeded()
- task: PublishTestResults@2
condition: succeededOrFailed()
displayName: Publish Nanoserver Test Results **\test*.xml
inputs:
testRunner: NUnit
testResultsFiles: '**\test*.xml'
testRunTitle: nanoserver
mergeTestResults: true
failTaskOnFailedTests: true
+240
View File
@@ -0,0 +1,240 @@
# 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" {
# The function parameter has Mandatory validation which rejects empty arrays by design
# This test verifies that behavior
$emptyArray = @()
{ Test-MergeConflictMarker -File $emptyArray -WorkspacePath $script:testWorkspace -OutputPath $script:testOutputPath -SummaryPath $script:testSummaryPath } | Should -Throw -ExpectedMessage "*empty array*"
}
}
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
}
}
}
+209 -1
View File
@@ -228,6 +228,45 @@ function Invoke-CIxUnit
}
}
# Install Pester module if not already installed with a compatible version
function Install-CIPester
{
[CmdletBinding()]
param(
[string]$MinimumVersion = '5.0.0',
[string]$MaximumVersion = '5.99.99',
[switch]$Force
)
Write-Verbose "Checking for Pester module (required: $MinimumVersion - $MaximumVersion)" -Verbose
# Check if a compatible version of Pester is already installed
$installedPester = Get-Module -Name Pester -ListAvailable |
Where-Object { $_.Version -ge $MinimumVersion -and $_.Version -le $MaximumVersion } |
Sort-Object -Property Version -Descending |
Select-Object -First 1
if ($installedPester -and -not $Force) {
Write-Host "Pester version $($installedPester.Version) is already installed and meets requirements" -ForegroundColor Green
return
}
if ($Force) {
Write-Host "Installing Pester module (forced)" -ForegroundColor Yellow
} else {
Write-Host "Installing Pester module" -ForegroundColor Yellow
}
try {
Install-Module -Name Pester -Force -SkipPublisherCheck -MaximumVersion $MaximumVersion -ErrorAction Stop
Write-Host "Successfully installed Pester module" -ForegroundColor Green
}
catch {
Write-Error "Failed to install Pester module: $_"
throw
}
}
# Implement CI 'Test_script'
function Invoke-CITest
{
@@ -621,7 +660,7 @@ function Invoke-CIFinish
# Install the latest Pester and import it
$maximumPesterVersion = '4.99'
Install-Module Pester -Force -SkipPublisherCheck -MaximumVersion $maximumPesterVersion
Install-CIPester -MinimumVersion '4.0.0' -MaximumVersion $maximumPesterVersion -Force
Import-Module Pester -Force -MaximumVersion $maximumPesterVersion
$testResultPath = Join-Path -Path $env:TEMP -ChildPath "win-package-$channel-$runtime.xml"
@@ -977,3 +1016,172 @@ function Invoke-InitializeContainerStage {
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(Mandatory)]
[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
Write-Host "Checking $($File.Count) changed files for merge conflict markers" -ForegroundColor Cyan
# Convert relative paths to absolute paths for processing
$absolutePaths = $File | 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)"
}
}