MegaBites-AI/Windows-powershell
0372
1# Copyright (c) Microsoft Corporation.2# Licensed under the MIT License.3 4[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]5param()6 7Set-StrictMode -Version 3.08 9$ErrorActionPreference = 'continue'10$repoRoot = Join-Path $PSScriptRoot '..'11$script:administratorsGroupSID = "S-1-5-32-544"12$script:usersGroupSID = "S-1-5-32-545"13 14# set .NET path15$dotNetPath = "$env:USERPROFILE\Appdata\Local\Microsoft\dotnet"16if(Test-Path $dotNetPath)17{18 $env:PATH = $dotNetPath + ';' + $env:PATH19}20 21# import build into the global scope so it can be used by packaging22# argumentList $true says ignore tha we may not be able to build23Write-Verbose "Importing build.psm1" -Verbose24Import-Module (Join-Path $repoRoot 'build.psm1') -Scope Global -ArgumentList $true25$buildCommands = Get-Command -Module build26Write-Verbose "Imported build.psm1 commands: $($buildCommands.Count)" -Verbose27 28Write-Verbose "Importing packaging.psm1" -Verbose29Import-Module (Join-Path $repoRoot 'tools\packaging') -Scope Global30$packagingCommands = Get-Command -Module packaging31Write-Verbose "Imported packaging.psm1 commands: $($packagingCommands.Count)" -Verbose32 33# import the windows specific functcion only in Windows PowerShell or on Windows34if($PSVersionTable.PSEdition -eq 'Desktop' -or $IsWindows)35{36 Import-Module (Join-Path $PSScriptRoot 'WindowsCI.psm1') -Scope Global37}38 39# tests if we should run a daily build40# returns true if the build is scheduled41# or is a pushed tag42Function Test-DailyBuild43{44 $trueString = 'True'45 if(($env:PS_DAILY_BUILD -eq $trueString) -or $env:BUILD_REASON -eq 'Schedule')46 {47 return $true48 }49 return $false50}51 52# Sets a build variable53Function Set-BuildVariable54{55 param(56 [Parameter(Mandatory=$true)]57 [string]58 $Name,59 60 [Parameter(Mandatory=$true)]61 [string]62 $Value,63 64 [switch]65 $IsOutput66 )67 68 $IsOutputString = if ($IsOutput) { 'true' } else { 'false' }69 $command = "vso[task.setvariable variable=$Name;isOutput=$IsOutputString]$Value"70 71 # always log command to make local debugging easier72 Write-Verbose -Message "sending command: $command" -Verbose73 74 if ($env:TF_BUILD) {75 # In VSTS76 Write-Host "##$command"77 # The variable will not show up until the next task.78 }79 80 # Setting in the current session for the same behavior as the CI and to make it show up in the same task81 Set-Item env:/$name -Value $Value82}83 84# Emulates running all of CI but locally85function Invoke-CIFull86{87 param(88 [switch] $CleanRepo89 )90 if($CleanRepo)91 {92 Clear-PSRepo93 }94 95 Invoke-CIInstall96 Invoke-CIBuild97 Invoke-CITest -ErrorAction Continue98 Invoke-CIFinish99}100 101# Implements the CI 'build_script' step102function Invoke-CIBuild103{104 param(105 [ValidateSet('Debug', 'Release', 'CodeCoverage', 'StaticAnalysis')]106 [string]$Configuration = 'Release'107 )108 109 $releaseTag = Get-ReleaseTag110 # check to be sure our test tags are correct111 $result = Get-PesterTag112 if ( $result.Result -ne "Pass" )113 {114 $result.Warnings115 throw "Tags must be CI, Feature, Scenario, or Slow"116 }117 118 if(Test-DailyBuild)119 {120 Start-PSBuild -Configuration 'CodeCoverage' -PSModuleRestore -CI -ReleaseTag $releaseTag121 }122 123 Start-PSBuild -PSModuleRestore -Configuration $Configuration -CI -ReleaseTag $releaseTag -UseNuGetOrg124 Save-PSOptions125 126 $options = (Get-PSOptions)127 128 $path = Split-Path -Path $options.Output129 130 $psOptionsPath = (Join-Path -Path $PSScriptRoot -ChildPath '../psoptions.json')131 $buildZipPath = (Join-Path -Path $PSScriptRoot -ChildPath '../build.zip')132 133 Compress-Archive -Path $path -DestinationPath $buildZipPath134 135 Push-Artifact -Path $psOptionsPath -Name 'build'136 Push-Artifact -Path $buildZipPath -Name 'build'137}138 139# Implements the CI 'install' step140function Invoke-CIInstall141{142 param(143 [switch]144 $SkipUser145 )146 147 # Switch to public sources in CI148 Switch-PSNugetConfig -Source Public149 150 # Make sure we have all the tags151 Sync-PSTags -AddRemoteIfMissing152 153 if(Test-DailyBuild)154 {155 if ($env:BUILD_REASON -eq 'Schedule')156 {157 Write-Host "##vso[build.updatebuildnumber]Daily-$env:BUILD_SOURCEBRANCHNAME-$env:BUILD_SOURCEVERSION-$((Get-Date).ToString("yyyyMMddhhss"))"158 }159 }160 161 if ($env:TF_BUILD -and !$SkipUser.IsPresent)162 {163 # Generate new credential for CI (only) remoting tests.164 Write-Verbose "Creating account for remoting tests in CI." -Verbose165 166 # Password167 $randomObj = [System.Random]::new()168 $password = ""169 1..(Get-Random -Minimum 15 -Maximum 126) | ForEach-Object { $password = $password + [char]$randomObj.next(45,126) }170 171 # Account172 $userName = 'ciRemote'173 New-LocalUser -username $userName -Password $password174 Add-UserToGroup -username $userName -groupSid $script:administratorsGroupSID175 176 # Provide credentials globally for remote tests.177 $ss = ConvertTo-SecureString -String $password -AsPlainText -Force178 $ciRemoteCredential = [PSCredential]::new("$env:COMPUTERNAME\$userName", $ss)179 $ciRemoteCredential | Export-Clixml -Path "$env:TEMP\CIRemoteCred.xml" -Force180 181 # Check that LocalAccountTokenFilterPolicy policy is set, since it is needed for remoting182 # using above local admin account.183 Write-Log -Message "Checking for LocalAccountTokenFilterPolicy in the CI."184 $haveLocalAccountTokenFilterPolicy = $false185 try186 {187 $haveLocalAccountTokenFilterPolicy = ((Get-ItemPropertyValue -Path HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System -Name LocalAccountTokenFilterPolicy) -eq 1)188 }189 # ignore if anything is caught:190 catch {}191 if (!$haveLocalAccountTokenFilterPolicy)192 {193 Write-Verbose "Setting the LocalAccountTokenFilterPolicy for remoting tests"194 Set-ItemProperty -Path HKLM:SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System -Name LocalAccountTokenFilterPolicy -Value 1195 }196 }197 198 Set-BuildVariable -Name TestPassed -Value False199}200 201function Invoke-CIxUnit202{203 param(204 [switch]205 $SkipFailing206 )207 $env:CoreOutput = Split-Path -Parent (Get-PSOutput -Options (Get-PSOptions))208 $path = "$env:CoreOutput\pwsh.exe"209 if($IsMacOS -or $IsLinux)210 {211 $path = "$env:CoreOutput\pwsh"212 }213 214 if(!(Test-Path $path))215 {216 throw "CoreCLR pwsh.exe was not built"217 }218 219 $xUnitTestResultsFile = Join-Path -Path $PWD -ChildPath "xUnitTestResults.xml"220 221 Start-PSxUnit -xUnitTestResultsFile $xUnitTestResultsFile222 Push-Artifact -Path $xUnitTestResultsFile -name xunit223 224 if(!$SkipFailing.IsPresent)225 {226 # Fail the build, if tests failed227 Test-XUnitTestResults -TestResultsFile $xUnitTestResultsFile228 }229}230 231# Install Pester module if not already installed with a compatible version232function Install-CIPester233{234 [CmdletBinding()]235 param(236 [string]$MinimumVersion = '5.0.0',237 [string]$MaximumVersion = '5.99.99',238 [switch]$Force239 )240 241 Write-Verbose "Checking for Pester module (required: $MinimumVersion - $MaximumVersion)" -Verbose242 243 # Check if a compatible version of Pester is already installed244 $installedPester = Get-Module -Name Pester -ListAvailable |245 Where-Object { $_.Version -ge $MinimumVersion -and $_.Version -le $MaximumVersion } |246 Sort-Object -Property Version -Descending |247 Select-Object -First 1248 249 if ($installedPester -and -not $Force) {250 Write-Host "Pester version $($installedPester.Version) is already installed and meets requirements" -ForegroundColor Green251 return252 }253 254 if ($Force) {255 Write-Host "Installing Pester module (forced)" -ForegroundColor Yellow256 } else {257 Write-Host "Installing Pester module" -ForegroundColor Yellow258 }259 260 try {261 Install-Module -Name Pester -Force -SkipPublisherCheck -MaximumVersion $MaximumVersion -ErrorAction Stop262 Write-Host "Successfully installed Pester module" -ForegroundColor Green263 }264 catch {265 Write-Error "Failed to install Pester module: $_"266 throw267 }268}269 270# Implement CI 'Test_script'271function Invoke-CITest272{273 [CmdletBinding()]274 param(275 [ValidateSet('UnelevatedPesterTests', 'ElevatedPesterTests')]276 [string] $Purpose,277 [ValidateSet('CI', 'Others')]278 [string] $TagSet,279 [string] $TitlePrefix,280 [string] $OutputFormat = "NUnitXml"281 )282 283 Write-Verbose -Verbose "CI test: OutputFormat: $OutputFormat"284 285 # Set locale correctly for Linux CIs286 Set-CorrectLocale287 288 # Pester doesn't allow Invoke-Pester -TagAll@('CI', 'RequireAdminOnWindows') currently289 # https://github.com/pester/Pester/issues/608290 # To work-around it, we exlude all categories, but 'CI' from the list291 switch ($TagSet) {292 'CI' {293 Write-Host -Foreground Green 'Running "CI" CoreCLR tests..'294 $ExcludeTag = @('Slow', 'Feature', 'Scenario')295 }296 'Others' {297 Write-Host -Foreground Green 'Running non-CI CoreCLR tests..'298 $ExcludeTag = @('CI')299 }300 Default {301 throw "Unknown TagSet: '$TagSet'"302 }303 }304 305 if($IsLinux -or $IsMacOS)306 {307 return Invoke-LinuxTestsCore -Purpose $Purpose -ExcludeTag $ExcludeTag -TagSet $TagSet -TitlePrefix $TitlePrefix -OutputFormat $OutputFormat308 }309 310 # CoreCLR311 312 $env:CoreOutput = Split-Path -Parent (Get-PSOutput -Options (Get-PSOptions))313 Write-Host -Foreground Green 'Run CoreCLR tests'314 $testResultsNonAdminFile = "$PWD\TestsResultsNonAdmin-$TagSet.xml"315 $testResultsAdminFile = "$PWD\TestsResultsAdmin-$TagSet.xml"316 if(!(Test-Path "$env:CoreOutput\pwsh.exe"))317 {318 throw "CoreCLR pwsh.exe was not built"319 }320 321 # Get the experimental feature names and the tests associated with them322 $ExperimentalFeatureTests = Get-ExperimentalFeatureTests323 324 if ($Purpose -eq 'UnelevatedPesterTests') {325 $unelevate = $true326 $environment = Get-EnvironmentInformation327 if ($environment.OSArchitecture -eq 'arm64') {328 Write-Verbose -Verbose "running on arm64, running unelevated tests as elevated"329 $unelevate = $false330 }331 332 $arguments = @{333 Bindir = $env:CoreOutput334 OutputFile = $testResultsNonAdminFile335 Unelevate = $unelevate336 Terse = $true337 Tag = @()338 ExcludeTag = $ExcludeTag + 'RequireAdminOnWindows'339 OutputFormat = $OutputFormat340 }341 342 $title = "Pester Unelevated - $TagSet"343 if ($TitlePrefix) {344 $title = "$TitlePrefix - $title"345 }346 Write-Verbose -Verbose "Starting Pester with output format $($arguments.OutputFormat)"347 Start-PSPester @arguments -Title $title348 349 # Fail the build, if tests failed350 Test-PSPesterResults -TestResultsFile $testResultsNonAdminFile351 352 # Run tests with specified experimental features enabled353 foreach ($entry in $ExperimentalFeatureTests.GetEnumerator())354 {355 $featureName = $entry.Key356 $testFiles = $entry.Value357 358 $expFeatureTestResultFile = "$PWD\TestsResultsNonAdmin.$featureName.xml"359 $arguments['OutputFile'] = $expFeatureTestResultFile360 $arguments['ExperimentalFeatureName'] = $featureName361 if ($testFiles.Count -eq 0) {362 # If an empty array is specified for the feature name, we run all tests with the feature enabled.363 # This allows us to prevent regressions to a critical engine experimental feature.364 $arguments.Remove('Path')365 } else {366 # If a non-empty string or array is specified for the feature name, we only run those test files.367 $arguments['Path'] = $testFiles368 }369 370 $title = "Pester Experimental Unelevated - $featureName"371 if ($TitlePrefix) {372 $title = "$TitlePrefix - $title"373 }374 375 # We just built the test tools, we don't need to rebuild them376 Write-Verbose -Verbose "Starting Pester with output format $($arguments.OutputFormat)"377 Start-PSPester @arguments -Title $title -SkipTestToolBuild378 379 # Fail the build, if tests failed380 Test-PSPesterResults -TestResultsFile $expFeatureTestResultFile381 }382 }383 384 if ($Purpose -eq 'ElevatedPesterTests') {385 $arguments = @{386 Terse = $true387 Bindir = $env:CoreOutput388 OutputFile = $testResultsAdminFile389 Tag = @('RequireAdminOnWindows')390 ExcludeTag = $ExcludeTag391 OutputFormat = $OutputFormat392 }393 394 $title = "Pester Elevated - $TagSet"395 if ($TitlePrefix) {396 $title = "$TitlePrefix - $title"397 }398 399 Write-Verbose -Verbose "Starting Pester with output format $($arguments.OutputFormat)"400 Start-PSPester @arguments -Title $title401 402 # Fail the build, if tests failed403 Test-PSPesterResults -TestResultsFile $testResultsAdminFile404 405 # Run tests with specified experimental features enabled406 foreach ($entry in $ExperimentalFeatureTests.GetEnumerator())407 {408 $featureName = $entry.Key409 $testFiles = $entry.Value410 411 $expFeatureTestResultFile = "$PWD\TestsResultsAdmin.$featureName.xml"412 $arguments['OutputFile'] = $expFeatureTestResultFile413 $arguments['ExperimentalFeatureName'] = $featureName414 if ($testFiles.Count -eq 0)415 {416 # If an empty array is specified for the feature name, we run all tests with the feature enabled.417 # This allows us to prevent regressions to a critical engine experimental feature.418 $arguments.Remove('Path')419 }420 else421 {422 # If a non-empty string or array is specified for the feature name, we only run those test files.423 $arguments['Path'] = $testFiles424 }425 426 $title = "Pester Experimental >levated - $featureName"427 if ($TitlePrefix) {428 $title = "$TitlePrefix - $title"429 }430 431 Write-Verbose -Verbose "Starting Pester with output format $($arguments.OutputFormat)"432 # We just built the test tools, we don't need to rebuild them433 Start-PSPester @arguments -Title $title -SkipTestToolBuild434 435 # Fail the build, if tests failed436 Test-PSPesterResults -TestResultsFile $expFeatureTestResultFile437 }438 }439 440 Set-BuildVariable -Name TestPassed -Value True441}442 443function New-CodeCoverageAndTestPackage444{445 [CmdletBinding()]446 param()447 448 if (Test-DailyBuild)449 {450 Start-PSBuild -Configuration 'CodeCoverage' -Clean451 452 $codeCoverageOutput = Split-Path -Parent (Get-PSOutput)453 $codeCoverageArtifacts = Compress-CoverageArtifacts -CodeCoverageOutput $codeCoverageOutput454 455 $destBasePath = if ($env:TF_BUILD) {456 $env:BUILD_ARTIFACTSTAGINGDIRECTORY457 } else {458 Join-Path (Get-Location).Path "out"459 }460 461 if (-not (Test-Path $destBasePath))462 {463 $null = New-Item -ItemType Directory -Path $destBasePath464 }465 466 Write-Host -ForegroundColor Green 'Upload CodeCoverage artifacts'467 $codeCoverageArtifacts | ForEach-Object {468 Copy-Item -Path $_ -Destination $destBasePath469 $newPath = Join-Path $destBasePath (Split-Path $_ -Leaf)470 Push-Artifact -Path $newPath -Name 'CodeCoverage'471 }472 473 New-TestPackage -Destination $destBasePath474 $testPackageFullName = Join-Path $destBasePath 'TestPackage.zip'475 Write-Verbose "Created TestPackage.zip" -Verbose476 Write-Host -ForegroundColor Green 'Upload test package'477 Push-Artifact $testPackageFullName -Name 'CodeCoverage'478 }479}480 481# Wrapper to push artifact482function Push-Artifact483{484 param(485 [Parameter(Mandatory)]486 [ValidateScript({Test-Path -Path $_})]487 $Path,488 [string]489 $Name490 )491 492 if(!$Name)493 {494 $artifactName = [system.io.path]::GetFileName($Path)495 }496 else497 {498 $artifactName = $Name499 }500 501 if ($env:TF_BUILD) {502 # In Azure DevOps503 Write-Host "##vso[artifact.upload containerfolder=$artifactName;artifactname=$artifactName;]$Path"504 } elseif ($env:GITHUB_WORKFLOW -and $env:RUNNER_WORKSPACE) {505 # In GitHub Actions506 $destinationPath = Join-Path -Path $env:RUNNER_WORKSPACE -ChildPath $artifactName507 508 # Create the folder if it does not exist509 if (!(Test-Path -Path $destinationPath)) {510 $null = New-Item -ItemType Directory -Path $destinationPath -Force511 }512 513 Copy-Item -Path $Path -Destination $destinationPath -Force -Verbose514 } else {515 Write-Warning "Push-Artifact is not supported in this environment."516 }517}518 519function Compress-CoverageArtifacts520{521 param([string] $CodeCoverageOutput)522 523 # Create archive for test content, OpenCover module and CodeCoverage build524 $artifacts = New-Object System.Collections.ArrayList525 526 Add-Type -AssemblyName System.IO.Compression.FileSystem527 $resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath((Join-Path $PSScriptRoot '..\test\tools\OpenCover'))528 $zipOpenCoverPath = Join-Path $PWD 'OpenCover.zip'529 [System.IO.Compression.ZipFile]::CreateFromDirectory($resolvedPath, $zipOpenCoverPath)530 $null = $artifacts.Add($zipOpenCoverPath)531 532 $zipCodeCoveragePath = Join-Path $PWD "CodeCoverage.zip"533 Write-Verbose "Zipping ${CodeCoverageOutput} into $zipCodeCoveragePath" -Verbose534 [System.IO.Compression.ZipFile]::CreateFromDirectory($CodeCoverageOutput, $zipCodeCoveragePath)535 $null = $artifacts.Add($zipCodeCoveragePath)536 537 return $artifacts538}539 540function Get-ReleaseTag541{542 $metaDataPath = Join-Path -Path $PSScriptRoot -ChildPath 'metadata.json'543 $metaData = Get-Content $metaDataPath | ConvertFrom-Json544 $releaseTag = $metadata.NextReleaseTag545 if($env:BUILD_BUILID)546 {547 $releaseTag = $releaseTag.split('.')[0..2] -join '.'548 $releaseTag = $releaseTag + '.' + $env:BUILD_BUILID549 }550 return $releaseTag551}552 553# Implements CI 'on_finish' step554function Invoke-CIFinish555{556 param(557 [string] $Runtime = 'win7-x64',558 [string] $Channel = 'preview',559 [Validateset('Build','Package')]560 [string[]] $Stage = ('Build','Package')561 )562 563 # Switch to public sources in CI564 Switch-PSNugetConfig -Source Public565 566 if ($PSEdition -eq 'Core' -and ($IsLinux -or $IsMacOS) -and $Stage -contains 'Build') {567 return New-LinuxPackage568 }569 570 $artifacts = New-Object System.Collections.ArrayList571 try {572 $buildFolder = "${env:SYSTEM_ARTIFACTSDIRECTORY}/mainBuild"573 574 if ($Stage -contains "Build") {575 if ($Channel -eq 'preview') {576 $releaseTag = Get-ReleaseTag577 578 $previewVersion = $releaseTag.Split('-')579 $previewPrefix = $previewVersion[0]580 $previewLabel = $previewVersion[1].replace('.','')581 582 if (Test-DailyBuild) {583 $previewLabel = "daily{0}" -f $previewLabel584 }585 586 $prereleaseIteration = (get-date).Day587 $preReleaseVersion = "$previewPrefix-$previewLabel.$prereleaseIteration"588 # Build clean before backing to remove files from testing589 Start-PSBuild -PSModuleRestore -Configuration 'Release' -ReleaseTag $preReleaseVersion -Clean -Runtime $Runtime -output $buildFolder -PSOptionsPath "${buildFolder}/psoptions.json"590 $options = Get-PSOptions591 # Remove symbol files.592 $filter = Join-Path -Path (Split-Path $options.Output) -ChildPath '*.pdb'593 Write-Verbose "Removing symbol files from $filter" -Verbose594 Remove-Item $filter -Force -Recurse595 } else {596 $releaseTag = Get-ReleaseTag597 $releaseTagParts = $releaseTag.split('.')598 $preReleaseVersion = $releaseTagParts[0]+ ".9.9"599 Write-Verbose "newPSReleaseTag: $preReleaseVersion" -Verbose600 Start-PSBuild -PSModuleRestore -Configuration 'Release' -ReleaseTag $preReleaseVersion -Clean -Runtime $Runtime -output $buildFolder -PSOptionsPath "${buildFolder}/psoptions.json"601 $options = Get-PSOptions602 # Remove symbol files.603 $filter = Join-Path -Path (Split-Path $options.Output) -ChildPath '*.pdb'604 Write-Verbose "Removing symbol files from $filter" -Verbose605 Remove-Item $filter -Force -Recurse606 }607 608 # Set a variable, both in the current process and in AzDevOps for the packaging stage to get the release tag609 $env:CI_FINISH_RELASETAG=$preReleaseVersion610 $vstsCommandString = "vso[task.setvariable variable=CI_FINISH_RELASETAG]$preReleaseVersion"611 Write-Verbose -Message "$vstsCommandString" -Verbose612 Write-Host -Object "##$vstsCommandString"613 }614 615 if ($Stage -contains "Package") {616 Restore-PSOptions -PSOptionsPath "${buildFolder}/psoptions.json"617 $preReleaseVersion = $env:CI_FINISH_RELASETAG618 619 # Build packages620 $packageTypes = 'zip', 'zip-pdb', 'msix'621 622 $packages = Start-PSPackage -Type $packageTypes -ReleaseTag $preReleaseVersion -SkipReleaseChecks -WindowsRuntime $Runtime623 624 foreach ($package in $packages) {625 if (Test-Path $package -ErrorAction Ignore) {626 Write-Log "Package found: $package"627 } else {628 Write-Warning -Message "Package NOT found: $package"629 }630 631 if ($package -is [string]) {632 $null = $artifacts.Add($package)633 }634 }635 636 }637 } catch {638 Get-Error -InputObject $_639 throw640 } finally {641 $pushedAllArtifacts = $true642 643 $artifacts | ForEach-Object {644 Write-Log -Message "Pushing $_ as CI artifact"645 if (Test-Path $_) {646 Push-Artifact -Path $_ -Name 'artifacts'647 } else {648 $pushedAllArtifacts = $false649 Write-Warning "Artifact $_ does not exist."650 }651 }652 653 if (!$pushedAllArtifacts) {654 throw "Some artifacts did not exist!"655 }656 }657}658 659function Set-Path660{661 param662 (663 [Parameter(Mandatory)]664 [string]665 $Path,666 667 [Parameter(Mandatory)]668 [switch]669 $Append670 )671 672 $machinePathString = [System.Environment]::GetEnvironmentVariable('path',[System.EnvironmentVariableTarget]::Machine)673 $machinePath = $machinePathString -split ';'674 675 if($machinePath -inotcontains $path)676 {677 $newPath = "$machinePathString;$path"678 Write-Verbose "Adding $path to path..." -Verbose679 [System.Environment]::SetEnvironmentVariable('path',$newPath,[System.EnvironmentVariableTarget]::Machine)680 Write-Verbose "Added $path to path." -Verbose681 }682 else683 {684 Write-Verbose "$path already in path." -Verbose685 }686}687 688# Display environment variables in a log group for GitHub Actions689function Show-Environment690{691 Write-LogGroupStart -Title 'Environment'692 Get-ChildItem -Path env: | Out-String -width 9999 -Stream | Write-Verbose -Verbose693 Write-LogGroupEnd -Title 'Environment'694}695 696# Bootstrap script for Linux and macOS697function Invoke-BootstrapStage698{699 $createPackages = Test-DailyBuild700 Write-Log -Message "Executing ci.psm1 Bootstrap Stage"701 # Make sure we have all the tags702 Sync-PSTags -AddRemoteIfMissing703 Start-PSBootstrap -Scenario Package:$createPackages704}705 706# Run pester tests for Linux and macOS707function Invoke-LinuxTestsCore708{709 [CmdletBinding()]710 param(711 [ValidateSet('UnelevatedPesterTests', 'ElevatedPesterTests', 'All')]712 [string] $Purpose = 'All',713 [string[]] $ExcludeTag = @('Slow', 'Feature', 'Scenario'),714 [string] $TagSet = 'CI',715 [string] $TitlePrefix,716 [string] $OutputFormat = "NUnitXml"717 )718 719 $output = Split-Path -Parent (Get-PSOutput -Options (Get-PSOptions))720 $testResultsNoSudo = "$PWD/TestResultsNoSudo.xml"721 $testResultsSudo = "$PWD/TestResultsSudo.xml"722 $testExcludeTag = $ExcludeTag + 'RequireSudoOnUnix'723 $pesterPassThruNoSudoObject = $null724 $pesterPassThruSudoObject = $null725 $noSudoResultsWithExpFeatures = $null726 $sudoResultsWithExpFeatures = $null727 728 $noSudoPesterParam = @{729 'BinDir' = $output730 'PassThru' = $true731 'Terse' = $true732 'Tag' = @()733 'ExcludeTag' = $testExcludeTag734 'OutputFile' = $testResultsNoSudo735 'OutputFormat' = $OutputFormat736 }737 738 # Get the experimental feature names and the tests associated with them739 $ExperimentalFeatureTests = Get-ExperimentalFeatureTests740 741 # Running tests which do not require sudo.742 if($Purpose -eq 'UnelevatedPesterTests' -or $Purpose -eq 'All')743 {744 $title = "Pester No Sudo - $TagSet"745 if ($TitlePrefix) {746 $title = "$TitlePrefix - $title"747 }748 $pesterPassThruNoSudoObject = Start-PSPester @noSudoPesterParam -Title $title749 750 # Running tests that do not require sudo, with specified experimental features enabled751 $noSudoResultsWithExpFeatures = @()752 foreach ($entry in $ExperimentalFeatureTests.GetEnumerator()) {753 $featureName = $entry.Key754 $testFiles = $entry.Value755 756 $expFeatureTestResultFile = "$PWD\TestResultsNoSudo.$featureName.xml"757 $noSudoPesterParam['OutputFile'] = $expFeatureTestResultFile758 $noSudoPesterParam['ExperimentalFeatureName'] = $featureName759 if ($testFiles.Count -eq 0) {760 # If an empty array is specified for the feature name, we run all tests with the feature enabled.761 # This allows us to prevent regressions to a critical engine experimental feature.762 $noSudoPesterParam.Remove('Path')763 }764 else765 {766 # If a non-empty string or array is specified for the feature name, we only run those test files.767 $noSudoPesterParam['Path'] = $testFiles768 }769 $title = "Pester Experimental No Sudo - $featureName - $TagSet"770 if ($TitlePrefix) {771 $title = "$TitlePrefix - $title"772 }773 $passThruResult = Start-PSPester @noSudoPesterParam -Title $title -SkipTestToolBuild774 775 $noSudoResultsWithExpFeatures += $passThruResult776 }777 }778 779 # Running tests, which require sudo.780 if($Purpose -eq 'ElevatedPesterTests' -or $Purpose -eq 'All')781 {782 $sudoPesterParam = $noSudoPesterParam.Clone()783 $sudoPesterParam.Remove('Path')784 $sudoPesterParam['Tag'] = @('RequireSudoOnUnix')785 $sudoPesterParam['ExcludeTag'] = $ExcludeTag786 $sudoPesterParam['Sudo'] = $true787 $sudoPesterParam['OutputFile'] = $testResultsSudo788 $sudoPesterParam['OutputFormat'] = $OutputFormat789 790 $title = "Pester Sudo - $TagSet"791 if ($TitlePrefix) {792 $title = "$TitlePrefix - $title"793 }794 $pesterPassThruSudoObject = Start-PSPester @sudoPesterParam -Title $title795 796 # Running tests that require sudo, with specified experimental features enabled797 $sudoResultsWithExpFeatures = @()798 foreach ($entry in $ExperimentalFeatureTests.GetEnumerator()) {799 $featureName = $entry.Key800 $testFiles = $entry.Value801 802 $expFeatureTestResultFile = "$PWD\TestResultsSudo.$featureName.xml"803 $sudoPesterParam['OutputFile'] = $expFeatureTestResultFile804 $sudoPesterParam['ExperimentalFeatureName'] = $featureName805 if ($testFiles.Count -eq 0)806 {807 # If an empty array is specified for the feature name, we run all tests with the feature enabled.808 # This allows us to prevent regressions to a critical engine experimental feature.809 $sudoPesterParam.Remove('Path')810 }811 else812 {813 # If a non-empty string or array is specified for the feature name, we only run those test files.814 $sudoPesterParam['Path'] = $testFiles815 }816 817 $title = "Pester Experimental Sudo - $featureName - $TagSet"818 if ($TitlePrefix) {819 $title = "$TitlePrefix - $title"820 }821 822 # We just built the test tools for the main test run, we don't need to rebuild them823 $passThruResult = Start-PSPester @sudoPesterParam -Title $title -SkipTestToolBuild824 825 $sudoResultsWithExpFeatures += $passThruResult826 }827 }828 829 # Determine whether the build passed830 try {831 $allTestResultsWithNoExpFeature = @($pesterPassThruNoSudoObject, $pesterPassThruSudoObject)832 $allTestResultsWithExpFeatures = @($noSudoResultsWithExpFeatures, $sudoResultsWithExpFeatures)833 # This throws if there was an error:834 $allTestResultsWithNoExpFeature | Where-Object {$null -ne $_} | ForEach-Object { Test-PSPesterResults -ResultObject $_ }835 $allTestResultsWithExpFeatures | Where-Object {$null -ne $_} | ForEach-Object { Test-PSPesterResults -ResultObject $_ -CanHaveNoResult }836 $result = "PASS"837 } catch {838 # The build failed, set the result:839 $resultError = $_840 $result = "FAIL"841 }842 843 # If the tests did not pass, throw the reason why844 if ( $result -eq "FAIL" )845 {846 Write-Warning "Tests failed. See the issue below."847 Throw $resultError848 }849 else850 {851 Write-Verbose "Tests did not fail! Nice job!"852 }853}854 855function New-LinuxPackage856{857 858 $isFullBuild = Test-DailyBuild859 $releaseTag = Get-ReleaseTag860 $packageParams = @{}861 $packageParams += @{ReleaseTag=$releaseTag}862 863 # Only build packages for PowerShell/PowerShell repository864 # branches, not pull requests865 $packages = @(Start-PSPackage @packageParams -SkipReleaseChecks -Type deb, rpm, rpm-fxdependent-arm64, tar)866 foreach($package in $packages)867 {868 if (Test-Path $package)869 {870 Write-Log "Package found: $package"871 }872 else873 {874 Write-Error -Message "Package NOT found: $package"875 }876 877 if ($package -isnot [System.IO.FileInfo])878 {879 $packageObj = Get-Item $package880 Write-Error -Message "The PACKAGE is not a FileInfo object"881 }882 else883 {884 $packageObj = $package885 }886 887 # Determine artifacts directory (GitHub Actions or Azure DevOps)888 $artifactsDir = if ($env:GITHUB_ACTIONS -eq 'true') {889 "${env:GITHUB_WORKSPACE}/../packages"890 } else {891 "${env:BUILD_ARTIFACTSTAGINGDIRECTORY}"892 }893 894 # Ensure artifacts directory exists895 if (-not (Test-Path $artifactsDir)) {896 New-Item -ItemType Directory -Path $artifactsDir -Force | Out-Null897 }898 899 Write-Log -message "Artifacts directory: $artifactsDir"900 Copy-Item $packageObj.FullName -Destination $artifactsDir -Force901 }902 903 if ($IsLinux)904 {905 # Determine artifacts directory (GitHub Actions or Azure DevOps)906 $artifactsDir = if ($env:GITHUB_ACTIONS -eq 'true') {907 "${env:GITHUB_WORKSPACE}/../packages"908 } else {909 "${env:BUILD_ARTIFACTSTAGINGDIRECTORY}"910 }911 912 # Create and package Raspbian .tgz913 # Build must be clean for Raspbian914 Start-PSBuild -PSModuleRestore -Clean -Runtime linux-arm -Configuration 'Release'915 $armPackage = Start-PSPackage @packageParams -Type tar-arm -SkipReleaseChecks916 Copy-Item $armPackage -Destination $artifactsDir -Force917 }918}919 920function Invoke-InitializeContainerStage {921 param(922 [string]923 $ContainerPattern = '.'924 )925 926 Write-Verbose "Invoking InitializeContainerStage with ContainerPattern: ${ContainerPattern}" -Verbose927 928 $fallbackSeed = (get-date).DayOfYear929 Write-Verbose "Fall back seed: $fallbackSeed" -Verbose930 931 # For PRs set the seed to the PR number so that the image is always the same932 $seed = $env:SYSTEM_PULLREQUEST_PULLREQUESTID933 934 if(!$seed) {935 # for non-PRs use the integer identifier of the build as the seed.936 $seed = $fallbackSeed937 }938 939 # cut down to 32 bits and keep the most varying parts, which are lower bits940 if ($seed -ge [Int32]::MaxValue) {941 $seed = [int]($seed -band [int]::MaxValue)942 }943 944 Write-Verbose "Seed: $seed" -Verbose945 946 # Get the latest image matrix JSON for preview947 $matrix = ./PowerShell-Docker/build.ps1 -GenerateMatrixJson -FullJson -Channel preview | ConvertFrom-Json948 949 # Chose images that are validated or validating, Linux and can be used in CI.950 $linuxImages = $matrix.preview |951 Where-Object {$_.IsLinux -and $_.UseInCi -and $_.DistributionState -match 'Validat.*' -and $_.JobName -match $ContainerPattern -and $_.JobName -notlike "*arm*"} |952 Select-Object JobName, Taglist |953 Sort-Object -property JobName954 955 # Use the selected seed to pick a container956 $index = Get-Random -Minimum 0 -Maximum $linuxImages.Count -SetSeed $seed957 $selectedImage = $linuxImages[$index]958 959 # Filter to the first test-deps compatible tag960 $tag = $selectedImage.Taglist -split ';' | Where-Object {$_ -match 'preview-\D+'} | Select-Object -First 1961 962 # Calculate the container name963 $containerName = "mcr.microsoft.com/powershell/test-deps:$tag"964 965 Set-BuildVariable -Name containerName -Value $containerName -IsOutput966 Set-BuildVariable -Name containerBuildName -Value $selectedImage.JobName -IsOutput967 968 if($env:BUILD_REASON -eq 'PullRequest') {969 Write-Host "##vso[build.updatebuildnumber]PR-${env:SYSTEM_PULLREQUEST_PULLREQUESTNUMBER}-$($selectedImage.JobName)-$((get-date).ToString("yyyyMMddhhmmss"))"970 } else {971 Write-Host "##vso[build.updatebuildnumber]${env:BUILD_SOURCEBRANCHNAME}-$($selectedImage.JobName)-${env:BUILD_SOURCEVERSION}-$((get-date).ToString("yyyyMMddhhmmss"))"972 973 # Cannot do this for a PR974 Write-Host "##vso[build.addbuildtag]$($selectedImage.JobName)"975 }976}977 978Function Test-MergeConflictMarker979{980 <#981 .SYNOPSIS982 Checks files for Git merge conflict markers and outputs results for GitHub Actions.983 .DESCRIPTION984 Scans the specified files for Git merge conflict markers (<<<<<<<, =======, >>>>>>>)985 and generates console output, GitHub Actions outputs, and job summary.986 Designed for use in GitHub Actions workflows.987 .PARAMETER File988 Array of file paths (relative or absolute) to check for merge conflict markers.989 .PARAMETER WorkspacePath990 Base workspace path for resolving relative paths. Defaults to current directory.991 .PARAMETER OutputPath992 Path to write GitHub Actions outputs. Defaults to $env:GITHUB_OUTPUT.993 .PARAMETER SummaryPath994 Path to write GitHub Actions job summary. Defaults to $env:GITHUB_STEP_SUMMARY.995 .EXAMPLE996 Test-MergeConflictMarker -File @('file1.txt', 'file2.cs') -WorkspacePath $env:GITHUB_WORKSPACE997 #>998 [CmdletBinding()]999 param(1000 [Parameter()]1001 [AllowEmptyCollection()]1002 [string[]] $File = @(),1003 1004 [Parameter()]1005 [string] $WorkspacePath = $PWD,1006 1007 [Parameter()]1008 [string] $OutputPath = $env:GITHUB_OUTPUT,1009 1010 [Parameter()]1011 [string] $SummaryPath = $env:GITHUB_STEP_SUMMARY1012 )1013 1014 Write-Host "Starting merge conflict marker check..." -ForegroundColor Cyan1015 1016 # Helper function to write outputs when no files to check1017 function Write-NoFilesOutput {1018 param(1019 [string]$Message,1020 [string]$OutputPath,1021 [string]$SummaryPath1022 )1023 1024 # Output results to GitHub Actions1025 if ($OutputPath) {1026 "files-checked=0" | Out-File -FilePath $OutputPath -Append -Encoding utf81027 "conflicts-found=0" | Out-File -FilePath $OutputPath -Append -Encoding utf81028 }1029 1030 # Create GitHub Actions job summary1031 if ($SummaryPath) {1032 $summaryContent = @"1033# Merge Conflict Marker Check Results1034 1035## Summary1036- **Files Checked:** 01037- **Files with Conflicts:** 01038 1039## ℹ️ No Files to Check1040 1041$Message1042 1043"@1044 $summaryContent | Out-File -FilePath $SummaryPath -Encoding utf81045 }1046 }1047 1048 # Handle empty file list (e.g., when PR only deletes files)1049 if ($File.Count -eq 0) {1050 Write-Host "No files to check (empty file list)" -ForegroundColor Yellow1051 Write-NoFilesOutput -Message "No files were provided for checking (this can happen when a PR only deletes files)." -OutputPath $OutputPath -SummaryPath $SummaryPath1052 return1053 }1054 1055 # Filter out *.cs files from merge conflict checking1056 $filesToCheck = @($File | Where-Object { $_ -notlike "*.cs" })1057 $filteredCount = $File.Count - $filesToCheck.Count1058 1059 if ($filteredCount -gt 0) {1060 Write-Host "Filtered out $filteredCount *.cs file(s) from merge conflict checking" -ForegroundColor Yellow1061 }1062 1063 if ($filesToCheck.Count -eq 0) {1064 Write-Host "No files to check after filtering (all files were *.cs)" -ForegroundColor Yellow1065 Write-NoFilesOutput -Message "All $filteredCount file(s) were filtered out (*.cs files are excluded from merge conflict checking)." -OutputPath $OutputPath -SummaryPath $SummaryPath1066 return1067 }1068 1069 Write-Host "Checking $($filesToCheck.Count) changed files for merge conflict markers" -ForegroundColor Cyan1070 1071 # Convert relative paths to absolute paths for processing1072 $absolutePaths = $filesToCheck | ForEach-Object {1073 if ([System.IO.Path]::IsPathRooted($_)) {1074 $_1075 } else {1076 Join-Path $WorkspacePath $_1077 }1078 }1079 1080 $filesWithConflicts = @()1081 $filesChecked = 01082 1083 foreach ($filePath in $absolutePaths) {1084 # Check if file exists (might be deleted)1085 if (-not (Test-Path $filePath)) {1086 Write-Verbose " Skipping deleted file: $filePath"1087 continue1088 }1089 1090 # Skip binary files and directories1091 if ((Get-Item $filePath) -is [System.IO.DirectoryInfo]) {1092 continue1093 }1094 1095 $filesChecked++1096 1097 # Get relative path for display1098 $relativePath = if ($WorkspacePath -and $filePath.StartsWith($WorkspacePath)) {1099 $filePath.Substring($WorkspacePath.Length).TrimStart([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar)1100 } else {1101 $filePath1102 }1103 1104 Write-Host " Checking: $relativePath" -ForegroundColor Gray1105 1106 # Search for conflict markers using Select-String1107 try {1108 # Git conflict markers are 7 characters followed by a space or end of line1109 # Regex pattern breakdown:1110 # ^ - Matches the start of a line1111 # (<{7}|={7}|>{7}) - Matches exactly 7 consecutive '<', '=', or '>' characters (Git conflict markers)1112 # (\s|$) - Ensures the marker is followed by whitespace or end of line1113 $pattern = '^(<{7}|={7}|>{7})(\s|$)'1114 $matchedLines = Select-String -Path $filePath -Pattern $pattern -AllMatches -ErrorAction Stop1115 1116 if ($matchedLines) {1117 # Collect marker details with line numbers (Select-String provides LineNumber automatically)1118 $markerDetails = @()1119 1120 foreach ($match in $matchedLines) {1121 $markerDetails += [PSCustomObject]@{1122 Marker = $match.Matches[0].Groups[1].Value1123 Line = $match.LineNumber1124 }1125 }1126 1127 $filesWithConflicts += [PSCustomObject]@{1128 File = $relativePath1129 MarkerDetails = $markerDetails1130 }1131 1132 Write-Host " ❌ CONFLICT MARKERS FOUND in $relativePath" -ForegroundColor Red1133 foreach ($detail in $markerDetails) {1134 Write-Host " Line $($detail.Line): $($detail.Marker)" -ForegroundColor Red1135 }1136 }1137 }1138 catch {1139 # Skip files that can't be read (likely binary)1140 Write-Verbose " Skipping unreadable file: $relativePath"1141 }1142 }1143 1144 # Output results to GitHub Actions1145 if ($OutputPath) {1146 "files-checked=$filesChecked" | Out-File -FilePath $OutputPath -Append -Encoding utf81147 "conflicts-found=$($filesWithConflicts.Count)" | Out-File -FilePath $OutputPath -Append -Encoding utf81148 }1149 1150 Write-Host "`nSummary:" -ForegroundColor Cyan1151 Write-Host " Files checked: $filesChecked" -ForegroundColor Cyan1152 Write-Host " Files with conflicts: $($filesWithConflicts.Count)" -ForegroundColor Cyan1153 1154 # Create GitHub Actions job summary1155 if ($SummaryPath) {1156 $summaryContent = @"1157# Merge Conflict Marker Check Results1158 1159## Summary1160- **Files Checked:** $filesChecked1161- **Files with Conflicts:** $($filesWithConflicts.Count)1162 1163"@1164 1165 if ($filesWithConflicts.Count -gt 0) {1166 Write-Host "`n❌ Merge conflict markers detected in the following files:" -ForegroundColor Red1167 1168 $summaryContent += "`n## ❌ Conflicts Detected`n`n"1169 $summaryContent += "The following files contain merge conflict markers:`n`n"1170 1171 foreach ($fileInfo in $filesWithConflicts) {1172 Write-Host " - $($fileInfo.File)" -ForegroundColor Red1173 1174 $summaryContent += "### 📄 ``$($fileInfo.File)```n`n"1175 $summaryContent += "| Line | Marker |`n"1176 $summaryContent += "|------|--------|`n"1177 1178 foreach ($detail in $fileInfo.MarkerDetails) {1179 Write-Host " Line $($detail.Line): $($detail.Marker)" -ForegroundColor Red1180 $summaryContent += "| $($detail.Line) | ``$($detail.Marker)`` |`n"1181 }1182 $summaryContent += "`n"1183 }1184 1185 $summaryContent += "`n**Action Required:** Please resolve these conflicts before merging.`n"1186 Write-Host "`nPlease resolve these conflicts before merging." -ForegroundColor Red1187 } else {1188 Write-Host "`n✅ No merge conflict markers found" -ForegroundColor Green1189 $summaryContent += "`n## ✅ No Conflicts Found`n`nAll checked files are free of merge conflict markers.`n"1190 }1191 1192 $summaryContent | Out-File -FilePath $SummaryPath -Encoding utf81193 }1194 1195 # Exit with error if conflicts found1196 if ($filesWithConflicts.Count -gt 0) {1197 throw "Merge conflict markers detected in $($filesWithConflicts.Count) file(s)"1198 }1199}1200 