From 5715d33398d97bed2ccf9e6526faea3db1626b88 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 13 Nov 2025 09:56:09 -0800 Subject: [PATCH 001/112] DSC v3 resource for Powershell Profile (#26157) --- .github/actions/test/nix/action.yml | 42 ++++ .github/actions/test/windows/action.yml | 20 ++ dsc/pwsh.profile.dsc.resource.json | 126 +++++++++++ dsc/pwsh.profile.resource.ps1 | 179 +++++++++++++++ experimental-feature-linux.json | 1 + experimental-feature-windows.json | 1 + .../ExperimentalFeature.cs | 5 + src/powershell-unix/powershell-unix.csproj | 4 + .../powershell-win-core.csproj | 4 + .../dsc/dsc.profileresource.Tests.ps1 | 204 ++++++++++++++++++ .../dsc/psprofile_alluser_allhost.dsc.yaml | 9 + .../psprofile_allusers_currenthost.dsc.yaml | 9 + .../psprofile_currentuser_allhosts.dsc.yaml | 9 + ...psprofile_currentuser_currenthost.dsc.yaml | 9 + ..._currentuser_currenthost_emptycontent.yaml | 9 + test/powershell/dsc/psprofile_export.dsc.yaml | 5 + tools/packaging/boms/linux.json | 10 + tools/packaging/boms/mac.json | 10 + tools/packaging/boms/windows.json | 10 + 19 files changed, 666 insertions(+) create mode 100644 dsc/pwsh.profile.dsc.resource.json create mode 100644 dsc/pwsh.profile.resource.ps1 create mode 100644 test/powershell/dsc/dsc.profileresource.Tests.ps1 create mode 100644 test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml create mode 100644 test/powershell/dsc/psprofile_export.dsc.yaml diff --git a/.github/actions/test/nix/action.yml b/.github/actions/test/nix/action.yml index ef943bfce7..b8c1778974 100644 --- a/.github/actions/test/nix/action.yml +++ b/.github/actions/test/nix/action.yml @@ -43,6 +43,48 @@ runs: with: global-json-file: ./global.json + - name: Set Package Name by Platform + id: set_package_name + shell: pwsh + run: |- + Import-Module ./.github/workflows/GHWorkflowHelper/GHWorkflowHelper.psm1 + $platform = $env:RUNNER_OS + Write-Host "Runner platform: $platform" + if ($platform -eq 'Linux') { + $packageName = 'DSC-*-x86_64-linux.tar.gz' + } elseif ($platform -eq 'macOS') { + $packageName = 'DSC-*-x86_64-apple-darwin.tar.gz' + } else { + throw "Unsupported platform: $platform" + } + + Set-GWVariable -Name "DSC_PACKAGE_NAME" -Value $packageName + + - name: Get Latest DSC Package Version + shell: pwsh + run: |- + Import-Module ./.github/workflows/GHWorkflowHelper/GHWorkflowHelper.psm1 + $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" + $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 + $latestVersion = $latestRelease.tag_name.TrimStart("v") + Write-Host "Latest DSC Version: $latestVersion" + + $packageName = "$env:DSC_PACKAGE_NAME" + + Write-Host "Package Name: $packageName" + + $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "*$packageName*" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url + Write-Host "Download URL: $downloadUrl" + + $tempPath = Get-GWTempPath + + Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath/DSC.tar.gz" -Verbose + New-Item -ItemType Directory -Path "$tempPath/DSC" -Force -Verbose + tar xvf "$tempPath/DSC.tar.gz" -C "$tempPath/DSC" + $dscRoot = "$tempPath/DSC" + Write-Host "DSC Root: $dscRoot" + Set-GWVariable -Name "DSC_ROOT" -Value $dscRoot + - name: Bootstrap shell: pwsh run: |- diff --git a/.github/actions/test/windows/action.yml b/.github/actions/test/windows/action.yml index 3b3ce0cafe..923e9606e5 100644 --- a/.github/actions/test/windows/action.yml +++ b/.github/actions/test/windows/action.yml @@ -43,6 +43,26 @@ runs: with: global-json-file: .\global.json + - name: Get Latest DSC Package Version + shell: pwsh + run: |- + Import-Module .\.github\workflows\GHWorkflowHelper\GHWorkflowHelper.psm1 + $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" + $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 + $latestVersion = $latestRelease.tag_name.TrimStart("v") + Write-Host "Latest DSC Version: $latestVersion" + + $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "DSC-*-x86_64-pc-windows-msvc.zip" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url + Write-Host "Download URL: $downloadUrl" + $tempPath = Get-GWTempPath + Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath\DSC.zip" + + $null = New-Item -ItemType Directory -Path "$tempPath\DSC" -Force + Expand-Archive -Path "$tempPath\DSC.zip" -DestinationPath "$tempPath\DSC" -Force + $dscRoot = "$tempPath\DSC" + Write-Host "DSC Root: $dscRoot" + Set-GWVariable -Name "DSC_ROOT" -Value $dscRoot + - name: Bootstrap shell: powershell run: |- diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json new file mode 100644 index 0000000000..cd18e94eec --- /dev/null +++ b/dsc/pwsh.profile.dsc.resource.json @@ -0,0 +1,126 @@ +{ + "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", + "description": "Manage PowerShell profiles.", + "tags": [ + "Linux", + "Windows", + "macOS", + "PowerShell" + ], + "type": "Microsoft.PowerShell/Profile", + "version": "0.1.0", + "get": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "./pwsh.profile.resource.ps1", + "-operation", + "get" + ], + "input": "stdin" + }, + "set": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "./pwsh.profile.resource.ps1", + "-operation", + "set" + ], + "input": "stdin" + }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + "./pwsh.profile.resource.ps1", + "-operation", + "export" + ], + "input": "stdin" + }, + "exitCodes": { + "0": "Success", + "1": "Error", + "2": "Input not supported for export operation" + }, + "schema": { + "embedded": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Profile", + "description": "Manage PowerShell profiles.", + "type": "object", + "unevaluatedProperties": false, + "required": [ + "profileType" + ], + "properties": { + "profileType": { + "type": "string", + "title": "Profile Type", + "description": "Defines which profile to manage. Valid values are: 'AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserAllHosts', and 'CurrentUserCurrentHost'.", + "enum": [ + "AllUsersCurrentHost", + "AllUsersAllHosts", + "CurrentUserAllHosts", + "CurrentUserCurrentHost" + ] + }, + "profilePath": { + "title": "Profile Path", + "description": "The full path to the profile file.", + "type": "string", + "readOnly": true + }, + "content": { + "title": "Content", + "description": "Defines the content of the profile. If you don't specify this property, the resource doesn't manage the file contents. If you specify this property as an empty string, the resource removes all content from the file. If you specify this property as a non-empty string, the resource sets the file contents to the specified string. The resources retains newlines from this property without any modification.", + "type": "string" + }, + "_exist": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json" + }, + "_name": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/name.json" + } + }, + "$defs": { + "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json", + "title": "Instance should exist", + "description": "Indicates whether the DSC resource instance should exist.", + "type": "boolean", + "default": true, + "enum": [ + false, + true + ] + }, + "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/name.json": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/name.json", + "title": "Exported instance name", + "description": "Returns a generated name for the resource instance from an export operation.", + "readOnly": true, + "type": "string" + } + } + } + } +} diff --git a/dsc/pwsh.profile.resource.ps1 b/dsc/pwsh.profile.resource.ps1 new file mode 100644 index 0000000000..ad9cfa4a63 --- /dev/null +++ b/dsc/pwsh.profile.resource.ps1 @@ -0,0 +1,179 @@ +## Copyright (c) Microsoft Corporation. All rights reserved. +## Licensed under the MIT License. + +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [ValidateSet('get', 'set', 'export')] + [string]$Operation, + [Parameter(ValueFromPipeline)] + [string[]]$UserInput +) + +Begin { + enum ProfileType { + AllUsersCurrentHost + AllUsersAllHosts + CurrentUserAllHosts + CurrentUserCurrentHost + } + + function New-PwshResource { + param( + [Parameter(Mandatory = $true)] + [ProfileType] $ProfileType, + + [Parameter(ParameterSetName = 'WithContent')] + [string] $Content, + + [Parameter(ParameterSetName = 'WithContent')] + [bool] $Exist + ) + + # Create the PSCustomObject with properties + $resource = [PSCustomObject]@{ + profileType = $ProfileType + content = $null + profilePath = GetProfilePath -profileType $ProfileType + _exist = $false + } + + # Add ToJson method + $resource | Add-Member -MemberType ScriptMethod -Name 'ToJson' -Value { + return ([ordered] @{ + profileType = $this.profileType + content = $this.content + profilePath = $this.profilePath + _exist = $this._exist + }) | ConvertTo-Json -Compress -EnumsAsStrings + } + + # Constructor logic - if Content and Exist parameters are provided (WithContent parameter set) + if ($PSCmdlet.ParameterSetName -eq 'WithContent') { + $resource.content = $Content + $resource._exist = $Exist + } else { + # Default constructor logic - read from file system + $fileExists = Test-Path $resource.profilePath + if ($fileExists) { + $resource.content = Get-Content -Path $resource.profilePath + } else { + $resource.content = $null + } + $resource._exist = $fileExists + } + + return $resource + } + + function GetProfilePath { + param ( + [ProfileType] $profileType + ) + + $path = switch ($profileType) { + 'AllUsersCurrentHost' { $PROFILE.AllUsersCurrentHost } + 'AllUsersAllHosts' { $PROFILE.AllUsersAllHosts } + 'CurrentUserAllHosts' { $PROFILE.CurrentUserAllHosts } + 'CurrentUserCurrentHost' { $PROFILE.CurrentUserCurrentHost } + } + + return $path + } + + function ExportOperation { + $allUserCurrentHost = New-PwshResource -ProfileType 'AllUsersCurrentHost' + $allUsersAllHost = New-PwshResource -ProfileType 'AllUsersAllHosts' + $currentUserAllHost = New-PwshResource -ProfileType 'CurrentUserAllHosts' + $currentUserCurrentHost = New-PwshResource -ProfileType 'CurrentUserCurrentHost' + + # Cannot use the ToJson() method here as we are adding a note property + $allUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $allUsersAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $currentUserAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $currentUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + } + + function GetOperation { + param ( + [Parameter(Mandatory = $true)] + $InputResource, + [Parameter()] + [switch] $AsJson + ) + + $profilePath = GetProfilePath -profileType $InputResource.profileType.ToString() + + $actualState = New-PwshResource -ProfileType $InputResource.profileType + + $actualState.profilePath = $profilePath + + $exists = Test-Path $profilePath + + if ($InputResource._exist -and $exists) { + $content = Get-Content -Path $profilePath + $actualState.Content = $content + } elseif ($InputResource._exist -and -not $exists) { + $actualState.Content = $null + $actualState._exist = $false + } elseif (-not $InputResource._exist -and $exists) { + $actualState.Content = Get-Content -Path $profilePath + $actualState._exist = $true + } else { + $actualState.Content = $null + $actualState._exist = $false + } + + if ($AsJson) { + return $actualState.ToJson() + } else { + return $actualState + } + } + + function SetOperation { + param ( + $InputResource + ) + + $actualState = GetOperation -InputResource $InputResource + + if ($InputResource._exist) { + if (-not $actualState._exist) { + $null = New-Item -Path $actualState.profilePath -ItemType File -Force + } + + if ($null -ne $InputResource.content) { + Set-Content -Path $actualState.profilePath -Value $InputResource.content + } + } elseif ($actualState._exist) { + Remove-Item -Path $actualState.profilePath -Force + } + } +} +End { + $inputJson = $input | ConvertFrom-Json + + if ($inputJson) { + $InputResource = New-PwshResource -ProfileType $inputJson.profileType -Content $inputJson.content -Exist $inputJson._exist + } + + switch ($Operation) { + 'get' { + GetOperation -InputResource $InputResource -AsJson + } + 'set' { + SetOperation -InputResource $InputResource + } + 'export' { + if ($inputJson) { + Write-Error "Input not supported for export operation" + exit 2 + } + + ExportOperation + } + } + + exit 0 +} diff --git a/experimental-feature-linux.json b/experimental-feature-linux.json index ca5b49878a..31f7b965a5 100644 --- a/experimental-feature-linux.json +++ b/experimental-feature-linux.json @@ -2,6 +2,7 @@ "PSFeedbackProvider", "PSLoadAssemblyFromNativeCode", "PSNativeWindowsTildeExpansion", + "PSProfileDSCResource", "PSSerializeJSONLongEnumAsNumber", "PSRedirectToVariable", "PSSubsystemPluginModel" diff --git a/experimental-feature-windows.json b/experimental-feature-windows.json index ca5b49878a..31f7b965a5 100644 --- a/experimental-feature-windows.json +++ b/experimental-feature-windows.json @@ -2,6 +2,7 @@ "PSFeedbackProvider", "PSLoadAssemblyFromNativeCode", "PSNativeWindowsTildeExpansion", + "PSProfileDSCResource", "PSSerializeJSONLongEnumAsNumber", "PSRedirectToVariable", "PSSubsystemPluginModel" diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 1c17a22ae9..7e17ec4313 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -21,6 +21,7 @@ namespace System.Management.Automation internal const string EngineSource = "PSEngine"; internal const string PSSerializeJSONLongEnumAsNumber = nameof(PSSerializeJSONLongEnumAsNumber); + internal const string PSProfileDSCResource = "PSProfileDSCResource"; #endregion @@ -109,6 +110,10 @@ namespace System.Management.Automation new ExperimentalFeature( name: PSSerializeJSONLongEnumAsNumber, description: "Serialize enums based on long or ulong as an numeric value rather than the string representation when using ConvertTo-Json." + ), + new ExperimentalFeature( + name: PSProfileDSCResource, + description: "DSC v3 resources for managing PowerShell profile." ) }; diff --git a/src/powershell-unix/powershell-unix.csproj b/src/powershell-unix/powershell-unix.csproj index 802acf05e3..20c61247d2 100644 --- a/src/powershell-unix/powershell-unix.csproj +++ b/src/powershell-unix/powershell-unix.csproj @@ -37,6 +37,10 @@ PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + diff --git a/src/powershell-win-core/powershell-win-core.csproj b/src/powershell-win-core/powershell-win-core.csproj index 5368518dd3..e6efeac10f 100644 --- a/src/powershell-win-core/powershell-win-core.csproj +++ b/src/powershell-win-core/powershell-win-core.csproj @@ -29,6 +29,10 @@ PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + PreserveNewest PreserveNewest diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 new file mode 100644 index 0000000000..f361e67ea8 --- /dev/null +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -0,0 +1,204 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { + BeforeAll { + $DSC_ROOT = $env:DSC_ROOT + + if (-not (Test-Path -Path $DSC_ROOT)) { + throw "DSC_ROOT environment variable is not set or path does not exist." + } + + Write-Verbose "DSC_ROOT is set to $DSC_ROOT" -Verbose + + $originalPath = $env:PATH + + $pathSeparator = [System.IO.Path]::PathSeparator + $env:PATH += "$pathSeparator$DSC_ROOT" + $env:PATH += "$pathSeparator$PSHome" + + Write-Verbose "Updated PATH to include DSC_ROOT: $env:PATH" -Verbose + + # Ensure DSC v3 is available + if (-not (Get-Command -name dsc -CommandType Application -ErrorAction SilentlyContinue)) { + Get-ChildItem $DSC_ROOT -Recurse 'dsc' | ForEach-Object { + Write-Verbose "Found DSC executable at $($_.FullName)" -Verbose + } + throw "DSC v3 is not installed" + } + + $dscExe = Get-Command -name dsc -CommandType Application | Select-Object -First 1 + + $testProfileContent = "# Test profile content currentuser currenthost" + $testProfilePathCurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost + Copy-Item -Path $testProfilePathCurrentUserCurrentHost -Destination "$TestDrive/currentuser-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue + New-Item -Path $testProfilePathCurrentUserCurrentHost -Value $testProfileContent -Force -ItemType File + + $testProfileContent = "# Test profile content currentuser allhosts" + $testProfilePathCurrentUserAllHosts = $PROFILE.CurrentUserAllHosts + Copy-Item -Path $testProfilePathCurrentUserAllHosts -Destination "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + New-Item -Path $testProfilePathCurrentUserAllHosts -Value $testProfileContent -Force -ItemType File + } + AfterAll { + # Restore original profile + $testProfilePathCurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost + if (Test-Path "$TestDrive/currentuser-currenthost-profile.bak") { + Copy-Item -Path "$TestDrive/currentuser-currenthost-profile.bak" -Destination $testProfilePathCurrentUserCurrentHost -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathCurrentUserCurrentHost -Force -ErrorAction SilentlyContinue + } + + $testProfilePathCurrentUserAllHosts = $PROFILE.CurrentUserAllHosts + if (Test-Path "$TestDrive/currentuser-allhosts-profile.bak") { + Copy-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Destination $testProfilePathCurrentUserAllHosts -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathCurrentUserAllHosts -Force -ErrorAction SilentlyContinue + } + + $env:PATH = $originalPath + Remove-Item -Path "$TestDrive/currentuser-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + } + + It 'DSC resource is located at $PSHome' { + $resourceFile = Join-Path -Path $PSHome -ChildPath 'pwsh.profile.resource.ps1' + $resourceFile | Should -Exist + + $resourceManifest = Join-Path -Path $PSHome -ChildPath 'pwsh.profile.dsc.resource.json' + $resourceManifest | Should -Exist + } + + It 'DSC resource can be found' { + (& $dscExe resource list -o json | ConvertFrom-Json | Select-Object -Property type).type | Should -Contain 'Microsoft.PowerShell/Profile' + } + + It 'DSC resource can set current user current host profile' { + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get current user current host profile' { + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can set content as empty for current user current host profile' -Pending { + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_currenthost_emptycontent.dsc.yaml -o json) | ConvertFrom-Json + $setOutput.results.result.afterState.content | Should -BeExactly '' + } + + It 'DSC resource can set current user all hosts profile' { + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get current user all hosts profile' { + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can export all profiles' { + $exportOutput = (& $dscExe config export --file $PSScriptRoot/psprofile_export.dsc.yaml -o json) | ConvertFrom-Json + + $exportOutput.resources | Should -HaveCount 4 + + $exportOutput.resources | ForEach-Object { + $_.type | Should -Be 'Microsoft.PowerShell/Profile' + $_.name | Should -BeIn @('AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserCurrentHost', 'CurrentUserAllHosts') + } + } +} + +Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdminOnWindows', 'RequireSudoOnUnix' { + BeforeAll { + $DSC_ROOT = $env:DSC_ROOT + + if (-not (Test-Path -Path $DSC_ROOT)) { + throw "DSC_ROOT environment variable is not set or path does not exist." + } + + Write-Verbose "DSC_ROOT is set to $DSC_ROOT" -Verbose + $pathSeparator = [System.IO.Path]::PathSeparator + + $env:PATH += "$pathSeparator$DSC_ROOT" + + $env:PATH += "$pathSeparator$PSHome" + + Write-Verbose "Updated PATH to include DSC_ROOT: $env:PATH" -Verbose + + # Ensure DSC v3 is available + if (-not (Get-Command -name dsc -CommandType Application -ErrorAction SilentlyContinue)) { + Get-ChildItem $DSC_ROOT -Recurse 'dsc' | ForEach-Object { + Write-Verbose "Found DSC executable at $($_.FullName)" -Verbose + } + throw "DSC v3 is not installed" + } + + $dscExe = Get-Command -name dsc -CommandType Application | Select-Object -First 1 + + $testProfileContent = "# Test profile content allusers currenthost" + $testProfilePathAllUsersCurrentHost = $PROFILE.AllUsersCurrentHost + Copy-Item -Path $testProfilePathAllUsersCurrentHost -Destination "$TestDrive/allusers-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue + New-Item -Path $testProfilePathAllUsersCurrentHost -Value $testProfileContent -Force -ItemType File + + $testProfileContent = "# Test profile content allusers allhosts" + $testProfilePathAllUsersAllHosts = $PROFILE.AllUsersAllHosts + Copy-Item -Path $testProfilePathAllUsersAllHosts -Destination "$TestDrive/allusers-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + New-Item -Path $testProfilePathAllUsersAllHosts -Value $testProfileContent -Force -ItemType File + + $originalPath = $env:PATH + $env:PATH += "$pathSeparator$PSHome" + } + AfterAll { + $env:PATH = $originalPath + + $testProfilePathAllUsersCurrentHost = $PROFILE.AllUsersCurrentHost + if (Test-Path "$TestDrive/allusers-currenthost-profile.bak") { + Copy-Item -Path "$TestDrive/allusers-currenthost-profile.bak" -Destination $testProfilePathAllUsersCurrentHost -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathAllUsersCurrentHost -Force -ErrorAction SilentlyContinue + } + + $testProfilePathAllUsersAllHosts = $PROFILE.AllUsersAllHosts + if (Test-Path "$TestDrive/allusers-allhosts-profile.bak") { + Copy-Item -Path "$TestDrive/allusers-allhosts-profile.bak" -Destination $testProfilePathAllUsersAllHosts -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathAllUsersAllHosts -Force -ErrorAction SilentlyContinue + } + + Remove-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$TestDrive/allusers-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + } + + It 'DSC resource can set all users all hosts profile' { + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get all users all hosts profile' { + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can set all users current hosts profile' { + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get all users current hosts profile' { + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } +} diff --git a/test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml b/test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml new file mode 100644 index 0000000000..356119826c --- /dev/null +++ b/test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: AllUsersAllHosts + content: "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml b/test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml new file mode 100644 index 0000000000..bc51f0a439 --- /dev/null +++ b/test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: AllUsersCurrentHost + content: "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml b/test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml new file mode 100644 index 0000000000..8ed8d98c3a --- /dev/null +++ b/test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: CurrentUserAllHosts + content: "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml b/test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml new file mode 100644 index 0000000000..5a42c28eb9 --- /dev/null +++ b/test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: CurrentUserCurrentHost + content: "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml b/test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml new file mode 100644 index 0000000000..76439387d2 --- /dev/null +++ b/test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: CurrentUserCurrentHost + content: '' + _exist: true diff --git a/test/powershell/dsc/psprofile_export.dsc.yaml b/test/powershell/dsc/psprofile_export.dsc.yaml new file mode 100644 index 0000000000..cf7881b5df --- /dev/null +++ b/test/powershell/dsc/psprofile_export.dsc.yaml @@ -0,0 +1,5 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile diff --git a/tools/packaging/boms/linux.json b/tools/packaging/boms/linux.json index 9f5f886a4c..db29e47a29 100644 --- a/tools/packaging/boms/linux.json +++ b/tools/packaging/boms/linux.json @@ -2442,5 +2442,15 @@ { "Pattern": "System.Management.Automation.dll", "FileType": "Product" + }, + { + "Pattern": "pwsh.profile.dsc.resource.json", + "FileType": "Product", + "Architecture": null + }, + { + "Pattern": "pwsh.profile.resource.ps1", + "FileType": "Product", + "Architecture": null } ] diff --git a/tools/packaging/boms/mac.json b/tools/packaging/boms/mac.json index a4b5bc2bff..af4fcb0c96 100644 --- a/tools/packaging/boms/mac.json +++ b/tools/packaging/boms/mac.json @@ -2218,5 +2218,15 @@ { "Pattern": "System.Management.Automation.dll", "FileType": "Product" + }, + { + "Pattern": "pwsh.profile.dsc.resource.json", + "FileType": "Product", + "Architecture": null + }, + { + "Pattern": "pwsh.profile.resource.ps1", + "FileType": "Product", + "Architecture": null } ] diff --git a/tools/packaging/boms/windows.json b/tools/packaging/boms/windows.json index 2999db1a18..f811109f81 100644 --- a/tools/packaging/boms/windows.json +++ b/tools/packaging/boms/windows.json @@ -4605,5 +4605,15 @@ "Pattern": "System.Management.Automation.dll", "FileType": "Product", "Architecture": null + }, + { + "Pattern": "pwsh.profile.dsc.resource.json", + "FileType": "Product", + "Architecture": null + }, + { + "Pattern": "pwsh.profile.resource.ps1", + "FileType": "Product", + "Architecture": null } ] From 250e18cc9bbbf06a5562f3071355ff24ce92f880 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:06:49 +0000 Subject: [PATCH 002/112] Refactor IsComputerNameValid character validation (#26274) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../commands/management/Computer.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/Computer.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/Computer.cs index 0ea1c91aae..ea8c111531 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/Computer.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/Computer.cs @@ -2019,35 +2019,24 @@ $result /// internal static bool IsComputerNameValid(string computerName) { - bool allDigits = true; + bool hasAsciiLetterOrHyphen = false; if (computerName.Length >= 64) return false; foreach (char t in computerName) { - if (t >= 'A' && t <= 'Z' || - t >= 'a' && t <= 'z') + if (char.IsAsciiLetter(t) || t is '-') { - allDigits = false; - continue; + hasAsciiLetterOrHyphen = true; } - else if (t >= '0' && t <= '9') - { - continue; - } - else if (t == '-') - { - allDigits = false; - continue; - } - else + else if (!char.IsAsciiDigit(t)) { return false; } } - return !allDigits; + return hasAsciiLetterOrHyphen; } /// From 77f1d99568dd809d6cbc10f02ea6a586070a5ea9 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Mon, 17 Nov 2025 12:36:48 +0900 Subject: [PATCH 003/112] Respect -Shuffle:$false in Get-Random (#26457) --- .../commands/utility/GetRandomCommandBase.cs | 2 +- .../Microsoft.PowerShell.Utility/Get-Random.Tests.ps1 | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommandBase.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommandBase.cs index fef7764c5b..8e50b1cf5a 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommandBase.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/GetRandomCommandBase.cs @@ -488,7 +488,7 @@ namespace Microsoft.PowerShell.Commands { if (EffectiveParameterSet == MyParameterSet.RandomListItem) { - if (ParameterSetName == ShuffleParameterSet) + if (Shuffle) { // this allows for $null to be in an array passed to InputObject foreach (object item in InputObject ?? _nullInArray) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Random.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Random.Tests.ps1 index cd9fee4187..100ca90b35 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Random.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-Random.Tests.ps1 @@ -162,6 +162,12 @@ Describe "Get-Random" -Tags "CI" { $randomNumber | Should -BeIn 1, 2, 3, 5, 8, 13 } + It "Should return a single random item when -Shuffle:`$false is used" { + $randomNumber = Get-Random -InputObject 1, 2, 3, 5, 8, 13 -Shuffle:$false + $randomNumber.Count | Should -Be 1 + $randomNumber | Should -BeIn 1, 2, 3, 5, 8, 13 + } + It "Should return for a string collection " { $randomNumber = Get-Random -InputObject "red", "yellow", "blue" $randomNumber | Should -Be ("red" -or "yellow" -or "blue") From e741cecab394958337a21fd54e280950197df44b Mon Sep 17 00:00:00 2001 From: yotsuda Date: Mon, 17 Nov 2025 15:45:14 +0900 Subject: [PATCH 004/112] Respect -Shuffle:$false in Get-SecureRandom (#26460) --- .../Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 index acc560abdb..957bfa30e9 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Get-SecureRandom.Tests.ps1 @@ -150,6 +150,12 @@ Describe "Get-SecureRandom" -Tags "CI" { $randomNumber | Should -BeIn 1, 2, 3, 5, 8, 13 } + It "Should return a single random item when -Shuffle:`$false is used" { + $randomNumber = Get-SecureRandom -InputObject 1, 2, 3, 5, 8, 13 -Shuffle:$false + $randomNumber.Count | Should -Be 1 + $randomNumber | Should -BeIn 1, 2, 3, 5, 8, 13 + } + It "Should return for a string collection " { $randomNumber = Get-SecureRandom -InputObject "red", "yellow", "blue" $randomNumber | Should -Be ("red" -or "yellow" -or "blue") From 11d154c6730dc2ef05c4df09de119b2dc5a4fa88 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Tue, 18 Nov 2025 02:04:57 +0900 Subject: [PATCH 005/112] Respect -ListAvailable:$false in Get-TimeZone (#26463) --- .../commands/management/TimeZoneCommands.cs | 2 +- .../Microsoft.PowerShell.Management/TimeZone.Tests.ps1 | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/TimeZoneCommands.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/TimeZoneCommands.cs index 8fdb2c55ac..22a50e4117 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/TimeZoneCommands.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/TimeZoneCommands.cs @@ -54,7 +54,7 @@ namespace Microsoft.PowerShell.Commands // make sure we've got the latest time zone settings TimeZoneInfo.ClearCachedData(); - if (this.ParameterSetName.Equals("ListAvailable", StringComparison.OrdinalIgnoreCase)) + if (ListAvailable) { // output the list of all available time zones WriteObject(TimeZoneInfo.GetSystemTimeZones(), true); diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/TimeZone.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/TimeZone.Tests.ps1 index 5a23c7df75..a3904fca41 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/TimeZone.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/TimeZone.Tests.ps1 @@ -71,6 +71,12 @@ Describe "Get-Timezone test cases" -Tags "CI" { $oneExpectedOffset | Should -BeIn $observedIdList } + It "Call with -ListAvailable:`$false returns current TimeZoneInfo (not list)" { + $result = Get-TimeZone -ListAvailable:$false + $result | Should -BeOfType TimeZoneInfo + $result.Id | Should -Be ([System.TimeZoneInfo]::Local).Id + } + It "Call Get-TimeZone using ID param and single item" { $selectedTZ = $TimeZonesAvailable[0] (Get-TimeZone -Id $selectedTZ.Id).Id | Should -Be $selectedTZ.Id From 6d61ef1034f4b6866fa1e727eb1d157427df28d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:51:56 -0800 Subject: [PATCH 006/112] Bump github/codeql-action from 4.31.2 to 4.31.3 (#26451) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index a16a840a60..6b9878be9e 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 + uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 + uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 4246af344f..4c502ed38a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@0499de31b99561a6d14a36a5f662c2a54f91beee # v3.29.5 + uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 with: sarif_file: results.sarif From bb44aa1bc4e83dcf4ab7cf31bf9ae241b395f86b Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Mon, 17 Nov 2025 21:56:51 +0200 Subject: [PATCH 007/112] Mirror .NET/runtime ICU version range in PowerShell Linux packaging (#26304) --- tools/packaging/packaging.psm1 | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/tools/packaging/packaging.psm1 b/tools/packaging/packaging.psm1 index baeb12e360..8f9cec7179 100644 --- a/tools/packaging/packaging.psm1 +++ b/tools/packaging/packaging.psm1 @@ -2125,6 +2125,22 @@ function Get-PackageDependencies # These should match those in the Dockerfiles, but exclude tools like Git, which, and curl $Dependencies = @() + + # ICU version range follows .NET runtime policy. + # See: https://github.com/dotnet/runtime/blob/3fe8518d51bbcaa179bbe275b2597fbe1b88bc5a/src/native/libs/System.Globalization.Native/pal_icushim.c#L235-L243 + # + # Version range rationale: + # - The runtime supports ICU versions >= the version it was built against + # and <= that version + 30, to allow sufficient headroom for future releases. + # - ICU typically releases about twice per year, so +30 provides roughly + # 15 years of forward compatibility. + # - On some platforms, the minimum supported version may be lower + # than the build version and we know that older versions just works. + # + $MinICUVersion = 60 # runtime minimum supported + $BuildICUVersion = 76 # current build version + $MaxICUVersion = $BuildICUVersion + 30 # headroom + if ($Distribution -eq 'deb') { $Dependencies = @( "libc6", @@ -2132,10 +2148,9 @@ function Get-PackageDependencies "libgssapi-krb5-2", "libstdc++6", "zlib1g", - "libicu76|libicu74|libicu72|libicu71|libicu70|libicu69|libicu68|libicu67|libicu66|libicu65|libicu63|libicu60|libicu57|libicu55|libicu52", + (($MaxICUVersion..$MinICUVersion).ForEach{ "libicu$_" } -join '|'), "libssl3|libssl1.1|libssl1.0.2|libssl1.0.0" ) - } elseif ($Distribution -eq 'rh') { $Dependencies = @( "openssl-libs", From 015b5cd80152c3f4e44b40293a18e7798a18b1aa Mon Sep 17 00:00:00 2001 From: PowerShell Team Bot <69177312+pwshBot@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:26:45 -0800 Subject: [PATCH 008/112] Update build to use .NET SDK 10.0.100 (#26448) --- DotnetRuntimeMetadata.json | 2 +- global.json | 2 +- ...oft.PowerShell.Commands.Diagnostics.csproj | 2 +- ...soft.PowerShell.Commands.Management.csproj | 2 +- ...crosoft.PowerShell.Commands.Utility.csproj | 2 +- ...crosoft.PowerShell.CoreCLR.Eventing.csproj | 2 +- .../Microsoft.PowerShell.SDK.csproj | 10 +- .../Microsoft.WSMan.Management.csproj | 2 +- .../System.Management.Automation.csproj | 12 +- .../BenchmarkDotNet.Extensions.csproj | 4 +- .../ResultsComparer/ResultsComparer.csproj | 4 +- .../nested/Test.Isolated.Nested.csproj | 2 +- test/tools/TestService/TestService.csproj | 2 +- test/tools/WebListener/WebListener.csproj | 2 +- test/xUnit/xUnit.tests.csproj | 2 +- tools/cgmanifest.json | 162 +++++++----------- 16 files changed, 87 insertions(+), 127 deletions(-) diff --git a/DotnetRuntimeMetadata.json b/DotnetRuntimeMetadata.json index 8541f8ba79..267f0bda7d 100644 --- a/DotnetRuntimeMetadata.json +++ b/DotnetRuntimeMetadata.json @@ -4,7 +4,7 @@ "quality": "daily", "qualityFallback": "preview", "packageVersionPattern": "9.0.0-preview.6", - "sdkImageVersion": "10.0.100-rc.1.25451.107", + "sdkImageVersion": "10.0.100", "nextChannel": "9.0.0-preview.7", "azureFeed": "", "sdkImageOverride": "" diff --git a/global.json b/global.json index 83e6a8b43e..376af49c07 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "10.0.100-rc.2.25502.107" + "version": "10.0.100" } } diff --git a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj index cc3760db39..a338c175f1 100644 --- a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj +++ b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj index f5f6fede1a..ef94950834 100644 --- a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj +++ b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index da15fe8235..1f9f342b25 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj index 228d636002..45b4d8c0a0 100644 --- a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj +++ b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj index 57994fb98f..87f71daada 100644 --- a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj +++ b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj @@ -16,19 +16,19 @@ - - + + - - + + - + diff --git a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj index bef80519d5..48b4d8ff29 100644 --- a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj +++ b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 114a4c09aa..e929166b20 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -32,12 +32,12 @@ - - - - - - + + + + + + diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj index d67f17c60f..91566f64fb 100644 --- a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj index 3274f04ace..af3fc10ffc 100644 --- a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj +++ b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj @@ -9,7 +9,7 @@ - - + + diff --git a/test/tools/TestAlc/nested/Test.Isolated.Nested.csproj b/test/tools/TestAlc/nested/Test.Isolated.Nested.csproj index de06b206f3..886b5f1182 100644 --- a/test/tools/TestAlc/nested/Test.Isolated.Nested.csproj +++ b/test/tools/TestAlc/nested/Test.Isolated.Nested.csproj @@ -19,7 +19,7 @@ - + diff --git a/test/tools/TestService/TestService.csproj b/test/tools/TestService/TestService.csproj index d2004ea2fd..15a6e49864 100644 --- a/test/tools/TestService/TestService.csproj +++ b/test/tools/TestService/TestService.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/tools/WebListener/WebListener.csproj b/test/tools/WebListener/WebListener.csproj index 056f6810c7..1dddf99ba5 100644 --- a/test/tools/WebListener/WebListener.csproj +++ b/test/tools/WebListener/WebListener.csproj @@ -7,6 +7,6 @@ - + diff --git a/test/xUnit/xUnit.tests.csproj b/test/xUnit/xUnit.tests.csproj index 5cb4a1e48d..0863a23d44 100644 --- a/test/xUnit/xUnit.tests.csproj +++ b/test/xUnit/xUnit.tests.csproj @@ -30,7 +30,7 @@ all - + diff --git a/tools/cgmanifest.json b/tools/cgmanifest.json index 2693680461..d8165298b2 100644 --- a/tools/cgmanifest.json +++ b/tools/cgmanifest.json @@ -86,7 +86,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Bcl.AsyncInterfaces", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -126,7 +126,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Extensions.ObjectPool", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -166,7 +166,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.Registry.AccessControl", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -176,7 +176,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.SystemEvents", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -186,7 +186,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Windows.Compatibility", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -206,7 +206,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -216,7 +216,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -226,7 +226,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -236,7 +236,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x86.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -246,7 +246,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -256,7 +256,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -266,7 +266,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -276,7 +276,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-x64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -286,7 +286,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -296,7 +296,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -306,7 +306,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-x64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -316,7 +316,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-x64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -326,7 +326,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -336,7 +336,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-x64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -356,7 +356,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -366,7 +366,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-arm64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -376,7 +376,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-x64.runtime.native.System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -436,7 +436,7 @@ "Type": "nuget", "Nuget": { "Name": "System.CodeDom", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -446,7 +446,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition.Registration", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -456,7 +456,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -466,7 +466,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Configuration.ConfigurationManager", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -476,7 +476,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.Odbc", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -486,7 +486,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.OleDb", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -506,7 +506,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.EventLog", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -516,7 +516,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.PerformanceCounter", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -526,7 +526,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.AccountManagement", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -536,7 +536,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.Protocols", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -546,7 +546,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -556,7 +556,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Drawing.Common", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -566,7 +566,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Packaging", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -576,7 +576,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Ports", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -586,7 +586,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Management", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -596,17 +596,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Net.Http.WinHttpHandler", - "Version": "9.0.10" - } - }, - "DevelopmentDependency": false - }, - { - "Component": { - "Type": "nuget", - "Nuget": { - "Name": "System.Private.ServiceModel", - "Version": "4.10.3" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -616,7 +606,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Reflection.Context", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -626,7 +616,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Runtime.Caching", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -636,7 +626,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Pkcs", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -646,7 +636,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.ProtectedData", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -656,7 +646,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Xml", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -666,17 +656,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Permissions", - "Version": "9.0.10" - } - }, - "DevelopmentDependency": false - }, - { - "Component": { - "Type": "nuget", - "Nuget": { - "Name": "System.ServiceModel.Duplex", - "Version": "4.10.3" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -686,7 +666,17 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Http", - "Version": "4.10.3" + "Version": "8.1.2" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ServiceModel.NetFramingBase", + "Version": "8.1.2" } }, "DevelopmentDependency": false @@ -696,7 +686,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.NetTcp", - "Version": "4.10.3" + "Version": "8.1.2" } }, "DevelopmentDependency": false @@ -706,17 +696,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Primitives", - "Version": "4.10.3" - } - }, - "DevelopmentDependency": false - }, - { - "Component": { - "Type": "nuget", - "Nuget": { - "Name": "System.ServiceModel.Security", - "Version": "4.10.3" + "Version": "8.1.2" } }, "DevelopmentDependency": false @@ -726,7 +706,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Syndication", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -736,7 +716,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceProcess.ServiceController", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -746,27 +726,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Speech", - "Version": "9.0.10" - } - }, - "DevelopmentDependency": false - }, - { - "Component": { - "Type": "nuget", - "Nuget": { - "Name": "System.Text.Encoding.CodePages", - "Version": "9.0.10" - } - }, - "DevelopmentDependency": false - }, - { - "Component": { - "Type": "nuget", - "Nuget": { - "Name": "System.Threading.AccessControl", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false @@ -786,7 +746,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Windows.Extensions", - "Version": "9.0.10" + "Version": "10.0.0" } }, "DevelopmentDependency": false From d486365b246c29ad9bc2704452ead96083e93a40 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Tue, 18 Nov 2025 21:49:38 +0900 Subject: [PATCH 009/112] Fix Invoke-RestMethod to support read-only files in multipart form data (#26454) --- .../Common/WebRequestPSCmdlet.Common.cs | 2 +- .../WebCmdlets.Tests.ps1 | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs index 7a784a1e49..f1a455974b 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/WebRequestPSCmdlet.Common.cs @@ -1995,7 +1995,7 @@ namespace Microsoft.PowerShell.Commands /// The file to use for the private static StreamContent GetMultipartFileContent(object fieldName, FileInfo file) { - StreamContent result = GetMultipartStreamContent(fieldName: fieldName, stream: new FileStream(file.FullName, FileMode.Open)); + StreamContent result = GetMultipartStreamContent(fieldName: fieldName, stream: new FileStream(file.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)); result.Headers.ContentDisposition.FileName = file.Name; result.Headers.ContentDisposition.FileNameStar = file.Name; diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 index 1ddb9f10ba..7c0fffa5c4 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/WebCmdlets.Tests.ps1 @@ -1727,6 +1727,26 @@ Describe "Invoke-WebRequest tests" -Tags "Feature", "RequireAdminOnWindows" { $result.Files[0].Content | Should -Match $file1Contents } + It "Verifies Invoke-WebRequest -Form supports read-only file values" { + # Create a read-only test file + $readOnlyFile = Join-Path $TestDrive "readonly-test.txt" + "ReadOnly test content" | Out-File -FilePath $readOnlyFile -Encoding utf8 + Set-ItemProperty -Path $readOnlyFile -Name IsReadOnly -Value $true + + $form = @{TestFile = [System.IO.FileInfo]$readOnlyFile} + $uri = Get-WebListenerUrl -Test 'Multipart' + $response = Invoke-WebRequest -Uri $uri -Form $form -Method 'POST' + $result = $response.Content | ConvertFrom-Json + + $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' + $result.Files.Count | Should -Be 1 + + $result.Files[0].Name | Should -BeExactly "TestFile" + $result.Files[0].FileName | Should -BeExactly "readonly-test.txt" + $result.Files[0].ContentType | Should -BeExactly 'application/octet-stream' + $result.Files[0].Content | Should -Match "ReadOnly test content" + } + It "Verifies Invoke-WebRequest -Form sets Content-Disposition FileName and FileNameStar." { $ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("attachment") $ContentDisposition.FileName = $fileName @@ -3568,6 +3588,25 @@ Describe "Invoke-RestMethod tests" -Tags "Feature", "RequireAdminOnWindows" { $result.Files[0].Content | Should -Match $file1Contents } + It "Verifies Invoke-RestMethod -Form supports read-only file values" { + # Create a read-only test file + $readOnlyFile = Join-Path $TestDrive "readonly-test.txt" + "ReadOnly test content" | Out-File -FilePath $readOnlyFile -Encoding utf8 + Set-ItemProperty -Path $readOnlyFile -Name IsReadOnly -Value $true + + $form = @{TestFile = [System.IO.FileInfo]$readOnlyFile} + $uri = Get-WebListenerUrl -Test 'Multipart' + $result = Invoke-RestMethod -Uri $uri -Form $form -Method 'POST' + + $result.Headers.'Content-Type' | Should -Match 'multipart/form-data' + $result.Files.Count | Should -Be 1 + + $result.Files[0].Name | Should -Be "TestFile" + $result.Files[0].FileName | Should -Be "readonly-test.txt" + $result.Files[0].ContentType | Should -Be 'application/octet-stream' + $result.Files[0].Content | Should -Match "ReadOnly test content" + } + It "Verifies Invoke-RestMethod -Form sets Content-Disposition FileName and FileNameStar." { $ContentDisposition = [System.Net.Http.Headers.ContentDispositionHeaderValue]::new("attachment") $ContentDisposition.FileName = $fileName From 6928efb4c5224805c9d6f9fef5a20e26a20fd89c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 19 Nov 2025 14:47:44 -0800 Subject: [PATCH 010/112] Fix GitHub API rate limit errors in test actions (#26489) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- .github/actions/test/nix/action.yml | 10 ++++++++-- .github/actions/test/windows/action.yml | 10 ++++++++-- .github/workflows/linux-ci.yml | 4 ++++ .github/workflows/macos-ci.yml | 4 ++++ .github/workflows/windows-ci.yml | 4 ++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/.github/actions/test/nix/action.yml b/.github/actions/test/nix/action.yml index b8c1778974..7f68e71c1f 100644 --- a/.github/actions/test/nix/action.yml +++ b/.github/actions/test/nix/action.yml @@ -14,6 +14,9 @@ inputs: required: false default: ctrf type: string + GITHUB_TOKEN: + description: 'GitHub token for API authentication' + required: true runs: using: composite @@ -64,7 +67,10 @@ runs: shell: pwsh run: |- Import-Module ./.github/workflows/GHWorkflowHelper/GHWorkflowHelper.psm1 - $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" + $headers = @{ + Authorization = "Bearer ${{ inputs.GITHUB_TOKEN }}" + } + $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" -Headers $headers $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 $latestVersion = $latestRelease.tag_name.TrimStart("v") Write-Host "Latest DSC Version: $latestVersion" @@ -78,7 +84,7 @@ runs: $tempPath = Get-GWTempPath - Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath/DSC.tar.gz" -Verbose + Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath/DSC.tar.gz" -Verbose -Headers $headers New-Item -ItemType Directory -Path "$tempPath/DSC" -Force -Verbose tar xvf "$tempPath/DSC.tar.gz" -C "$tempPath/DSC" $dscRoot = "$tempPath/DSC" diff --git a/.github/actions/test/windows/action.yml b/.github/actions/test/windows/action.yml index 923e9606e5..2c41f6aac5 100644 --- a/.github/actions/test/windows/action.yml +++ b/.github/actions/test/windows/action.yml @@ -14,6 +14,9 @@ inputs: required: false default: ctrf type: string + GITHUB_TOKEN: + description: 'GitHub token for API authentication' + required: true runs: using: composite @@ -47,7 +50,10 @@ runs: shell: pwsh run: |- Import-Module .\.github\workflows\GHWorkflowHelper\GHWorkflowHelper.psm1 - $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" + $headers = @{ + Authorization = "Bearer ${{ inputs.GITHUB_TOKEN }}" + } + $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" -Headers $headers $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 $latestVersion = $latestRelease.tag_name.TrimStart("v") Write-Host "Latest DSC Version: $latestVersion" @@ -55,7 +61,7 @@ runs: $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "DSC-*-x86_64-pc-windows-msvc.zip" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url Write-Host "Download URL: $downloadUrl" $tempPath = Get-GWTempPath - Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath\DSC.zip" + Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath\DSC.zip" -Headers $headers $null = New-Item -ItemType Directory -Path "$tempPath\DSC" -Force Expand-Archive -Path "$tempPath\DSC.zip" -DestinationPath "$tempPath\DSC" -Force diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index 2058bd6156..030139b6c3 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -111,6 +111,7 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} linux_test_elevated_ci: name: Linux Elevated CI needs: @@ -128,6 +129,7 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} linux_test_unelevated_others: name: Linux Unelevated Others needs: @@ -145,6 +147,7 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} linux_test_elevated_others: name: Linux Elevated Others needs: @@ -162,6 +165,7 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} xunit_tests: name: xUnit Tests needs: diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index bdb7c128d1..721077a6b7 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -93,6 +93,7 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} macos_test_elevated_ci: name: macOS Elevated CI needs: @@ -110,6 +111,7 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} macos_test_unelevated_others: name: macOS Unelevated Others needs: @@ -127,6 +129,7 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} macos_test_elevated_others: name: macOS Elevated Others needs: @@ -144,6 +147,7 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} xunit_tests: name: xUnit Tests needs: diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 582860de34..4f774fcdc9 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -97,6 +97,7 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} windows_test_elevated_ci: name: Windows Elevated CI needs: @@ -114,6 +115,7 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} windows_test_unelevated_others: name: Windows Unelevated Others needs: @@ -131,6 +133,7 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} windows_test_elevated_others: name: Windows Elevated Others needs: @@ -148,6 +151,7 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} xunit_tests: name: xUnit Tests needs: From 230970bb5dcf16939533f6038e02c484aa50998f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:07:46 -0800 Subject: [PATCH 011/112] Bump github/codeql-action from 4.31.3 to 4.31.4 (#26484) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 6b9878be9e..9ed85e11e2 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 + uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 + uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 4c502ed38a..3f5951951e 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@014f16e7ab1402f30e7c3329d33797e7948572db # v3.29.5 + uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v3.29.5 with: sarif_file: results.sarif From 108eb857bb2beed653556857f1fb551bdbdd1f0a Mon Sep 17 00:00:00 2001 From: yotsuda Date: Fri, 21 Nov 2025 21:06:23 +0900 Subject: [PATCH 012/112] Correct handling of explicit -[Operator]:$false parameter values in Where-Object (#26485) --- .../engine/InternalCommands.cs | 157 +++++++++--- .../Where-Object.Tests.ps1 | 238 ++++++++++++++++++ 2 files changed, 363 insertions(+), 32 deletions(-) diff --git a/src/System.Management.Automation/engine/InternalCommands.cs b/src/System.Management.Automation/engine/InternalCommands.cs index 844a965ac1..4e50e2d130 100644 --- a/src/System.Management.Automation/engine/InternalCommands.cs +++ b/src/System.Management.Automation/engine/InternalCommands.cs @@ -1441,8 +1441,11 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ieq; - _forceBooleanEvaluation = false; + if (value) + { + _binaryOperator = TokenKind.Ieq; + _forceBooleanEvaluation = false; + } } } @@ -1459,7 +1462,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ceq; + if (value) + { + _binaryOperator = TokenKind.Ceq; + } } } @@ -1477,7 +1483,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ine; + if (value) + { + _binaryOperator = TokenKind.Ine; + } } } @@ -1494,7 +1503,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cne; + if (value) + { + _binaryOperator = TokenKind.Cne; + } } } @@ -1512,7 +1524,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Igt; + if (value) + { + _binaryOperator = TokenKind.Igt; + } } } @@ -1529,7 +1544,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cgt; + if (value) + { + _binaryOperator = TokenKind.Cgt; + } } } @@ -1547,7 +1565,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ilt; + if (value) + { + _binaryOperator = TokenKind.Ilt; + } } } @@ -1564,7 +1585,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Clt; + if (value) + { + _binaryOperator = TokenKind.Clt; + } } } @@ -1582,7 +1606,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ige; + if (value) + { + _binaryOperator = TokenKind.Ige; + } } } @@ -1599,7 +1626,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cge; + if (value) + { + _binaryOperator = TokenKind.Cge; + } } } @@ -1617,7 +1647,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ile; + if (value) + { + _binaryOperator = TokenKind.Ile; + } } } @@ -1634,7 +1667,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cle; + if (value) + { + _binaryOperator = TokenKind.Cle; + } } } @@ -1652,7 +1688,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ilike; + if (value) + { + _binaryOperator = TokenKind.Ilike; + } } } @@ -1669,7 +1708,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Clike; + if (value) + { + _binaryOperator = TokenKind.Clike; + } } } @@ -1687,7 +1729,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Inotlike; + if (value) + { + _binaryOperator = TokenKind.Inotlike; + } } } @@ -1704,7 +1749,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cnotlike; + if (value) + { + _binaryOperator = TokenKind.Cnotlike; + } } } @@ -1722,7 +1770,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Imatch; + if (value) + { + _binaryOperator = TokenKind.Imatch; + } } } @@ -1739,7 +1790,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cmatch; + if (value) + { + _binaryOperator = TokenKind.Cmatch; + } } } @@ -1757,7 +1811,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Inotmatch; + if (value) + { + _binaryOperator = TokenKind.Inotmatch; + } } } @@ -1774,7 +1831,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cnotmatch; + if (value) + { + _binaryOperator = TokenKind.Cnotmatch; + } } } @@ -1792,7 +1852,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Icontains; + if (value) + { + _binaryOperator = TokenKind.Icontains; + } } } @@ -1809,7 +1872,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Ccontains; + if (value) + { + _binaryOperator = TokenKind.Ccontains; + } } } @@ -1827,7 +1893,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Inotcontains; + if (value) + { + _binaryOperator = TokenKind.Inotcontains; + } } } @@ -1844,7 +1913,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cnotcontains; + if (value) + { + _binaryOperator = TokenKind.Cnotcontains; + } } } @@ -1862,7 +1934,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.In; + if (value) + { + _binaryOperator = TokenKind.In; + } } } @@ -1879,7 +1954,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cin; + if (value) + { + _binaryOperator = TokenKind.Cin; + } } } @@ -1897,7 +1975,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Inotin; + if (value) + { + _binaryOperator = TokenKind.Inotin; + } } } @@ -1914,7 +1995,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Cnotin; + if (value) + { + _binaryOperator = TokenKind.Cnotin; + } } } @@ -1931,7 +2015,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Is; + if (value) + { + _binaryOperator = TokenKind.Is; + } } } @@ -1948,7 +2035,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.IsNot; + if (value) + { + _binaryOperator = TokenKind.IsNot; + } } } @@ -1965,7 +2055,10 @@ namespace Microsoft.PowerShell.Commands set { - _binaryOperator = TokenKind.Not; + if (value) + { + _binaryOperator = TokenKind.Not; + } } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Where-Object.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Where-Object.Tests.ps1 index c12a1bdb51..eae7d9951a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Where-Object.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Where-Object.Tests.ps1 @@ -142,4 +142,242 @@ Describe "Where-Object" -Tags "CI" { $Result[0] | Should -Be $dynObj $Result[1] | Should -Be $dynObj } + + Context "Switch parameter explicit `$false handling" { + BeforeAll { + $testData = @( + [PSCustomObject]@{ Name = 'Alice'; Age = 25; City = 'New York' } + [PSCustomObject]@{ Name = 'Bob'; Age = 30; City = 'Boston' } + [PSCustomObject]@{ Name = 'Charlie'; Age = 35; City = 'Chicago' } + ) + } + + It "-EQ:`$false should behave like default boolean evaluation" { + # With -EQ:$false, the parameter is not specified, so default boolean evaluation applies + # "... | Where-Object Name" is equivalent to "... | Where-Object {$true -eq $_.Name}" + # Non-empty strings are truthy + $result = $testData | Where-Object Name -EQ:$false + $result | Should -HaveCount 3 + } + + It "-GT:`$false should behave like default boolean evaluation" { + # With -GT:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -GT:$false + $result | Should -HaveCount 3 + } + + It "-LT:`$false should behave like default boolean evaluation" { + # With -LT:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -LT:$false + $result | Should -HaveCount 3 + } + + It "-NE:`$false should behave like default boolean evaluation" { + # With -NE:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -NE:$false + $result | Should -HaveCount 3 + } + + It "-GE:`$false should behave like default boolean evaluation" { + # With -GE:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -GE:$false + $result | Should -HaveCount 3 + } + + It "-LE:`$false should behave like default boolean evaluation" { + # With -LE:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -LE:$false + $result | Should -HaveCount 3 + } + + It "-NotLike:`$false should behave like default boolean evaluation" { + # With -NotLike:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object City -NotLike:$false + $result | Should -HaveCount 3 + } + + It "-NotMatch:`$false should behave like default boolean evaluation" { + # With -NotMatch:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -NotMatch:$false + $result | Should -HaveCount 3 + } + + It "-NotContains:`$false should behave like default boolean evaluation" { + # With -NotContains:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -NotContains:$false + $result | Should -HaveCount 3 + } + + It "-NotIn:`$false should behave like default boolean evaluation" { + # With -NotIn:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -NotIn:$false + $result | Should -HaveCount 3 + } + + It "-IsNot:`$false should behave like default boolean evaluation" { + # With -IsNot:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -IsNot:$false + $result | Should -HaveCount 3 + } + + It "-Like:`$false should behave like default boolean evaluation" { + # With -Like:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object City -Like:$false + $result | Should -HaveCount 3 + } + + It "-Match:`$false should behave like default boolean evaluation" { + # With -Match:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -Match:$false + $result | Should -HaveCount 3 + } + + It "-Contains:`$false should behave like default boolean evaluation" { + # With -Contains:$false, the parameter is not specified, so default boolean evaluation applies + # "... | Where-Object Name" checks if Name property is truthy + $result = $testData | Where-Object Name -Contains:$false + $result | Should -HaveCount 3 + } + + It "-In:`$false should behave like default boolean evaluation" { + # With -In:$false, the parameter is not specified, so default boolean evaluation applies + # "... | Where-Object Name" checks if Name property is truthy + $result = $testData | Where-Object Name -In:$false + $result | Should -HaveCount 3 + } + + It "-Is:`$false should behave like default boolean evaluation" { + # With -Is:$false, the parameter is not specified, so default boolean evaluation applies + # "... | Where-Object Name" checks if Name property is truthy + $result = $testData | Where-Object Name -Is:$false + $result | Should -HaveCount 3 + } + + It "-Not:`$false should behave like default boolean evaluation" { + $testData2 = @( + [PSCustomObject]@{ Name = ''; Value = 1 } + [PSCustomObject]@{ Name = 'Test'; Value = 2 } + ) + # With -Not:$false, the parameter is not specified, so default boolean evaluation applies + # "... | Where-Object Name" returns objects where Name is truthy (non-empty) + $result = $testData2 | Where-Object Name -Not:$false + $result | Should -HaveCount 1 + $result.Name | Should -Be 'Test' + } + + It "-GT:`$false with -Value should throw an error requiring an operator" { + # When a switch parameter is set to $false with -Value specified, + # it should throw an error because no valid operator is active + { $testData | Where-Object Age -GT:$false -Value 25 } | Should -Throw -ErrorId 'OperatorNotSpecified,Microsoft.PowerShell.Commands.WhereObjectCommand' + } + + It "-EQ:`$false with -Value should throw an error requiring an operator" { + # Similar behavior for -EQ:$false with -Value + { $testData | Where-Object Name -EQ:$false -Value 'Alice' } | Should -Throw -ErrorId 'OperatorNotSpecified,Microsoft.PowerShell.Commands.WhereObjectCommand' + } + + It "-Like:`$false with -Value should throw an error requiring an operator" { + # Similar behavior for -Like:$false with -Value + { $testData | Where-Object Name -Like:$false -Value 'A*' } | Should -Throw -ErrorId 'OperatorNotSpecified,Microsoft.PowerShell.Commands.WhereObjectCommand' + } + + It "-CEQ:`$false should behave like default boolean evaluation" { + # With -CEQ:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CEQ:$false + $result | Should -HaveCount 3 + } + + It "-CNE:`$false should behave like default boolean evaluation" { + # With -CNE:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CNE:$false + $result | Should -HaveCount 3 + } + + It "-CGT:`$false should behave like default boolean evaluation" { + # With -CGT:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -CGT:$false + $result | Should -HaveCount 3 + } + + It "-CLT:`$false should behave like default boolean evaluation" { + # With -CLT:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -CLT:$false + $result | Should -HaveCount 3 + } + + It "-CGE:`$false should behave like default boolean evaluation" { + # With -CGE:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -CGE:$false + $result | Should -HaveCount 3 + } + + It "-CLE:`$false should behave like default boolean evaluation" { + # With -CLE:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Age -CLE:$false + $result | Should -HaveCount 3 + } + + It "-CLike:`$false should behave like default boolean evaluation" { + # With -CLike:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object City -CLike:$false + $result | Should -HaveCount 3 + } + + It "-CNotLike:`$false should behave like default boolean evaluation" { + # With -CNotLike:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object City -CNotLike:$false + $result | Should -HaveCount 3 + } + + It "-CMatch:`$false should behave like default boolean evaluation" { + # With -CMatch:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CMatch:$false + $result | Should -HaveCount 3 + } + + It "-CNotMatch:`$false should behave like default boolean evaluation" { + # With -CNotMatch:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CNotMatch:$false + $result | Should -HaveCount 3 + } + + It "-CContains:`$false should behave like default boolean evaluation" { + # With -CContains:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CContains:$false + $result | Should -HaveCount 3 + } + + It "-CNotContains:`$false should behave like default boolean evaluation" { + # With -CNotContains:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CNotContains:$false + $result | Should -HaveCount 3 + } + + It "-CIn:`$false should behave like default boolean evaluation" { + # With -CIn:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CIn:$false + $result | Should -HaveCount 3 + } + + It "-CNotIn:`$false should behave like default boolean evaluation" { + # With -CNotIn:$false, the parameter is not specified, so default boolean evaluation applies + $result = $testData | Where-Object Name -CNotIn:$false + $result | Should -HaveCount 3 + } + + It "-CEQ:`$false with -Value should throw an error requiring an operator" { + # Similar behavior for -CEQ:$false with -Value + { $testData | Where-Object Name -CEQ:$false -Value 'Alice' } | Should -Throw -ErrorId 'OperatorNotSpecified,Microsoft.PowerShell.Commands.WhereObjectCommand' + } + + It "-CGT:`$false with -Value should throw an error requiring an operator" { + # Similar behavior for -CGT:$false with -Value + { $testData | Where-Object Age -CGT:$false -Value 25 } | Should -Throw -ErrorId 'OperatorNotSpecified,Microsoft.PowerShell.Commands.WhereObjectCommand' + } + + It "-CLike:`$false with -Value should throw an error requiring an operator" { + # Similar behavior for -CLike:$false with -Value + { $testData | Where-Object Name -CLike:$false -Value 'A*' } | Should -Throw -ErrorId 'OperatorNotSpecified,Microsoft.PowerShell.Commands.WhereObjectCommand' + } + } } From fb6403d4da3e9379f7350c2d6b1d22d732698e76 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Fri, 21 Nov 2025 21:12:28 +0900 Subject: [PATCH 013/112] Respect -Repeat/-MtuSize/-Traceroute:$false in Test-Connection (#26479) --- .../management/TestConnectionCommand.cs | 44 +++++++++---------- .../Test-Connection.Tests.ps1 | 29 ++++++++++++ 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs index 17c9ab9a5d..2a4a453f8f 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/TestConnectionCommand.cs @@ -242,14 +242,13 @@ namespace Microsoft.PowerShell.Commands /// protected override void BeginProcessing() { - switch (ParameterSetName) + if (Repeat) { - case RepeatPingParameterSet: - Count = int.MaxValue; - break; - case TcpPortParameterSet: - SetCountForTcpTest(); - break; + Count = int.MaxValue; + } + else if (ParameterSetName == TcpPortParameterSet) + { + SetCountForTcpTest(); } } @@ -265,21 +264,22 @@ namespace Microsoft.PowerShell.Commands foreach (var targetName in TargetName) { - switch (ParameterSetName) + if (MtuSize) { - case DefaultPingParameterSet: - case RepeatPingParameterSet: - ProcessPing(targetName); - break; - case MtuSizeDetectParameterSet: - ProcessMTUSize(targetName); - break; - case TraceRouteParameterSet: - ProcessTraceroute(targetName); - break; - case TcpPortParameterSet: - ProcessConnectionByTCPPort(targetName); - break; + ProcessMTUSize(targetName); + } + else if (Traceroute) + { + ProcessTraceroute(targetName); + } + else if (ParameterSetName == TcpPortParameterSet) + { + ProcessConnectionByTCPPort(targetName); + } + else + { + // None of the switch parameters are true: handle default ping or -Repeat + ProcessPing(targetName); } } } @@ -298,7 +298,7 @@ namespace Microsoft.PowerShell.Commands private void SetCountForTcpTest() { - if (Repeat.IsPresent) + if (Repeat) { Count = int.MaxValue; } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1 index 2138a44758..ad01b461b5 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Test-Connection.Tests.ps1 @@ -258,6 +258,15 @@ Describe "Test-Connection" -tags "CI", "RequireSudoOnUnix" { $pingResults.Where( { $_.Status -eq 'Success' }, 'Default', 1 ).BufferSize | Should -Be 32 } } + + It "Repeat with explicit false should behave like default ping" { + # -Repeat:$false should behave the same as not specifying -Repeat + $pingResults = Test-Connection $targetAddress -Repeat:$false + + # Should get exactly 4 pings (the default Count value) + $pingResults.Count | Should -Be 4 + $pingResults[0].Address | Should -BeExactly $targetAddress + } } Context "MTUSizeDetect" { @@ -291,6 +300,16 @@ Describe "Test-Connection" -tags "CI", "RequireSudoOnUnix" { $result | Should -BeOfType Int32 $result | Should -BeGreaterThan 0 } + + It "MtuSize with explicit false should behave like default ping" { + # -MtuSize:$false should behave the same as not specifying -MtuSize (default ping) + $pingResults = Test-Connection $targetAddress -MtuSize:$false + + # Should return PingStatus, not PingMtuStatus + $pingResults[0] | Should -BeOfType Microsoft.PowerShell.Commands.TestConnectionCommand+PingStatus + # Should get 4 pings (the default Count value) + $pingResults.Count | Should -Be 4 + } } Context "TraceRoute" { @@ -332,6 +351,16 @@ Describe "Test-Connection" -tags "CI", "RequireSudoOnUnix" { $results.Hostname | Should -Not -BeNullOrEmpty } + + It "Traceroute with explicit false should behave like default ping" { + # -Traceroute:$false should behave the same as not specifying -Traceroute (default ping) + $pingResults = Test-Connection $targetAddress -Traceroute:$false + + # Should return PingStatus, not TraceStatus + $pingResults[0] | Should -BeOfType Microsoft.PowerShell.Commands.TestConnectionCommand+PingStatus + # Should get 4 pings (the default Count value) + $pingResults.Count | Should -Be 4 + } } } From e81985d8f2b32f4bad0684749d29dbe3d5c074e1 Mon Sep 17 00:00:00 2001 From: yotsuda Date: Sat, 22 Nov 2025 02:50:32 +0900 Subject: [PATCH 014/112] Respect -UseWindowsPowerShell:$false in New-PSSession (#26469) --- .../remoting/commands/newrunspacecommand.cs | 11 ++++- .../engine/Remoting/PSSession.Tests.ps1 | 45 +++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs b/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs index 8986def5a6..60ca0c35e5 100644 --- a/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs +++ b/src/System.Management.Automation/engine/remoting/commands/newrunspacecommand.cs @@ -267,7 +267,16 @@ namespace Microsoft.PowerShell.Commands case NewPSSessionCommand.UseWindowsPowerShellParameterSet: { - remoteRunspaces = CreateRunspacesForUseWindowsPowerShellParameterSet(); + if (UseWindowsPowerShell) + { + remoteRunspaces = CreateRunspacesForUseWindowsPowerShellParameterSet(); + } + else + { + // When -UseWindowsPowerShell:$false is explicitly specified, + // fall back to the default ComputerName parameter set behavior + goto case NewPSSessionCommand.ComputerNameParameterSet; + } } break; diff --git a/test/powershell/engine/Remoting/PSSession.Tests.ps1 b/test/powershell/engine/Remoting/PSSession.Tests.ps1 index 689e587ddb..317ed4af73 100644 --- a/test/powershell/engine/Remoting/PSSession.Tests.ps1 +++ b/test/powershell/engine/Remoting/PSSession.Tests.ps1 @@ -91,3 +91,48 @@ Describe "SkipCACheck and SkipCNCheck PSSession options are required for New-PSS $er.Exception.ErrorCode | Should -Be $expectedErrorCode } } + +Describe "New-PSSession -UseWindowsPowerShell switch parameter" -Tag "CI" { + + BeforeAll { + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + + if (-not $IsWindows) { + $PSDefaultParameterValues['it:skip'] = $true + } + } + + AfterAll { + $global:PSDefaultParameterValues = $originalDefaultParameterValues + } + + It "Should respect explicit -UseWindowsPowerShell:`$false parameter value" { + # Test 1: -UseWindowsPowerShell:$true should create a Windows PowerShell 5.1 session + $sessionWithTrue = $null + try { + { $script:sessionWithTrue = New-PSSession -UseWindowsPowerShell:$true } | Should -Not -Throw + $script:sessionWithTrue | Should -Not -BeNullOrEmpty + + # Verify it's Windows PowerShell 5.1 + $version = Invoke-Command -Session $script:sessionWithTrue -ScriptBlock { $PSVersionTable.PSVersion } + $version.Major | Should -Be 5 + $version.Minor | Should -Be 1 + } + finally { + if ($script:sessionWithTrue) { Remove-PSSession $script:sessionWithTrue -ErrorAction SilentlyContinue } + } + + # Test 2: -UseWindowsPowerShell:$false should use WSMan transport (not Process) + $sessionWithFalse = $null + try { + { $script:sessionWithFalse = New-PSSession -UseWindowsPowerShell:$false } | Should -Not -Throw + $script:sessionWithFalse | Should -Not -BeNullOrEmpty + + # Transport should be WSMan, not Process + $script:sessionWithFalse.Transport | Should -Be 'WSMan' + } + finally { + if ($script:sessionWithFalse) { Remove-PSSession $script:sessionWithFalse -ErrorAction SilentlyContinue } + } + } +} From bb712def99cc6b2c8ac5a7e3a4d3e84ad7c3845f Mon Sep 17 00:00:00 2001 From: yotsuda Date: Sat, 22 Nov 2025 02:56:07 +0900 Subject: [PATCH 015/112] Respect -Qualifier/-NoQualifier/-Leaf/-IsAbsolute:$false in Split-Path (#26474) --- .../commands/management/ParsePathCommand.cs | 222 +++++++++--------- .../Split-Path.Tests.ps1 | 30 ++- 2 files changed, 133 insertions(+), 119 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/ParsePathCommand.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/ParsePathCommand.cs index b6609f8b69..ca616301eb 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/ParsePathCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/ParsePathCommand.cs @@ -289,137 +289,125 @@ namespace Microsoft.PowerShell.Commands { string result = null; - switch (ParameterSetName) + // Check switch parameters in order of specificity + if (IsAbsolute) { - case isAbsoluteSet: - string ignored; - bool isPathAbsolute = - SessionState.Path.IsPSAbsolute(pathsToParse[index], out ignored); + string ignored; + bool isPathAbsolute = + SessionState.Path.IsPSAbsolute(pathsToParse[index], out ignored); - WriteObject(isPathAbsolute); + WriteObject(isPathAbsolute); + continue; + } + else if (Qualifier) + { + int separatorIndex = pathsToParse[index].IndexOf(':'); + + if (separatorIndex < 0) + { + FormatException e = + new( + StringUtil.Format(NavigationResources.ParsePathFormatError, pathsToParse[index])); + WriteError( + new ErrorRecord( + e, + "ParsePathFormatError", // RENAME + ErrorCategory.InvalidArgument, + pathsToParse[index])); continue; + } + else + { + // Check to see if it is provider or drive qualified - case qualifierSet: - int separatorIndex = pathsToParse[index].IndexOf(':'); - - if (separatorIndex < 0) + if (SessionState.Path.IsProviderQualified(pathsToParse[index])) { - FormatException e = - new( - StringUtil.Format(NavigationResources.ParsePathFormatError, pathsToParse[index])); - WriteError( - new ErrorRecord( - e, - "ParsePathFormatError", // RENAME - ErrorCategory.InvalidArgument, - pathsToParse[index])); - continue; + // The plus 2 is for the length of the provider separator + // which is "::" + + result = + pathsToParse[index].Substring( + 0, + separatorIndex + 2); } else - { - // Check to see if it is provider or drive qualified - - if (SessionState.Path.IsProviderQualified(pathsToParse[index])) - { - // The plus 2 is for the length of the provider separator - // which is "::" - - result = - pathsToParse[index].Substring( - 0, - separatorIndex + 2); - } - else - { - result = - pathsToParse[index].Substring( - 0, - separatorIndex + 1); - } - } - - break; - - case parentSet: - case literalPathSet: - try { result = - SessionState.Path.ParseParent( - pathsToParse[index], - string.Empty, - CmdletProviderContext, - true); + pathsToParse[index].Substring( + 0, + separatorIndex + 1); } - catch (PSNotSupportedException) + } + } + else if (Leaf || LeafBase || Extension) + { + try + { + result = + SessionState.Path.ParseChildName( + pathsToParse[index], + CmdletProviderContext, + true); + if (LeafBase) { - // Since getting the parent path is not supported, - // the provider must be a container, item, or drive - // provider. Since the paths for these types of - // providers can't be split, asking for the parent - // is asking for an empty string. - result = string.Empty; + result = System.IO.Path.GetFileNameWithoutExtension(result); } - - break; - - case leafSet: - case leafBaseSet: - case extensionSet: - try + else if (Extension) { - // default handles leafSet - result = - SessionState.Path.ParseChildName( - pathsToParse[index], - CmdletProviderContext, - true); - if (LeafBase) - { - result = System.IO.Path.GetFileNameWithoutExtension(result); - } - else if (Extension) - { - result = System.IO.Path.GetExtension(result); - } + result = System.IO.Path.GetExtension(result); } - catch (PSNotSupportedException) - { - // Since getting the leaf part of a path is not supported, - // the provider must be a container, item, or drive - // provider. Since the paths for these types of - // providers can't be split, asking for the leaf - // is asking for the specified path back. - result = pathsToParse[index]; - } - catch (DriveNotFoundException driveNotFound) - { - WriteError( - new ErrorRecord( - driveNotFound.ErrorRecord, - driveNotFound)); - continue; - } - catch (ProviderNotFoundException providerNotFound) - { - WriteError( - new ErrorRecord( - providerNotFound.ErrorRecord, - providerNotFound)); - continue; - } - - break; - - case noQualifierSet: - result = RemoveQualifier(pathsToParse[index]); - break; - - default: - Dbg.Diagnostics.Assert( - false, - "Only a known parameter set should be called"); - break; + } + catch (PSNotSupportedException) + { + // Since getting the leaf part of a path is not supported, + // the provider must be a container, item, or drive + // provider. Since the paths for these types of + // providers can't be split, asking for the leaf + // is asking for the specified path back. + result = pathsToParse[index]; + } + catch (DriveNotFoundException driveNotFound) + { + WriteError( + new ErrorRecord( + driveNotFound.ErrorRecord, + driveNotFound)); + continue; + } + catch (ProviderNotFoundException providerNotFound) + { + WriteError( + new ErrorRecord( + providerNotFound.ErrorRecord, + providerNotFound)); + continue; + } + } + else if (NoQualifier) + { + result = RemoveQualifier(pathsToParse[index]); + } + else + { + // None of the switch parameters are true: default to -Parent behavior + try + { + result = + SessionState.Path.ParseParent( + pathsToParse[index], + string.Empty, + CmdletProviderContext, + true); + } + catch (PSNotSupportedException) + { + // Since getting the parent path is not supported, + // the provider must be a container, item, or drive + // provider. Since the paths for these types of + // providers can't be split, asking for the parent + // is asking for an empty string. + result = string.Empty; + } } if (result != null) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Split-Path.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Split-Path.Tests.ps1 index b9537a4a96..e34126b101 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Split-Path.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Split-Path.Tests.ps1 @@ -96,7 +96,33 @@ Describe "Split-Path" -Tags "CI" { Split-Path -Parent "\\server1\share1" | Should -BeExactly "${dirSep}${dirSep}server1" } - It 'Does not split a drive leter'{ - Split-Path -Path 'C:\' | Should -BeNullOrEmpty + It 'Does not split a drive letter' { + Split-Path -Path 'C:\' | Should -BeNullOrEmpty + } + + It "Should handle explicit -Qualifier:`$false parameter value correctly" { + # When -Qualifier:$false is specified, it should behave like -Parent (default) + # For env:PATH, the parent is empty string + $result = Split-Path -Path "env:PATH" -Qualifier:$false + $result | Should -BeNullOrEmpty + } + + It "Should handle explicit -NoQualifier:`$false parameter value correctly" { + # When -NoQualifier:$false is specified, it should behave like -Parent (default) + # For env:PATH with no qualifier, we expect empty string (parent of PATH in env drive) + $result = Split-Path -Path "env:PATH" -NoQualifier:$false + $result | Should -BeNullOrEmpty + } + + It "Should handle explicit -Leaf:`$false parameter value correctly" { + # When -Leaf:$false is specified, it should behave like -Parent (default) + $dirSep = [string]([System.IO.Path]::DirectorySeparatorChar) + Split-Path -Path "/usr/bin" -Leaf:$false | Should -BeExactly "${dirSep}usr" + } + + It "Should handle explicit -IsAbsolute:`$false parameter value correctly" { + # When -IsAbsolute:$false is specified, it should behave like -Parent (default) + $dirSep = [string]([System.IO.Path]::DirectorySeparatorChar) + Split-Path -Path "fs:/usr/bin" -IsAbsolute:$false | Should -BeExactly "fs:${dirSep}usr" } } From e2075afafe25dd0c2d090ab4032dbacbbcd32b0c Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Fri, 21 Nov 2025 16:08:16 -0600 Subject: [PATCH 016/112] Explain the parameter `-UseNuGetOrg` in build documentation (#26507) --- docs/building/linux.md | 2 ++ docs/building/macos.md | 2 ++ docs/building/windows-core.md | 2 ++ 3 files changed, 6 insertions(+) diff --git a/docs/building/linux.md b/docs/building/linux.md index bd7f9aadf6..6ccf12073e 100644 --- a/docs/building/linux.md +++ b/docs/building/linux.md @@ -64,6 +64,8 @@ Import-Module ./build.psm1 Start-PSBuild -UseNuGetOrg ``` +> The PowerShell project by default references packages from the private Azure Artifacts feed, which requires authentication. The `-UseNuGetOrg` flag reconfigures the build to use the public NuGet.org feed instead. + Congratulations! If everything went right, PowerShell is now built. The `Start-PSBuild` script will output the location of the executable: diff --git a/docs/building/macos.md b/docs/building/macos.md index 4f15e3fb54..7fd767ffa1 100644 --- a/docs/building/macos.md +++ b/docs/building/macos.md @@ -37,3 +37,5 @@ We cannot do this for you in the build module due to #[847][]. Start a PowerShell session by running `pwsh`, and then use `Start-PSBuild -UseNuGetOrg` from the module. After building, PowerShell will be at `./src/powershell-unix/bin/Debug/net6.0/osx-x64/publish/pwsh`. + +> The PowerShell project by default references packages from the private Azure Artifacts feed, which requires authentication. The `-UseNuGetOrg` flag reconfigures the build to use the public NuGet.org feed instead. diff --git a/docs/building/windows-core.md b/docs/building/windows-core.md index 48e5c95163..20774695fc 100644 --- a/docs/building/windows-core.md +++ b/docs/building/windows-core.md @@ -61,6 +61,8 @@ Import-Module ./build.psm1 Start-PSBuild -Clean -PSModuleRestore -UseNuGetOrg ``` +> The PowerShell project by default references packages from the private Azure Artifacts feed, which requires authentication. The `-UseNuGetOrg` flag reconfigures the build to use the public NuGet.org feed instead. + Congratulations! If everything went right, PowerShell is now built and executable as `./src/powershell-win-core/bin/Debug/net6.0/win7-x64/publish/pwsh.exe`. This location is of the form `./[project]/bin/[configuration]/[framework]/[rid]/publish/[binary name]`, From f4476c5226d0610373a5989c810dd0ee23da3607 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 21 Nov 2025 14:36:14 -0800 Subject: [PATCH 017/112] Bump actions/checkout from 5 to 6 (#26505) --- .github/workflows/analyze-reusable.yml | 2 +- .github/workflows/copilot-setup-steps.yml | 2 +- .github/workflows/dependency-review.yml | 2 +- .github/workflows/labels.yml | 2 +- .github/workflows/linux-ci.yml | 18 +++++++++--------- .github/workflows/macos-ci.yml | 14 +++++++------- .github/workflows/scorecards.yml | 2 +- .github/workflows/verify-markdown-links.yml | 2 +- .github/workflows/windows-ci.yml | 12 ++++++------ .../workflows/windows-packaging-reusable.yml | 2 +- .github/workflows/xunit-tests.yml | 2 +- 11 files changed, 30 insertions(+), 30 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 9ed85e11e2..c1d812e533 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: fetch-depth: '0' diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index ac57bd87b5..be2dd55df7 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -22,7 +22,7 @@ jobs: # You can define any steps you want, and they will run before the agent starts. # If you do not check out your code, Copilot will do this for you. steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: fetch-depth: 1000 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index dadc6fd437..dbef2e1c8b 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -17,6 +17,6 @@ jobs: runs-on: ubuntu-latest steps: - name: 'Checkout Repository' - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: 'Dependency Review' uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index cd0a1d3172..61b5eebb88 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Check out the repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Verify PR has label starting with 'cl-' id: verify-labels diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index 030139b6c3..49d1c0a055 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -57,7 +57,7 @@ jobs: packagingChanged: ${{ steps.filter.outputs.packagingChanged }} steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: persist-credentials: false @@ -76,7 +76,7 @@ jobs: contents: read steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Check for merge conflict markers uses: "./.github/actions/infrastructure/merge-conflict-checker" @@ -88,7 +88,7 @@ jobs: if: ${{ needs.changes.outputs.source == 'true' || needs.changes.outputs.buildModuleChanged == 'true' }} steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 @@ -103,7 +103,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Linux Unelevated CI @@ -121,7 +121,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Linux Elevated CI @@ -139,7 +139,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Linux Unelevated Others @@ -157,7 +157,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Linux Elevated Others @@ -181,7 +181,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1 @@ -255,7 +255,7 @@ jobs: runs-on: ubuntu-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Linux Packaging diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 721077a6b7..60596c75ed 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -56,7 +56,7 @@ jobs: buildModuleChanged: ${{ steps.filter.outputs.buildModuleChanged }} steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Change Detection id: filter @@ -71,7 +71,7 @@ jobs: if: ${{ needs.changes.outputs.source == 'true' || needs.changes.outputs.buildModuleChanged == 'true' }} steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Build @@ -85,7 +85,7 @@ jobs: runs-on: macos-15-large steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: macOS Unelevated CI @@ -103,7 +103,7 @@ jobs: runs-on: macos-15-large steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: macOS Elevated CI @@ -121,7 +121,7 @@ jobs: runs-on: macos-15-large steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: macOS Unelevated Others @@ -139,7 +139,7 @@ jobs: runs-on: macos-15-large steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: macOS Elevated Others @@ -166,7 +166,7 @@ jobs: - macos-15-large steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - uses: actions/setup-dotnet@v5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 3f5951951e..8df06cd9f1 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -32,7 +32,7 @@ jobs: steps: - name: "Checkout code" - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false diff --git a/.github/workflows/verify-markdown-links.yml b/.github/workflows/verify-markdown-links.yml index b26539eb0c..df37ba3c51 100644 --- a/.github/workflows/verify-markdown-links.yml +++ b/.github/workflows/verify-markdown-links.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Verify markdown links id: verify diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 4f774fcdc9..92bbf2f4c9 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -60,7 +60,7 @@ jobs: packagingChanged: ${{ steps.filter.outputs.packagingChanged }} steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Change Detection id: filter @@ -75,7 +75,7 @@ jobs: runs-on: windows-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Build @@ -89,7 +89,7 @@ jobs: runs-on: windows-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Windows Unelevated CI @@ -107,7 +107,7 @@ jobs: runs-on: windows-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Windows Elevated CI @@ -125,7 +125,7 @@ jobs: runs-on: windows-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Windows Unelevated Others @@ -143,7 +143,7 @@ jobs: runs-on: windows-latest steps: - name: checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 - name: Windows Elevated Others diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index 1f03aaf594..f3a46cae90 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 diff --git a/.github/workflows/xunit-tests.yml b/.github/workflows/xunit-tests.yml index efd05b7604..940e1cb839 100644 --- a/.github/workflows/xunit-tests.yml +++ b/.github/workflows/xunit-tests.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ inputs.runner_os }} steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 with: fetch-depth: 1000 From 8948c417c0ceb0a4f954b51599294ac06881eb5b Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Sat, 22 Nov 2025 16:38:35 +0900 Subject: [PATCH 018/112] Make Export-Csv -Append and -NoHeader mutually exclusive (#26472) --- .../commands/utility/CsvCommands.cs | 8 ++++++++ .../resources/CsvCommandStrings.resx | 3 +++ .../Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 | 4 ++++ 3 files changed, 15 insertions(+) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs index fa49c33d2a..e84a79b99b 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs @@ -270,6 +270,14 @@ namespace Microsoft.PowerShell.Commands this.ThrowTerminatingError(errorRecord); } + // Validate that Append and NoHeader are not specified together. + if (Append && NoHeader) + { + InvalidOperationException exception = new(CsvCommandStrings.CannotSpecifyAppendAndNoHeader); + ErrorRecord errorRecord = new(exception, "CannotSpecifyBothAppendAndNoHeader", ErrorCategory.InvalidData, null); + this.ThrowTerminatingError(errorRecord); + } + _shouldProcess = ShouldProcess(Path); if (!_shouldProcess) { diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx index 0eff0d6f84..8c5ded1346 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx @@ -157,4 +157,7 @@ EOF is reached. + + You must specify either the -Append or -NoHeader parameters, but not both. + diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 index db5c6aff9e..ea05d66d39 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 @@ -187,6 +187,10 @@ Describe "Export-Csv" -Tags "CI" { $results[1].PSObject.properties.Name | Should -Not -Contain 'third' } + It "Should throw when -Append and -NoHeader are specified together" { + { $P1 | Export-Csv -Path $testCsv -Append -NoHeader -ErrorAction Stop } | Should -Throw -ErrorId "CannotSpecifyBothAppendAndNoHeader,Microsoft.PowerShell.Commands.ExportCsvCommand" + } + It "First line should be #TYPE if -IncludeTypeInformation used and pstypenames object property is empty" { $object = [PSCustomObject]@{first = 1} $pstypenames = $object.pstypenames | ForEach-Object -Process {$_} From 70a6a80f6b88e5488b364fb077fd238b9085f6f1 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Sat, 22 Nov 2025 22:28:27 +0900 Subject: [PATCH 019/112] Add -Extension parameter to Join-Path cmdlet (#26482) --- .../commands/management/CombinePathCommand.cs | 21 ++++ .../Join-Path.Tests.ps1 | 98 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs b/src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs index 71ef5df8f0..79b1c862bf 100644 --- a/src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Management/commands/management/CombinePathCommand.cs @@ -51,6 +51,21 @@ namespace Microsoft.PowerShell.Commands [Parameter] public SwitchParameter Resolve { get; set; } + /// + /// Gets or sets the extension to use for the resulting path. + /// If not specified, the original extension (if any) is preserved. + /// + /// Behavior: + /// - If the path has an existing extension, it will be replaced with the specified extension. + /// - If the path does not have an extension, the specified extension will be added. + /// - If an empty string is provided, any existing extension will be removed. + /// - A leading dot in the extension is optional; if omitted, one will be added automatically. + /// + /// + [Parameter(ValueFromPipelineByPropertyName = true)] + [ValidateNotNull] + public string Extension { get; set; } + #endregion Parameters #region Command code @@ -128,6 +143,12 @@ namespace Microsoft.PowerShell.Commands continue; } + // If Extension parameter is present it is not null due to [ValidateNotNull]. + if (Extension is not null) + { + joinedPath = System.IO.Path.ChangeExtension(joinedPath, Extension.Length == 0 ? null : Extension); + } + if (Resolve) { // Resolve the paths. The default API (GetResolvedPSPathFromPSPath) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1 index a66bddd5e4..9b0898fee9 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Join-Path.Tests.ps1 @@ -84,4 +84,102 @@ Describe "Join-Path cmdlet tests" -Tags "CI" { $result = Join-Path -Path $Path -ChildPath $ChildPath $result | Should -BeExactly $ExpectedResult } + It "should handle extension parameter: " -TestCases @( + @{ + TestName = "change extension" + ChildPath = "file.txt" + Extension = ".log" + ExpectedChildPath = "file.log" + } + @{ + TestName = "add extension to file without extension" + ChildPath = "file" + Extension = ".txt" + ExpectedChildPath = "file.txt" + } + @{ + TestName = "extension without leading dot" + ChildPath = "file.txt" + Extension = "log" + ExpectedChildPath = "file.log" + } + @{ + TestName = "double extension with dot" + ChildPath = "file.txt" + Extension = ".tar.gz" + ExpectedChildPath = "file.tar.gz" + } + @{ + TestName = "double extension without dot" + ChildPath = "file.txt" + Extension = "tar.gz" + ExpectedChildPath = "file.tar.gz" + } + @{ + TestName = "remove extension with empty string" + ChildPath = "file.txt" + Extension = "" + ExpectedChildPath = "file" + } + @{ + TestName = "preserve dots in base name when removing extension with empty string" + ChildPath = "file...txt" + Extension = "" + ExpectedChildPath = "file.." + } + @{ + TestName = "replace only the last extension for files with multiple dots" + ChildPath = "file.backup.txt" + Extension = ".log" + ExpectedChildPath = "file.backup.log" + } + @{ + TestName = "preserve dots in base name when changing extension" + ChildPath = "file...txt" + Extension = ".md" + ExpectedChildPath = "file...md" + } + @{ + TestName = "add extension to directory-like path" + ChildPath = "subfolder" + Extension = ".log" + ExpectedChildPath = "subfolder.log" + } + ) { + param($TestName, $ChildPath, $Extension, $ExpectedChildPath) + $result = Join-Path -Path "folder" -ChildPath $ChildPath -Extension $Extension + $result | Should -BeExactly "folder${SepChar}${ExpectedChildPath}" + } + It "should handle extension parameter with multiple child path segments: " -TestCases @( + @{ + TestName = "change extension when joining multiple child path segments" + ChildPaths = @("subfolder", "file.txt") + Extension = ".log" + ExpectedPath = "folder${SepChar}subfolder${SepChar}file.log" + } + ) { + param($TestName, $ChildPaths, $Extension, $ExpectedPath) + $result = Join-Path -Path "folder" -ChildPath $ChildPaths -Extension $Extension + $result | Should -BeExactly $ExpectedPath + } + It "should change extension for multiple paths" { + $result = Join-Path -Path "folder1", "folder2" -ChildPath "file.txt" -Extension ".log" + $result.Count | Should -Be 2 + $result[0] | Should -BeExactly "folder1${SepChar}file.log" + $result[1] | Should -BeExactly "folder2${SepChar}file.log" + } + It "should resolve path when -Extension changes to existing file" { + New-Item -Path TestDrive:\testfile.log -ItemType File -Force | Out-Null + $result = Join-Path -Path TestDrive: -ChildPath "testfile.txt" -Extension ".log" -Resolve + $result | Should -BeLike "*testfile.log" + } + It "should throw error when -Extension changes to non-existing file with -Resolve" { + { Join-Path -Path TestDrive: -ChildPath "testfile.txt" -Extension ".nonexistent" -Resolve -ErrorAction Stop; Throw "Previous statement unexpectedly succeeded..." } | + Should -Throw -ErrorId "PathNotFound,Microsoft.PowerShell.Commands.JoinPathCommand" + } + It "should accept Extension from pipeline by property name" { + $obj = [PSCustomObject]@{ Path = "folder"; ChildPath = "file.txt"; Extension = ".log" } + $result = $obj | Join-Path + $result | Should -BeExactly "folder${SepChar}file.log" + } } From 9ee3c61c26f63070ba72b7303384db7956137aa3 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 24 Nov 2025 12:05:03 -0800 Subject: [PATCH 020/112] Close pipe client handles after creating the child ssh process (#26491) --- .../remoting/common/RunspaceConnectionInfo.cs | 34 +++++++++---------- .../fanin/OutOfProcTransportManager.cs | 8 ++--- .../Remoting/SSHRemotingCmdlets.Tests.ps1 | 21 ++++++++++++ 3 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs index b6913e0cc1..242bde2ac5 100644 --- a/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs +++ b/src/System.Management.Automation/engine/remoting/common/RunspaceConnectionInfo.cs @@ -2211,11 +2211,11 @@ namespace System.Management.Automation.Runspaces CommandTypes.Application, SearchResolutionOptions.None, CommandOrigin.Internal, - context) as ApplicationInfo; + context); - if (cmdInfo != null) + if (cmdInfo is ApplicationInfo appInfo) { - filePath = cmdInfo.Path; + filePath = appInfo.Path; } } else @@ -2273,13 +2273,13 @@ namespace System.Management.Automation.Runspaces // Subsystem powershell /usr/local/bin/pwsh -SSHServerMode -NoLogo -NoProfile // codeql[cs/microsoft/command-line-injection-shell-execution] - This is expected Poweshell behavior where user inputted paths are supported for the context of this method. The user assumes trust for the file path specified, so any file executed in the runspace would be in the user's local system/process or a system they have access to in which case restricted remoting security guidelines should be used. - System.Diagnostics.ProcessStartInfo startInfo = new System.Diagnostics.ProcessStartInfo(filePath); + ProcessStartInfo startInfo = new(filePath); // pass "-i identity_file" command line argument to ssh if KeyFilePath is set // if KeyFilePath is not set, then ssh will use IdentityFile / IdentityAgent from ssh_config if defined else none by default if (!string.IsNullOrEmpty(this.KeyFilePath)) { - if (!System.IO.File.Exists(this.KeyFilePath)) + if (!File.Exists(this.KeyFilePath)) { throw new FileNotFoundException( StringUtil.Format(RemotingErrorIdStrings.KeyFileNotFound, this.KeyFilePath)); @@ -2326,7 +2326,7 @@ namespace System.Management.Automation.Runspaces // note that ssh expects IPv6 addresses to not be enclosed in square brackets so trim them if present startInfo.ArgumentList.Add(string.Create(CultureInfo.InvariantCulture, $@"-s {this.ComputerName.TrimStart('[').TrimEnd(']')} {this.Subsystem}")); - startInfo.WorkingDirectory = System.IO.Path.GetDirectoryName(filePath); + startInfo.WorkingDirectory = Path.GetDirectoryName(filePath); startInfo.CreateNoWindow = true; startInfo.UseShellExecute = false; @@ -2580,7 +2580,7 @@ namespace System.Management.Automation.Runspaces // Allocate the unmanaged array to hold each string pointer. // It needs to have an extra element to null terminate the array. arrPtr = (byte**)Marshal.AllocHGlobal(sizeof(IntPtr) * arrLength); - System.Diagnostics.Debug.Assert(arrPtr != null, "Invalid array ptr"); + Debug.Assert(arrPtr != null, "Invalid array ptr"); // Zero the memory so that if any of the individual string allocations fails, // we can loop through the array to free any that succeeded. @@ -2597,7 +2597,7 @@ namespace System.Management.Automation.Runspaces byte[] byteArr = System.Text.Encoding.UTF8.GetBytes(arr[i]); arrPtr[i] = (byte*)Marshal.AllocHGlobal(byteArr.Length + 1); // +1 for null termination - System.Diagnostics.Debug.Assert(arrPtr[i] != null, "Invalid array ptr"); + Debug.Assert(arrPtr[i] != null, "Invalid array ptr"); Marshal.Copy(byteArr, 0, (IntPtr)arrPtr[i], byteArr.Length); // copy over the data from the managed byte array arrPtr[i][byteArr.Length] = (byte)'\0'; // null terminate @@ -2641,13 +2641,13 @@ namespace System.Management.Automation.Runspaces /// P-Invoking native APIs. /// private static int StartSSHProcessImpl( - System.Diagnostics.ProcessStartInfo startInfo, + ProcessStartInfo startInfo, out StreamWriter stdInWriterVar, out StreamReader stdOutReaderVar, out StreamReader stdErrReaderVar) { Exception ex = null; - System.Diagnostics.Process sshProcess = null; + Process sshProcess = null; // // These std pipe handles are bound to managed Reader/Writer objects and returned to the transport // manager object, which uses them for PSRP communication. The lifetime of these handles are then @@ -2668,7 +2668,7 @@ namespace System.Management.Automation.Runspaces catch (InvalidOperationException e) { ex = e; } catch (ArgumentException e) { ex = e; } catch (FileNotFoundException e) { ex = e; } - catch (System.ComponentModel.Win32Exception e) { ex = e; } + catch (Win32Exception e) { ex = e; } if ((ex != null) || (sshProcess == null) || @@ -2693,9 +2693,9 @@ namespace System.Management.Automation.Runspaces { if (stdInWriterVar != null) { stdInWriterVar.Dispose(); } else { stdInPipeServer.Dispose(); } - if (stdOutReaderVar != null) { stdInWriterVar.Dispose(); } else { stdOutPipeServer.Dispose(); } + if (stdOutReaderVar != null) { stdOutReaderVar.Dispose(); } else { stdOutPipeServer.Dispose(); } - if (stdErrReaderVar != null) { stdInWriterVar.Dispose(); } else { stdErrPipeServer.Dispose(); } + if (stdErrReaderVar != null) { stdErrReaderVar.Dispose(); } else { stdErrPipeServer.Dispose(); } throw; } @@ -2705,7 +2705,7 @@ namespace System.Management.Automation.Runspaces private static void KillSSHProcessImpl(int pid) { - using (var sshProcess = System.Diagnostics.Process.GetProcessById(pid)) + using (var sshProcess = Process.GetProcessById(pid)) { if ((sshProcess != null) && (sshProcess.Handle != IntPtr.Zero) && !sshProcess.HasExited) { @@ -2736,7 +2736,7 @@ namespace System.Management.Automation.Runspaces SafeFileHandle stdInPipeClient = null; SafeFileHandle stdOutPipeClient = null; SafeFileHandle stdErrPipeClient = null; - string randomName = System.IO.Path.GetFileNameWithoutExtension(System.IO.Path.GetRandomFileName()); + string randomName = Path.GetFileNameWithoutExtension(Path.GetRandomFileName()); try { @@ -2829,16 +2829,14 @@ namespace System.Management.Automation.Runspaces catch (Exception) { stdInPipeServer?.Dispose(); - stdInPipeClient?.Dispose(); stdOutPipeServer?.Dispose(); - stdOutPipeClient?.Dispose(); stdErrPipeServer?.Dispose(); - stdErrPipeClient?.Dispose(); throw; } finally { + lpStartupInfo.Dispose(); lpProcessInformation.Dispose(); } } diff --git a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs index d9532c8691..47ff6270db 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/OutOfProcTransportManager.cs @@ -1733,7 +1733,7 @@ namespace System.Management.Automation.Remoting.Client bool sshTerminated = false; try { - using (var sshProcess = System.Diagnostics.Process.GetProcessById(_sshProcessId)) + using (var sshProcess = Process.GetProcessById(_sshProcessId)) { sshTerminated = sshProcess == null || sshProcess.Handle == IntPtr.Zero || sshProcess.HasExited; } @@ -1847,7 +1847,7 @@ namespace System.Management.Automation.Remoting.Client // Messages in error stream from ssh are unreliable, and may just be warnings or // banner text. // So just report the messages but don't act on them. - System.Console.WriteLine(error); + Console.WriteLine(error); } catch (IOException) { } @@ -1907,10 +1907,10 @@ namespace System.Management.Automation.Remoting.Client break; } - if (data.StartsWith(System.Management.Automation.Remoting.Server.FormattedErrorTextWriter.ErrorPrefix, StringComparison.OrdinalIgnoreCase)) + if (data.StartsWith(OutOfProcessTextWriter.ErrorPrefix, StringComparison.OrdinalIgnoreCase)) { // Error message from the server. - string errorData = data.Substring(System.Management.Automation.Remoting.Server.FormattedErrorTextWriter.ErrorPrefix.Length); + string errorData = data.Substring(OutOfProcessTextWriter.ErrorPrefix.Length); HandleErrorDataReceived(errorData); } else diff --git a/test/powershell/engine/Remoting/SSHRemotingCmdlets.Tests.ps1 b/test/powershell/engine/Remoting/SSHRemotingCmdlets.Tests.ps1 index 1568d284d0..c1090bdc18 100644 --- a/test/powershell/engine/Remoting/SSHRemotingCmdlets.Tests.ps1 +++ b/test/powershell/engine/Remoting/SSHRemotingCmdlets.Tests.ps1 @@ -70,3 +70,24 @@ Describe "SSHConnection parameter hashtable type conversions" -Tags 'Feature', ' $err.FullyQualifiedErrorId | Should -Match 'PSSessionOpenFailed' } } + +Describe "No hangs when host doesn't exist" -Tags "CI" { + $testCases = @( + @{ + Name = 'Verifies no hang for New-PSSession with non-existing host name' + ScriptBlock = { New-PSSession -HostName "test-notexist" -UserName "test" -ErrorAction Stop } + FullyQualifiedErrorId = 'PSSessionOpenFailed' + }, + @{ + Name = 'Verifies no hang for Invoke-Command with non-existing host name' + ScriptBlock = { Invoke-Command -HostName "test-notexist" -UserName "test" -ScriptBlock { 1 } -ErrorAction Stop } + FullyQualifiedErrorId = 'PSSessionStateBroken' + } + ) + + It "" -TestCases $testCases { + param ($ScriptBlock, $FullyQualifiedErrorId) + + $ScriptBlock | Should -Throw -ErrorId $FullyQualifiedErrorId -ExceptionType 'System.Management.Automation.Remoting.PSRemotingTransportException' + } +} From eb2ead439a9d2f7366ef0fb5cad26235a77c8805 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 24 Nov 2025 14:38:23 -0800 Subject: [PATCH 021/112] Fix formatting to properly handle the `Reset` VT sequences that appear in the middle of a string (#26424) --- .../FormatAndOutput/common/ComplexWriter.cs | 37 +++++++-- .../FormatAndOutput/common/ListWriter.cs | 2 +- .../engine/Formatting/PSStyle.Tests.ps1 | 81 ++++++++++++++++++- 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/System.Management.Automation/FormatAndOutput/common/ComplexWriter.cs b/src/System.Management.Automation/FormatAndOutput/common/ComplexWriter.cs index 61a3f962da..a69ad41c96 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/ComplexWriter.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/ComplexWriter.cs @@ -9,7 +9,6 @@ using System.Globalization; using System.Management.Automation; using System.Management.Automation.Internal; using System.Text; -using System.Text.RegularExpressions; namespace Microsoft.PowerShell.Commands.Internal.Format { @@ -317,6 +316,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format { internal string Word; internal string Delim; + internal bool VtResetAdded; } /// @@ -367,9 +367,19 @@ namespace Microsoft.PowerShell.Commands.Internal.Format { var vtSpan = s.AsSpan(i, len); sb.Append(vtSpan); - vtSeqs.Append(vtSpan); - wordHasVtSeqs = true; + if (vtSpan.SequenceEqual(PSStyle.Instance.Reset)) + { + // The Reset sequence will void all previous VT sequences. + vtSeqs.Clear(); + wordHasVtSeqs = false; + } + else + { + vtSeqs.Append(vtSpan); + wordHasVtSeqs = true; + } + i += len - 1; continue; } @@ -390,15 +400,18 @@ namespace Microsoft.PowerShell.Commands.Internal.Format if (delimiter is not null) { + bool vtResetAdded = false; if (wordHasVtSeqs && !sb.EndsWith(PSStyle.Instance.Reset)) { + vtResetAdded = true; sb.Append(PSStyle.Instance.Reset); } var result = new GetWordsResult() { Word = sb.ToString(), - Delim = delimiter + Delim = delimiter, + VtResetAdded = vtResetAdded }; sb.Clear().Append(vtSeqs); @@ -611,7 +624,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format if (suffix is not null) { - wordToAdd = wordToAdd.EndsWith(resetStr) + wordToAdd = word.VtResetAdded ? wordToAdd.Insert(wordToAdd.Length - resetStr.Length, suffix) : wordToAdd + suffix; } @@ -760,9 +773,19 @@ namespace Microsoft.PowerShell.Commands.Internal.Format { var vtSpan = s.AsSpan(i, len); sb.Append(vtSpan); - vtSeqs.Append(vtSpan); - hasVtSeqs = true; + if (vtSpan.SequenceEqual(PSStyle.Instance.Reset)) + { + // The Reset sequence will void all previous VT sequences. + vtSeqs.Clear(); + hasVtSeqs = false; + } + else + { + vtSeqs.Append(vtSpan); + hasVtSeqs = true; + } + i += len - 1; continue; } diff --git a/src/System.Management.Automation/FormatAndOutput/common/ListWriter.cs b/src/System.Management.Automation/FormatAndOutput/common/ListWriter.cs index 7598c6b279..988b113374 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/ListWriter.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/ListWriter.cs @@ -259,7 +259,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format _cachedBuilder.Append(headPadding).Append(str); } - if (str.Contains(ValueStringDecorated.ESC) && !str.EndsWith(reset)) + if (str.Contains(ValueStringDecorated.ESC) && !str.AsSpan().TrimEnd().EndsWith(reset, StringComparison.Ordinal)) { _cachedBuilder.Append(reset); } diff --git a/test/powershell/engine/Formatting/PSStyle.Tests.ps1 b/test/powershell/engine/Formatting/PSStyle.Tests.ps1 index d8a124a233..18baed17dd 100644 --- a/test/powershell/engine/Formatting/PSStyle.Tests.ps1 +++ b/test/powershell/engine/Formatting/PSStyle.Tests.ps1 @@ -488,7 +488,7 @@ Billy Bob… Senior DevOps … 13 $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") } - It "Word wrapping for string with escape sequences" { + It "Word wrapping for string with escape sequences (1)" { $expected = @" `e[32;1mLongDescription : `e[0m`e[33mPowerShell `e[0m `e[33mscripting `e[0m @@ -501,7 +501,46 @@ Billy Bob… Senior DevOps … 13 $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") } - It "Splitting multi-line string with escape sequences" { + It "Word wrapping for string with escape sequences (2)" { + $expected = @" +`e[32;1mLongDescription : `e[0m`e[33mPowerShell`e[0m + scripting + language +"@ + $obj = [pscustomobject] @{ LongDescription = "`e[33mPowerShell`e[0m scripting language" } + $obj | Format-List | Out-String -Width 35 | Out-File $outFile + + $text = Get-Content $outFile -Raw + $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") + } + + It "Word wrapping for string with escape sequences (3)" { + $expected = @" +`e[32;1mLongDescription : `e[0m`e[33mPowerShell`e[0m + `e[32mscripting `e[0m + `e[32mlanguage`e[0m +"@ + $obj = [pscustomobject] @{ LongDescription = "`e[33mPowerShell`e[0m `e[32mscripting language" } + $obj | Format-List | Out-String -Width 35 | Out-File $outFile + + $text = Get-Content $outFile -Raw + $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") + } + + It "Word wrapping for string with escape sequences (4)" { + $expected = @" +`e[32;1mLongDescription : `e[0m`e[33mPowerShell`e[0m + `e[32mscripting`e[0m + language +"@ + $obj = [pscustomobject] @{ LongDescription = "`e[33mPowerShell`e[0m `e[32mscripting`e[0m language" } + $obj | Format-List | Out-String -Width 35 | Out-File $outFile + + $text = Get-Content $outFile -Raw + $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") + } + + It "Splitting multi-line string with escape sequences (1)" { $expected = @" `e[32;1mb : `e[0m`e[33mPowerShell is a task automation and configuration management program from Microsoft,`e[0m `e[33mconsisting of a command-line shell and the associated scripting language`e[0m @@ -513,6 +552,30 @@ Billy Bob… Senior DevOps … 13 $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") } + It "Splitting multi-line string with escape sequences (2)" { + $expected = @" +`e[32;1mb : `e[0m`e[33mPowerShell is a task automation and configuration management program from Microsoft,`e[0m + consisting of a command-line shell and the associated scripting language +"@ + $obj = [pscustomobject] @{ b = "`e[33mPowerShell is a task automation and configuration management program from Microsoft,`e[0m`nconsisting of a command-line shell and the associated scripting language" } + $obj | Format-List | Out-File $outFile + + $text = Get-Content $outFile -Raw + $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") + } + + It "Splitting multi-line string with escape sequences (3)" { + $expected = @" +`e[32;1mb : `e[0m`e[33mPowerShell is a task automation and configuration management program from Microsoft,`e[0m + `e[32mconsisting of a command-line shell and the associated scripting language`e[0m +"@ + $obj = [pscustomobject] @{ b = "`e[33mPowerShell is a task automation and configuration management program from Microsoft,`e[0m`n`e[32mconsisting of a command-line shell and the associated scripting language" } + $obj | Format-List | Out-File $outFile + + $text = Get-Content $outFile -Raw + $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") + } + It "Wrapping long word with escape sequences" { $expected = @" `e[32;1mb : `e[0m`e[33mC:\repos\PowerShell\src\powershell-w`e[0m @@ -525,4 +588,18 @@ Billy Bob… Senior DevOps … 13 $text = Get-Content $outFile -Raw $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") } + + It "Format 'MatchInfo' object correctly" { + $expected = @" +`e[32;1mb : `e[0mmouclass `e[7mMouse`e[0m Class Driver Mouse Class Driver Kernel Manual Running OK TRUE FALSE 12,288 `e[0m + 32,768 0 C:\WINDOWS\system32\drivers\mouclass.sys 4,096 +"@ + + ## This string mimics the VT decorated string for a 'MatchInfo' object that matches the word 'mouse'. + $str = "mouclass `e[7mMouse`e[0m Class Driver Mouse Class Driver Kernel Manual Running OK TRUE FALSE 12,288 32,768 0 C:\WINDOWS\system32\drivers\mouclass.sys 4,096" + $obj = [pscustomobject] @{ b = $str } + $text = $obj | Format-List | Out-String -Width 150 + + $text.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") + } } From b9ff1da7fc2250c247b9a2283a2fb7ed8dae9281 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 10:33:46 -0800 Subject: [PATCH 022/112] Bump github/codeql-action from 4.31.4 to 4.31.5 (#26527) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index c1d812e533..8f4b31ac47 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v3.29.5 + uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v3.29.5 + uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 8df06cd9f1..68eb0ea4b9 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v3.29.5 + uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 with: sarif_file: results.sarif From 01e36432fcfb3e5a0b801817a35ec5332ff7a199 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Wed, 26 Nov 2025 15:48:53 +0900 Subject: [PATCH 023/112] Add tab completion for $PSBoundParameters.Keys switch cases and access patterns (#26483) --- .../CommandCompletion/CompletionAnalysis.cs | 262 +++++++++++++++++- .../TabCompletion/TabCompletion.Tests.ps1 | 181 ++++++++++++ 2 files changed, 437 insertions(+), 6 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs index 8aafa939d8..a6b6f4170b 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionAnalysis.cs @@ -251,8 +251,7 @@ namespace System.Management.Automation { Tuple fileConditionTuple; - var errorStatement = lastAst as ErrorStatementAst; - if (errorStatement != null && errorStatement.Flags != null && errorStatement.Kind != null && tokenBeforeCursor != null && + if (lastAst is ErrorStatementAst errorStatement && errorStatement.Flags is not null && errorStatement.Kind is not null && tokenBeforeCursor is not null && errorStatement.Kind.Kind.Equals(TokenKind.Switch) && errorStatement.Flags.TryGetValue("file", out fileConditionTuple)) { // Handle "switch -file " @@ -267,19 +266,234 @@ namespace System.Management.Automation return false; } - errorStatement = pipeline.Parent as ErrorStatementAst; - if (errorStatement == null || errorStatement.Kind == null || errorStatement.Flags == null) + if (pipeline.Parent is not ErrorStatementAst parentErrorStatement || parentErrorStatement.Kind is null || parentErrorStatement.Flags is null) { return false; } - return (errorStatement.Kind.Kind.Equals(TokenKind.Switch) && - errorStatement.Flags.TryGetValue("file", out fileConditionTuple) && fileConditionTuple.Item2 == pipeline); + return (parentErrorStatement.Kind.Kind.Equals(TokenKind.Switch) && + parentErrorStatement.Flags.TryGetValue("file", out fileConditionTuple) && fileConditionTuple.Item2 == pipeline); } return false; } + /// + /// Check if we should complete parameter names for switch cases on $PSBoundParameters.Keys + /// + private static List CompleteAgainstSwitchCaseCondition(CompletionContext completionContext) + { + var lastAst = completionContext.RelatedAsts.Last(); + + PipelineAst conditionPipeline = null; + Ast switchAst = null; + + // Check if we're in a switch statement (complete) or error statement (incomplete switch) + if (lastAst.Parent is SwitchStatementAst switchStatementAst) + { + // Verify that the lastAst is one of the clause conditions (not in the body) + bool isClauseCondition = switchStatementAst.Clauses.Any(clause => clause.Item1 == lastAst); + + if (!isClauseCondition) + { + return null; + } + + conditionPipeline = switchStatementAst.Condition as PipelineAst; + switchAst = switchStatementAst; + } + else + { + // Check for incomplete switch parsed as ErrorStatementAst + if (lastAst.Parent is not ErrorStatementAst errorStatementAst || errorStatementAst.Kind is null || + errorStatementAst.Kind.Kind != TokenKind.Switch) + { + return null; + } + + // For ErrorStatementAst, the case value is in Bodies, condition is in Conditions + bool isInBodies = errorStatementAst.Bodies != null && errorStatementAst.Bodies.Any(body => body == lastAst); + + if (!isInBodies) + { + return null; + } + + // Get the condition from ErrorStatementAst.Conditions + if (errorStatementAst.Conditions != null && errorStatementAst.Conditions.Count > 0) + { + conditionPipeline = errorStatementAst.Conditions[0] as PipelineAst; + } + switchAst = errorStatementAst; + } + + if (conditionPipeline == null || conditionPipeline.PipelineElements.Count != 1) + { + return null; + } + + if (conditionPipeline.PipelineElements[0] is not CommandExpressionAst commandExpressionAst) + { + return null; + } + + // Check if the expression is a member access on $PSBoundParameters.Keys + if (commandExpressionAst.Expression is not MemberExpressionAst memberExpressionAst) + { + return null; + } + + // Check if the target is $PSBoundParameters + if (memberExpressionAst.Expression is not VariableExpressionAst variableExpressionAst || + !variableExpressionAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Check if the member is "Keys" + if (memberExpressionAst.Member is not StringConstantExpressionAst memberNameAst || + !memberNameAst.Value.Equals("Keys", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Find the nearest param block by traversing up the AST + var paramBlockAst = FindNearestParamBlock(switchAst.Parent); + + if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0) + { + return null; + } + + // Generate completion results from parameter names + var wordToComplete = completionContext.WordToComplete ?? string.Empty; + return CreateParameterCompletionResults(paramBlockAst, wordToComplete); + } + + /// + /// Check if we should complete parameter names for $PSBoundParameters access patterns + /// Supports: $PSBoundParameters.ContainsKey('...'), $PSBoundParameters['...'], $PSBoundParameters.Remove('...') + /// + private static List CompleteAgainstPSBoundParametersAccess(CompletionContext completionContext) + { + var lastAst = completionContext.RelatedAsts.Last(); + + // Must be a string constant + if (lastAst is not StringConstantExpressionAst stringAst) + { + return null; + } + + ExpressionAst targetAst = null; + + // Check for method invocation: $PSBoundParameters.ContainsKey('...') or $PSBoundParameters.Remove('...') + if (lastAst.Parent is InvokeMemberExpressionAst invokeMemberAst) + { + if (invokeMemberAst.Member is StringConstantExpressionAst memberName && + (memberName.Value.Equals("ContainsKey", StringComparison.OrdinalIgnoreCase) || + memberName.Value.Equals("Remove", StringComparison.OrdinalIgnoreCase))) + { + targetAst = invokeMemberAst.Expression; + } + } + // Check for indexer: $PSBoundParameters['...'] + else if (lastAst.Parent is IndexExpressionAst indexAst) + { + targetAst = indexAst.Target; + } + + if (targetAst is null) + { + return null; + } + + // Check if target is $PSBoundParameters + if (targetAst is not VariableExpressionAst variableAst || + !variableAst.VariablePath.UserPath.Equals("PSBoundParameters", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + // Find the nearest param block + var paramBlockAst = FindNearestParamBlock(lastAst.Parent); + + if (paramBlockAst == null || paramBlockAst.Parameters.Count == 0) + { + return null; + } + + // Generate completion results from parameter names + var wordToComplete = completionContext.WordToComplete ?? string.Empty; + + // Determine quote style based on the string constant type + string quoteChar = string.Empty; + if (stringAst.StringConstantType == StringConstantType.SingleQuoted) + { + quoteChar = "'"; + } + else if (stringAst.StringConstantType == StringConstantType.DoubleQuoted) + { + quoteChar = "\""; + } + + return CreateParameterCompletionResults(paramBlockAst, wordToComplete, quoteChar); + } + + /// + /// Finds the nearest ParamBlockAst by traversing up the AST hierarchy. + /// + /// The AST node to start searching from. + /// The nearest ParamBlockAst if found; otherwise, null. + private static ParamBlockAst FindNearestParamBlock(Ast startAst) + { + Ast current = startAst; + while (current != null) + { + if (current is FunctionDefinitionAst functionDefinitionAst) + { + return functionDefinitionAst.Body?.ParamBlock; + } + else if (current is ScriptBlockAst scriptBlockAst) + { + var paramBlock = scriptBlockAst.ParamBlock; + if (paramBlock != null) + { + return paramBlock; + } + } + + current = current.Parent; + } + + return null; + } + + /// + /// Creates completion results from parameter names with optional quote wrapping. + /// + /// The parameter block containing parameters to complete. + /// The partial word to match against parameter names. + /// Optional quote character to wrap completion text (empty string for no quotes). + /// A list of completion results, or null if no matches found. + private static List CreateParameterCompletionResults( + ParamBlockAst paramBlockAst, + string wordToComplete, + string quoteChar = "") + { + var result = paramBlockAst.Parameters + .Select(parameter => parameter.Name.VariablePath.UserPath) + .Where(parameterName => parameterName.StartsWith(wordToComplete, StringComparison.OrdinalIgnoreCase)) + .Select(parameterName => + new CompletionResult( + quoteChar + parameterName + quoteChar, + parameterName, + CompletionResultType.ParameterValue, + parameterName)) + .ToList(); + + return result.Count > 0 ? result : null; + } + private static bool CompleteOperator(Token tokenAtCursor, Ast lastAst) { if (tokenAtCursor.Kind == TokenKind.Minus) @@ -516,6 +730,13 @@ namespace System.Management.Automation { // Handles quoted string inside index expression like: $PSVersionTable[""] completionContext.WordToComplete = (tokenAtCursor as StringToken).Value; + // Check for $PSBoundParameters indexer first + var psBoundResult = CompleteAgainstPSBoundParametersAccess(completionContext); + if (psBoundResult != null && psBoundResult.Count > 0) + { + return psBoundResult; + } + return CompletionCompleters.CompleteIndexExpression(completionContext, indexExpressionAst.Target); } @@ -1915,6 +2136,21 @@ namespace System.Management.Automation string strValue = constantString != null ? constantString.Value : expandableString.Value; + // Check for switch case completion on $PSBoundParameters.Keys + completionContext.WordToComplete = strValue; + var switchCaseResult = CompleteAgainstSwitchCaseCondition(completionContext); + if (switchCaseResult != null && switchCaseResult.Count > 0) + { + return switchCaseResult; + } + + // Check for $PSBoundParameters access patterns (ContainsKey, indexer, Remove) + var psBoundResult = CompleteAgainstPSBoundParametersAccess(completionContext); + if (psBoundResult != null && psBoundResult.Count > 0) + { + return psBoundResult; + } + bool shouldContinue; List result = GetResultForEnumPropertyValueOfDSCResource(completionContext, strValue, ref replacementIndex, ref replacementLength, out shouldContinue); if (!shouldContinue || (result != null && result.Count > 0)) @@ -2076,6 +2312,20 @@ namespace System.Management.Automation var tokenAtCursorText = tokenAtCursor.Text; completionContext.WordToComplete = tokenAtCursorText; + // Check for switch case completion on $PSBoundParameters.Keys + var switchCaseResult = CompleteAgainstSwitchCaseCondition(completionContext); + if (switchCaseResult != null && switchCaseResult.Count > 0) + { + return switchCaseResult; + } + + // Check for $PSBoundParameters access patterns (ContainsKey, indexer, Remove) + var psBoundResult = CompleteAgainstPSBoundParametersAccess(completionContext); + if (psBoundResult != null && psBoundResult.Count > 0) + { + return psBoundResult; + } + if (lastAst.Parent is BreakStatementAst || lastAst.Parent is ContinueStatementAst) { return CompleteLoopLabel(completionContext); diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 index 046d00b442..f8762a6392 100644 --- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 +++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 @@ -4012,4 +4012,185 @@ Describe "WSMan Config Provider tab complete tests" -Tags Feature,RequireAdminOn # https://github.com/PowerShell/PowerShell/issues/4744 # TODO: move to test cases above once working } + + Context "Tab completion for switch cases on `$PSBoundParameters.Keys" { + It "Should complete parameter names in switch case for `$PSBoundParameters.Keys" { + $inputScript = @" +function Test-Func { + param( + [string]`$Param1, + [string]`$Param2, + [int]`$Count + ) + switch (`$PSBoundParameters.Keys) { + P + } +} +"@ + $cursorPosition = $inputScript.IndexOf("P", $inputScript.IndexOf("Keys)")) + 1 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "Param1" + $completionTexts[1] | Should -BeExactly "Param2" + } + + It "Should complete all parameter names when prefix matches single param" { + $inputScript = @" +function Test-Func { + param( + [string]`$Name, + [int]`$Value + ) + switch (`$PSBoundParameters.Keys) { + N + } +} +"@ + $cursorPosition = $inputScript.IndexOf("N", $inputScript.IndexOf("Keys)")) + 1 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "Name" + } + + It "Should complete parameter names in scriptblock param" { + $inputScript = @" +`$sb = { + param( + [string]`$ScriptParam1, + [string]`$ScriptParam2 + ) + switch (`$PSBoundParameters.Keys) { + S + } +} +"@ + $cursorPosition = $inputScript.IndexOf("S", $inputScript.IndexOf("Keys)")) + 1 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "ScriptParam1" + $completionTexts[1] | Should -BeExactly "ScriptParam2" + } + } + + Context "Tab completion for `$PSBoundParameters access patterns" { + It "Should complete parameter names for ContainsKey method" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2, [int]`$Count) + if (`$PSBoundParameters.ContainsKey('P')) { } +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "'Param1'" + $completionTexts[1] | Should -BeExactly "'Param2'" + } + + It "Should complete parameter names for indexer access" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2, [int]`$Count) + `$value = `$PSBoundParameters['P'] +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly "'Param1'" + $completionTexts[1] | Should -BeExactly "'Param2'" + } + + It "Should complete parameter names for Remove method" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2, [int]`$Count) + `$PSBoundParameters.Remove('C') +} +"@ + $cursorPosition = $inputScript.IndexOf("'C'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 1 + $res.CompletionMatches[0].CompletionText | Should -BeExactly "'Count'" + } + + It "Should complete with double quotes when using double-quoted string" { + $inputScript = @" +function Test-Func { + param([string]`$Param1, [string]`$Param2) + if (`$PSBoundParameters.ContainsKey("P")) { } +} +"@ + $cursorPosition = $inputScript.IndexOf('"P"') + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + $res.CompletionMatches | Should -HaveCount 2 + $completionTexts = $res.CompletionMatches.CompletionText | Sort-Object + $completionTexts[0] | Should -BeExactly '"Param1"' + $completionTexts[1] | Should -BeExactly '"Param2"' + } + + It "Should not complete for non-PSBoundParameters variable with indexer" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + `$value = `$hash['P'] +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq "'Param1'" } + $paramCompletion | Should -BeNullOrEmpty + } + + It "Should not complete for non-PSBoundParameters variable with ContainsKey" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + if (`$hash.ContainsKey('P')) { } +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq "'Param1'" } + $paramCompletion | Should -BeNullOrEmpty + } + + It "Should not complete for non-PSBoundParameters variable with Remove" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + `$hash.Remove('P') +} +"@ + $cursorPosition = $inputScript.IndexOf("'P'") + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq "'Param1'" } + $paramCompletion | Should -BeNullOrEmpty + } + + It "Should not complete for non-PSBoundParameters variable with double quotes" { + $inputScript = @" +function Test-Func { + param([string]`$Param1) + `$hash = @{} + if (`$hash.ContainsKey("P")) { } +} +"@ + $cursorPosition = $inputScript.IndexOf('"P"') + 2 + $res = TabExpansion2 -inputScript $inputScript -cursorColumn $cursorPosition + # Should not return Param1 as completion + $paramCompletion = $res.CompletionMatches | Where-Object { $_.CompletionText -eq '"Param1"' } + $paramCompletion | Should -BeNullOrEmpty + } + } } From ef0f9ae0ba9a67cab954d8d58c9868dd89569939 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 17:01:52 -0800 Subject: [PATCH 024/112] Bump github/codeql-action from 4.31.5 to 4.31.6 (#26554) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 8f4b31ac47..21bc1ed1e9 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 + uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 + uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 68eb0ea4b9..3c322397db 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@fdbfb4d2750291e159f0156def62b853c2798ca2 # v3.29.5 + uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 with: sarif_file: results.sarif From 11f91dfee3eeb7445be8680e0e59ef84f2cad2f5 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:16:56 +0000 Subject: [PATCH 025/112] Disable AMSI content logging in release (#26235) --- .../engine/runtime/Operations/MiscOps.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs index 50054f6501..d584666ab6 100644 --- a/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs +++ b/src/System.Management.Automation/engine/runtime/Operations/MiscOps.cs @@ -3644,6 +3644,7 @@ namespace System.Management.Automation internal static class MemberInvocationLoggingOps { +#if DEBUG private static readonly Lazy DumpLogAMSIContent = new Lazy( () => { object result = Environment.GetEnvironmentVariable("__PSDumpAMSILogContent"); @@ -3654,6 +3655,7 @@ namespace System.Management.Automation return false; } ); +#endif private static string ArgumentToString(object arg) { @@ -3708,20 +3710,24 @@ namespace System.Management.Automation string content = $"<{targetName}>.{name}({argsBuilder})"; +#if DEBUG if (DumpLogAMSIContent.Value) { Console.WriteLine("\n=== Amsi notification report content ==="); Console.WriteLine(content); } +#endif var success = AmsiUtils.ReportContent( name: contentName, content: content); +#if DEBUG if (DumpLogAMSIContent.Value) { Console.WriteLine($"=== Amsi notification report success: {success} ==="); } +#endif } catch (PSSecurityException) { @@ -3729,12 +3735,16 @@ namespace System.Management.Automation // must be propagated. throw; } +#pragma warning disable CS0168 // variable declared but never used catch (Exception ex) +#pragma warning restore CS0168 { +#if DEBUG if (DumpLogAMSIContent.Value) { Console.WriteLine($"!!! Amsi notification report exception: {ex} !!!"); } +#endif } } } From 5c57c3e19c85a03b9b493c14d9f4f6ec7b51bce6 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Wed, 3 Dec 2025 17:35:44 +0900 Subject: [PATCH 026/112] Fix NOTES section formatting in comment-based help (#26512) --- .../DefaultFormatters/Help_format_ps1xml.cs | 5 +--- .../ScriptHelp.NotesFormatting.Tests.ps1 | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 test/powershell/Language/Scripting/ScriptHelp.NotesFormatting.Tests.ps1 diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/Help_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/Help_format_ps1xml.cs index 05f5728602..6825ec14e6 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/Help_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/Help_format_ps1xml.cs @@ -680,10 +680,7 @@ if (($_.relatedLinks -ne $()) -and ($_.relatedLinks.navigationLink -ne $()) -and .StartFrame(leftIndent: 4) .AddPropertyExpressionBinding(@"title") .AddNewline() - .AddNewline() - .StartFrame(leftIndent: 4) - .AddPropertyExpressionBinding(@"alert", enumerateCollection: true, customControl: sharedControls[11]) - .EndFrame() + .AddPropertyExpressionBinding(@"alert", enumerateCollection: true, customControl: sharedControls[11]) .AddNewline() .EndFrame() .EndEntry() diff --git a/test/powershell/Language/Scripting/ScriptHelp.NotesFormatting.Tests.ps1 b/test/powershell/Language/Scripting/ScriptHelp.NotesFormatting.Tests.ps1 new file mode 100644 index 0000000000..77c0c65b1a --- /dev/null +++ b/test/powershell/Language/Scripting/ScriptHelp.NotesFormatting.Tests.ps1 @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Comment-based help NOTES section formatting' -Tags "CI", "Feature" { + It 'NOTES section should have single blank line after header and 4-space indentation' { + $helpText = Get-Help Remove-PSSession -Full | Out-String + $lines = $helpText -split "`r?`n" + + # Find NOTES section + $notesIndex = -1 + for ($i = 0; $i -lt $lines.Count; $i++) { + if ($lines[$i] -match '^NOTES\s*$') { + $notesIndex = $i + break + } + } + + $notesIndex | Should -BeGreaterThan -1 -Because "NOTES section should exist" + + # The line after NOTES header should be blank + $lines[$notesIndex + 1] | Should -Match '^\s*$' -Because "There should be a blank line after NOTES header" + + # The second line after NOTES header should have content with 4-space indentation + $contentLine = $lines[$notesIndex + 2] + $contentLine | Should -Match '^[ ]{4}\S' -Because "Content should have exactly 4-space indentation" + } +} From 6311c7c2f6aa913408aa6fb2e619cd15c95ebd40 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Fri, 5 Dec 2025 13:54:52 +0900 Subject: [PATCH 027/112] Add -ExcludeProperty parameter to Format-* cmdlets (#26514) --- .../format-object/Format-Object.cs | 34 ++++++++--- .../format-wide/Format-Wide.cs | 37 +++++++++--- .../commands/utility/Select-Object.cs | 41 ------------- .../common/BaseFormattingCommand.cs | 39 ++++++++++--- .../common/BaseFormattingCommandParameters.cs | 5 ++ .../common/FormatViewGenerator.cs | 15 ++++- .../common/FormatViewGenerator_Complex.cs | 22 +++++-- .../common/FormatViewGenerator_List.cs | 8 ++- .../common/FormatViewGenerator_Table.cs | 57 ++++++++++--------- .../common/FormatViewGenerator_Wide.cs | 49 ++++++++-------- .../common/Utilities/Mshexpression.cs | 37 ++++++++++++ .../CommandCompletion/CompletionCompleters.cs | 3 +- .../Format-Custom.Tests.ps1 | 44 ++++++++++++++ .../Format-List.Tests.ps1 | 44 ++++++++++++++ .../Format-Table.Tests.ps1 | 44 ++++++++++++++ .../Format-Wide.Tests.ps1 | 52 +++++++++++++++++ 16 files changed, 404 insertions(+), 127 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs index 2179243279..c2c16ff2cd 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs @@ -41,6 +41,12 @@ namespace Microsoft.PowerShell.Commands private object[] _props; + /// + /// Gets or sets the properties to exclude from formatting. + /// + [Parameter] + public string[] ExcludeProperty { get; set; } + /// /// /// @@ -61,6 +67,18 @@ namespace Microsoft.PowerShell.Commands { FormattingCommandLineParameters parameters = new(); + // Check View conflicts first (before any auto-expansion) + if (!string.IsNullOrEmpty(this.View)) + { + // View cannot be used with Property or ExcludeProperty + if ((_props is not null && _props.Length != 0) || (ExcludeProperty is not null && ExcludeProperty.Length != 0)) + { + ReportCannotSpecifyViewAndProperty(); + } + + parameters.viewName = this.View; + } + if (_props != null) { ParameterProcessor processor = new(new FormatObjectParameterDefinition()); @@ -68,15 +86,17 @@ namespace Microsoft.PowerShell.Commands parameters.mshParameterList = processor.ProcessParameters(_props, invocationContext); } - if (!string.IsNullOrEmpty(this.View)) + if (ExcludeProperty is not null) { - // we have a view command line switch - if (parameters.mshParameterList.Count != 0) - { - ReportCannotSpecifyViewAndProperty(); - } + parameters.excludePropertyFilter = new PSPropertyExpressionFilter(ExcludeProperty); - parameters.viewName = this.View; + // ExcludeProperty implies -Property * for better UX + if (_props is null || _props.Length == 0) + { + ParameterProcessor processor = new(new FormatObjectParameterDefinition()); + TerminatingErrorContext invocationContext = new(this); + parameters.mshParameterList = processor.ProcessParameters(new object[] { "*" }, invocationContext); + } } parameters.groupByParameter = this.ProcessGroupByParameter(); diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-wide/Format-Wide.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-wide/Format-Wide.cs index aeddf498f4..c6aef5c20b 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-wide/Format-Wide.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-wide/Format-Wide.cs @@ -42,9 +42,14 @@ namespace Microsoft.PowerShell.Commands private object _prop; /// - /// Optional, non positional parameter. + /// Gets or sets the properties to exclude from formatting. + /// + [Parameter] + public string[] ExcludeProperty { get; set; } + + /// + /// Gets or sets a value indicating whether to autosize the output. /// - /// [Parameter] public SwitchParameter AutoSize { @@ -74,6 +79,18 @@ namespace Microsoft.PowerShell.Commands { FormattingCommandLineParameters parameters = new(); + // Check View conflicts first (before any auto-expansion) + if (!string.IsNullOrEmpty(this.View)) + { + // View cannot be used with Property or ExcludeProperty + if (_prop is not null || (ExcludeProperty is not null && ExcludeProperty.Length != 0)) + { + ReportCannotSpecifyViewAndProperty(); + } + + parameters.viewName = this.View; + } + if (_prop != null) { ParameterProcessor processor = new(new FormatWideParameterDefinition()); @@ -81,15 +98,17 @@ namespace Microsoft.PowerShell.Commands parameters.mshParameterList = processor.ProcessParameters(new object[] { _prop }, invocationContext); } - if (!string.IsNullOrEmpty(this.View)) + if (ExcludeProperty is not null) { - // we have a view command line switch - if (parameters.mshParameterList.Count != 0) - { - ReportCannotSpecifyViewAndProperty(); - } + parameters.excludePropertyFilter = new PSPropertyExpressionFilter(ExcludeProperty); - parameters.viewName = this.View; + // ExcludeProperty implies -Property * for better UX + if (_prop is null) + { + ParameterProcessor processor = new(new FormatWideParameterDefinition()); + TerminatingErrorContext invocationContext = new(this); + parameters.mshParameterList = processor.ProcessParameters(new object[] { "*" }, invocationContext); + } } // we cannot specify -column and -autosize, they are mutually exclusive diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Select-Object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Select-Object.cs index 9db27335da..e0b0bff07c 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Select-Object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/Select-Object.cs @@ -12,47 +12,6 @@ using Microsoft.PowerShell.Commands.Internal.Format; namespace Microsoft.PowerShell.Commands { - /// - /// Helper class to do wildcard matching on PSPropertyExpressions. - /// - internal sealed class PSPropertyExpressionFilter - { - /// - /// Initializes a new instance of the class - /// with the specified array of patterns. - /// - /// Array of pattern strings to use. - internal PSPropertyExpressionFilter(string[] wildcardPatternsStrings) - { - ArgumentNullException.ThrowIfNull(wildcardPatternsStrings); - - _wildcardPatterns = new WildcardPattern[wildcardPatternsStrings.Length]; - for (int k = 0; k < wildcardPatternsStrings.Length; k++) - { - _wildcardPatterns[k] = WildcardPattern.Get(wildcardPatternsStrings[k], WildcardOptions.IgnoreCase); - } - } - - /// - /// Try to match the expression against the array of wildcard patterns. - /// The first match shortcircuits the search. - /// - /// PSPropertyExpression to test against. - /// True if there is a match, else false. - internal bool IsMatch(PSPropertyExpression expression) - { - for (int k = 0; k < _wildcardPatterns.Length; k++) - { - if (_wildcardPatterns[k].IsMatch(expression.ToString())) - return true; - } - - return false; - } - - private readonly WildcardPattern[] _wildcardPatterns; - } - internal sealed class SelectObjectExpressionParameterDefinition : CommandParameterDefinition { protected override void SetEntries() diff --git a/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs b/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs index 18b74cbcfe..683553a3b3 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Management.Automation; using System.Management.Automation.Internal; using System.Management.Automation.Runspaces; +using Microsoft.PowerShell.Commands; namespace Microsoft.PowerShell.Commands.Internal.Format { @@ -729,6 +730,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format [Parameter(Position = 0)] public object[] Property { get; set; } + /// + /// Optional parameter for excluding properties from formatting. + /// + [Parameter] + public string[] ExcludeProperty { get; set; } + #endregion internal override FormattingCommandLineParameters GetCommandLineParameters() @@ -751,6 +758,18 @@ namespace Microsoft.PowerShell.Commands.Internal.Format internal void GetCommandLineProperties(FormattingCommandLineParameters parameters, bool isTable) { + // Check View conflicts first (before any auto-expansion) + if (!string.IsNullOrEmpty(this.View)) + { + // View cannot be used with Property or ExcludeProperty + if ((Property is not null && Property.Length != 0) || (ExcludeProperty is not null && ExcludeProperty.Length != 0)) + { + ReportCannotSpecifyViewAndProperty(); + } + + parameters.viewName = this.View; + } + if (Property != null) { CommandParameterDefinition def; @@ -765,15 +784,21 @@ namespace Microsoft.PowerShell.Commands.Internal.Format parameters.mshParameterList = processor.ProcessParameters(Property, invocationContext); } - if (!string.IsNullOrEmpty(this.View)) + if (ExcludeProperty is not null) { - // we have a view command line switch - if (parameters.mshParameterList.Count != 0) - { - ReportCannotSpecifyViewAndProperty(); - } + parameters.excludePropertyFilter = new PSPropertyExpressionFilter(ExcludeProperty); - parameters.viewName = this.View; + // ExcludeProperty implies -Property * for better UX + if (Property is null || Property.Length == 0) + { + CommandParameterDefinition def = isTable + ? new FormatTableParameterDefinition() + : new FormatListParameterDefinition(); + ParameterProcessor processor = new ParameterProcessor(def); + TerminatingErrorContext invocationContext = new TerminatingErrorContext(this); + + parameters.mshParameterList = processor.ProcessParameters(new object[] { "*" }, invocationContext); + } } } } diff --git a/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommandParameters.cs b/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommandParameters.cs index 7f88727ace..498f6098a2 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommandParameters.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommandParameters.cs @@ -70,6 +70,11 @@ namespace Microsoft.PowerShell.Commands.Internal.Format /// Extension mechanism for shape specific parameters. /// internal ShapeSpecificParameters shapeParameters = null; + + /// + /// Filter for excluding properties from formatting. + /// + internal PSPropertyExpressionFilter excludePropertyFilter = null; } /// diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs index db1bc0704a..cb20e708cc 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.Linq; using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Internal; @@ -348,7 +349,19 @@ namespace Microsoft.PowerShell.Commands.Internal.Format protected DataBaseInfo dataBaseInfo = new DataBaseInfo(); protected List activeAssociationList = null; - protected FormattingCommandLineParameters inputParameters = null; + /// + /// Apply ExcludeProperty filter to activeAssociationList if specified. + /// This method filters and updates "activeAssociationList" instance property. + /// + protected void ApplyExcludePropertyFilter() + { + if (this.parameters is not null && this.parameters.excludePropertyFilter is not null) + { + this.activeAssociationList = this.activeAssociationList + .Where(item => !this.parameters.excludePropertyFilter.IsMatch(item.ResolvedExpression)) + .ToList(); + } + } protected string GetExpressionDisplayValue(PSObject so, int enumerationLimit, PSPropertyExpression ex, FieldFormattingDirective directive) diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs index b2bbd27c2b..972a716083 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs @@ -3,6 +3,7 @@ using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Internal; @@ -16,7 +17,6 @@ namespace Microsoft.PowerShell.Commands.Internal.Format PSObject so, TypeInfoDataBase db, FormattingCommandLineParameters parameters) { base.Initialize(errorContext, expressionFactory, so, db, parameters); - this.inputParameters = parameters; } internal override FormatStartData GenerateStartData(PSObject so) @@ -40,7 +40,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format private ComplexViewEntry GenerateComplexViewEntryFromProperties(PSObject so, int enumerationLimit) { ComplexViewObjectBrowser browser = new ComplexViewObjectBrowser(this.ErrorManager, this.expressionFactory, enumerationLimit); - return browser.GenerateView(so, this.inputParameters); + return browser.GenerateView(so, this.parameters); } private ComplexViewEntry GenerateComplexViewEntryFromDataBaseInfo(PSObject so, int enumerationLimit) @@ -425,17 +425,18 @@ namespace Microsoft.PowerShell.Commands.Internal.Format /// of the object. /// /// Object to process. - /// Parameters from the command line. + /// Parameters from the command line. /// Complex view entry to send to the output command. - internal ComplexViewEntry GenerateView(PSObject so, FormattingCommandLineParameters inputParameters) + internal ComplexViewEntry GenerateView(PSObject so, FormattingCommandLineParameters parameters) { - _complexSpecificParameters = (ComplexSpecificParameters)inputParameters.shapeParameters; + _parameters = parameters; + _complexSpecificParameters = (ComplexSpecificParameters)parameters.shapeParameters; int maxDepth = _complexSpecificParameters.maxDepth; TraversalInfo level = new TraversalInfo(0, maxDepth); List mshParameterList = null; - mshParameterList = inputParameters.mshParameterList; + mshParameterList = parameters.mshParameterList; // create a top level entry as root of the tree ComplexViewEntry cve = new ComplexViewEntry(); @@ -513,6 +514,14 @@ namespace Microsoft.PowerShell.Commands.Internal.Format List activeAssociationList = AssociationManager.SetupActiveProperties(parameterList, so, _expressionFactory); + // Apply ExcludeProperty filter if specified + if (_parameters != null && _parameters.excludePropertyFilter != null) + { + activeAssociationList = activeAssociationList + .Where(item => !_parameters.excludePropertyFilter.IsMatch(item.ResolvedExpression)) + .ToList(); + } + // create a format entry FormatEntry fe = new FormatEntry(); formatValueList.Add(fe); @@ -758,6 +767,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format return feFrame.formatValueList; } + private FormattingCommandLineParameters _parameters; private ComplexSpecificParameters _complexSpecificParameters; /// diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs index f7acd9ed22..6ef1b24ce1 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs @@ -31,7 +31,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format _listBody = (ListControlBody)this.dataBaseInfo.view.mainControl; } - this.inputParameters = parameters; + this.parameters = parameters; SetUpActiveProperties(so); } @@ -227,10 +227,12 @@ namespace Microsoft.PowerShell.Commands.Internal.Format { List mshParameterList = null; - if (this.inputParameters != null) - mshParameterList = this.inputParameters.mshParameterList; + if (this.parameters != null) + mshParameterList = this.parameters.mshParameterList; this.activeAssociationList = AssociationManager.SetupActiveProperties(mshParameterList, so, this.expressionFactory); + + ApplyExcludePropertyFilter(); } } } diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs index 0ddc307b64..33d2bd5350 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs @@ -40,40 +40,45 @@ namespace Microsoft.PowerShell.Commands.Internal.Format rawMshParameterList = parameters.mshParameterList; // check if we received properties from the command line - if (rawMshParameterList != null && rawMshParameterList.Count > 0) + if (rawMshParameterList is not null && rawMshParameterList.Count > 0) { this.activeAssociationList = AssociationManager.ExpandTableParameters(rawMshParameterList, so); - return; } - - // we did not get any properties: - // try to get properties from the default property set of the object - this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); - if (this.activeAssociationList.Count > 0) + else { - // we got a valid set of properties from the default property set..add computername for - // remoteobjects (if available) - if (PSObjectHelper.ShouldShowComputerNameProperty(so)) + // we did not get any properties: + // try to get properties from the default property set of the object + this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); + if (this.activeAssociationList.Count > 0) { - activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, - new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty))); + // we got a valid set of properties from the default property set..add computername for + // remoteobjects (if available) + if (PSObjectHelper.ShouldShowComputerNameProperty(so)) + { + activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, + new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty))); + } + } + else + { + // we failed to get anything from the default property set + this.activeAssociationList = AssociationManager.ExpandAll(so); + if (this.activeAssociationList.Count > 0) + { + // Remove PSComputerName and PSShowComputerName from the display as needed. + AssociationManager.HandleComputerNameProperties(so, activeAssociationList); + FilterActiveAssociationList(); + } + else + { + // we were unable to retrieve any properties, so we leave an empty list + this.activeAssociationList = new List(); + return; + } } - - return; } - // we failed to get anything from the default property set - this.activeAssociationList = AssociationManager.ExpandAll(so); - if (this.activeAssociationList.Count > 0) - { - // Remove PSComputerName and PSShowComputerName from the display as needed. - AssociationManager.HandleComputerNameProperties(so, activeAssociationList); - FilterActiveAssociationList(); - return; - } - - // we were unable to retrieve any properties, so we leave an empty list - this.activeAssociationList = new List(); + ApplyExcludePropertyFilter(); } /// diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs index 26c0af670c..a45c006e7f 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs @@ -13,7 +13,6 @@ namespace Microsoft.PowerShell.Commands.Internal.Format PSObject so, TypeInfoDataBase db, FormattingCommandLineParameters parameters) { base.Initialize(errorContext, expressionFactory, so, db, parameters); - this.inputParameters = parameters; } internal override FormatStartData GenerateStartData(PSObject so) @@ -159,39 +158,37 @@ namespace Microsoft.PowerShell.Commands.Internal.Format private void SetUpActiveProperty(PSObject so) { - List rawMshParameterList = null; - - if (this.inputParameters != null) - rawMshParameterList = this.inputParameters.mshParameterList; + List rawMshParameterList = this.parameters?.mshParameterList; // check if we received properties from the command line - if (rawMshParameterList != null && rawMshParameterList.Count > 0) + if (rawMshParameterList is not null && rawMshParameterList.Count > 0) { this.activeAssociationList = AssociationManager.ExpandParameters(rawMshParameterList, so); - return; } - - // we did not get any properties: - // try to get the display property of the object - PSPropertyExpression displayNameExpression = PSObjectHelper.GetDisplayNameExpression(so, this.expressionFactory); - if (displayNameExpression != null) + else { - this.activeAssociationList = new List(); - this.activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, displayNameExpression)); - return; + // we did not get any properties: + // try to get the display property of the object + PSPropertyExpression displayNameExpression = PSObjectHelper.GetDisplayNameExpression(so, this.expressionFactory); + if (displayNameExpression is not null) + { + this.activeAssociationList = new List(); + this.activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, displayNameExpression)); + } + else + { + // try to get the default property set (we will use the first property) + this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); + if (this.activeAssociationList.Count == 0) + { + // we failed to get anything from the default property set + // just get all the properties + this.activeAssociationList = AssociationManager.ExpandAll(so); + } + } } - // try to get the default property set (we will use the first property) - this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); - if (this.activeAssociationList.Count > 0) - { - // we got a valid set of properties from the default property set - return; - } - - // we failed to get anything from the default property set - // just get all the properties - this.activeAssociationList = AssociationManager.ExpandAll(so); + ApplyExcludePropertyFilter(); } } } diff --git a/src/System.Management.Automation/FormatAndOutput/common/Utilities/Mshexpression.cs b/src/System.Management.Automation/FormatAndOutput/common/Utilities/Mshexpression.cs index 552d511fd4..55ee3c30dd 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/Utilities/Mshexpression.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/Utilities/Mshexpression.cs @@ -375,5 +375,42 @@ namespace Microsoft.PowerShell.Commands private bool _isResolved = false; #endregion Private Members + + } + + /// + /// Helper class to do wildcard matching on PSPropertyExpressions. + /// + internal sealed class PSPropertyExpressionFilter + { + /// + /// Initializes a new instance of the class + /// with the specified array of patterns. + /// + /// Array of pattern strings to use. + internal PSPropertyExpressionFilter(string[] wildcardPatternsStrings) + { + ArgumentNullException.ThrowIfNull(wildcardPatternsStrings); + + _wildcardPatterns = new WildcardPattern[wildcardPatternsStrings.Length]; + for (int k = 0; k < wildcardPatternsStrings.Length; k++) + { + _wildcardPatterns[k] = WildcardPattern.Get(wildcardPatternsStrings[k], WildcardOptions.IgnoreCase); + } + } + + /// + /// Try to match the expression against the array of wildcard patterns. + /// The first match short-circuits the search. + /// + /// PSPropertyExpression to test against. + /// True if there is a match, else false. + internal bool IsMatch(PSPropertyExpression expression) + { + string expressionString = expression.ToString(); + return _wildcardPatterns.Any(pattern => pattern.IsMatch(expressionString)); + } + + private readonly WildcardPattern[] _wildcardPatterns; } } diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs index 335a704dd6..73406e449b 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionCompleters.cs @@ -2504,7 +2504,8 @@ namespace System.Management.Automation case "Format-Table": case "Format-Wide": { - if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase)) + if (parameterName.Equals("Property", StringComparison.OrdinalIgnoreCase) + || parameterName.Equals("ExcludeProperty", StringComparison.OrdinalIgnoreCase)) { NativeCompletionMemberName(context, result, commandAst, boundArguments?[parameterName]); } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Custom.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Custom.Tests.ps1 index 80aad6ef5b..c82d8b77b8 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Custom.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Custom.Tests.ps1 @@ -466,4 +466,48 @@ SelectScriptBlock $ps.Invoke() -replace '\r?\n', "^" | Should -BeExactly $expectedOutput $ps.Streams.Error | Should -BeNullOrEmpty } + + Context 'ExcludeProperty parameter' { + It 'Should exclude specified properties' { + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle' } + $result = $obj | Format-Custom -ExcludeProperty Age | Out-String + $result | Should -Match 'Name' + $result | Should -Match 'City' + $result | Should -Not -Match 'Age' + } + + It 'Should work with wildcard patterns' { + $obj = [pscustomobject]@{ Prop1 = 1; Prop2 = 2; Other = 3 } + $result = $obj | Format-Custom -ExcludeProperty Prop* | Out-String + $result | Should -Match 'Other' + $result | Should -Not -Match 'Prop1' + $result | Should -Not -Match 'Prop2' + } + + It 'Should work without Property parameter (implies -Property *)' { + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3 } + $result = $obj | Format-Custom -ExcludeProperty B | Out-String + $result | Should -Match 'A\s*=' + $result | Should -Match 'C\s*=' + $result | Should -Not -Match 'B\s*=' + } + + It 'Should work with Property parameter' { + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle'; Country = 'USA' } + $result = $obj | Format-Custom -Property Name, Age, City, Country -ExcludeProperty Age | Out-String + $result | Should -Match 'Name' + $result | Should -Match 'City' + $result | Should -Match 'Country' + $result | Should -Not -Match 'Age' + } + + It 'Should handle multiple excluded properties' { + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3; D = 4 } + $result = $obj | Format-Custom -ExcludeProperty B, D | Out-String + $result | Should -Match 'A\s*=' + $result | Should -Match 'C\s*=' + $result | Should -Not -Match 'B\s*=' + $result | Should -Not -Match 'D\s*=' + } + } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 index b9079b9213..342383f388 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-List.Tests.ps1 @@ -261,4 +261,48 @@ Describe 'Format-List color tests' -Tag 'CI' { $output = Get-Content "$TestDrive/outfile.txt" -Raw $output.Trim().Replace("`r", "") | Should -BeExactly $expected.Replace("`r", "") } + + Context 'ExcludeProperty parameter' { + It 'Should exclude specified properties' { + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle' } + $result = $obj | Format-List -ExcludeProperty Age | Out-String + $result | Should -Match 'Name' + $result | Should -Match 'City' + $result | Should -Not -Match 'Age' + } + + It 'Should work with wildcard patterns' { + $obj = [pscustomobject]@{ Prop1 = 1; Prop2 = 2; Other = 3 } + $result = $obj | Format-List -ExcludeProperty Prop* | Out-String + $result | Should -Match 'Other' + $result | Should -Not -Match 'Prop1' + $result | Should -Not -Match 'Prop2' + } + + It 'Should work without Property parameter (implies -Property *)' { + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3 } + $result = $obj | Format-List -ExcludeProperty B | Out-String + $result | Should -Match 'A' + $result | Should -Match 'C' + $result | Should -Not -Match 'B' + } + + It 'Should work with Property parameter' { + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle'; Country = 'USA' } + $result = $obj | Format-List -Property Name, Age, City, Country -ExcludeProperty Age | Out-String + $result | Should -Match 'Name' + $result | Should -Match 'City' + $result | Should -Match 'Country' + $result | Should -Not -Match 'Age' + } + + It 'Should handle multiple excluded properties' { + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3; D = 4 } + $result = $obj | Format-List -ExcludeProperty B, D | Out-String + $result | Should -Match 'A' + $result | Should -Match 'C' + $result | Should -Not -Match 'B' + $result | Should -Not -Match 'D' + } + } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 index 34f2245909..d102ef8bbd 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Table.Tests.ps1 @@ -953,4 +953,48 @@ Describe 'Table color tests' -Tag 'CI' { $actual | Should -BeExactly $expected } + + Context 'ExcludeProperty parameter' { + It 'Should exclude specified properties' { + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle' } + $result = $obj | Format-Table -ExcludeProperty Age | Out-String + $result | Should -Match 'Name' + $result | Should -Match 'City' + $result | Should -Not -Match 'Age' + } + + It 'Should work with wildcard patterns' { + $obj = [pscustomobject]@{ Prop1 = 1; Prop2 = 2; Other = 3 } + $result = $obj | Format-Table -ExcludeProperty Prop* | Out-String + $result | Should -Match 'Other' + $result | Should -Not -Match 'Prop1' + $result | Should -Not -Match 'Prop2' + } + + It 'Should work without Property parameter (implies -Property *)' { + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3 } + $result = $obj | Format-Table -ExcludeProperty B | Out-String + $result | Should -Match 'A' + $result | Should -Match 'C' + $result | Should -Not -Match 'B' + } + + It 'Should work with Property parameter' { + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle'; Country = 'USA' } + $result = $obj | Format-Table -Property Name, Age, City, Country -ExcludeProperty Age | Out-String + $result | Should -Match 'Name' + $result | Should -Match 'City' + $result | Should -Match 'Country' + $result | Should -Not -Match 'Age' + } + + It 'Should handle multiple excluded properties' { + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3; D = 4 } + $result = $obj | Format-Table -ExcludeProperty B, D | Out-String + $result | Should -Match 'A' + $result | Should -Match 'C' + $result | Should -Not -Match 'B' + $result | Should -Not -Match 'D' + } + } } diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Wide.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Wide.Tests.ps1 index c81db9fa1f..10acd69906 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Wide.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Format-Wide.Tests.ps1 @@ -105,4 +105,56 @@ Describe "Format-Wide DRT basic functionality" -Tags "CI" { $result | Should -Match " GroupingKey:" $result | Should -Match "name2\s+name3" } + + Context 'ExcludeProperty parameter' { + It 'Should exclude specified property and display first remaining' { + # PSCustomObject properties are in definition order: Name, Age, City + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle' } + # Exclude Name, should display Age (first remaining) + $result = $obj | Format-Wide -ExcludeProperty Name | Out-String + $result | Should -Match '30' + $result | Should -Not -Match 'Test' + } + + It 'Should work with wildcard patterns' { + # Properties: Prop1, Prop2, Other + $obj = [pscustomobject]@{ Prop1 = 1; Prop2 = 2; Other = 3 } + # Exclude Prop*, should display Other (only remaining) + $result = $obj | Format-Wide -ExcludeProperty Prop* | Out-String + $result | Should -Match '3' + $result | Should -Not -Match '1' + $result | Should -Not -Match '2' + } + + It 'Should work without Property parameter (implies -Property *)' { + # Properties: A, B, C + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3 } + # Exclude B, C - should display A (first remaining) + $result = $obj | Format-Wide -ExcludeProperty B, C | Out-String + $result | Should -Match '1' + $result | Should -Not -Match '2' + $result | Should -Not -Match '3' + } + + It 'Should display first remaining property after exclusion' { + # Properties: Name, Age, City, Country + $obj = [pscustomobject]@{ Name = 'Test'; Age = 30; City = 'Seattle'; Country = 'USA' } + # Exclude Name and Age, should display City (first remaining) + $result = $obj | Format-Wide -ExcludeProperty Name, Age | Out-String + $result | Should -Match 'Seattle' + $result | Should -Not -Match 'Test' + $result | Should -Not -Match '30' + # USA might appear but not in the wide field, or might not appear at all since only first property is shown + } + + It 'Should handle multiple excluded properties' { + # Properties: A, B, C, D + $obj = [pscustomobject]@{ A = 1; B = 2; C = 3; D = 4 } + # Exclude A and B, should display C (first remaining) + $result = $obj | Format-Wide -ExcludeProperty A, B | Out-String + $result | Should -Match '3' + $result | Should -Not -Match '1' + $result | Should -Not -Match '2' + } + } } From 2a49561af4c92b90d5ada6f7780a8a3dbdb02d32 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 8 Dec 2025 15:14:07 -0800 Subject: [PATCH 028/112] Update `Microsoft.PowerShell.PSResourceGet` to `v1.2.0-preview5` (#26589) --- src/Modules/PSGalleryModules.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/PSGalleryModules.csproj b/src/Modules/PSGalleryModules.csproj index d2a49e26d2..b6e5506e0b 100644 --- a/src/Modules/PSGalleryModules.csproj +++ b/src/Modules/PSGalleryModules.csproj @@ -13,7 +13,7 @@ - + From 235ff058e71d6caca57d1f06ed49de5fe4cdb223 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Thu, 11 Dec 2025 01:22:36 +0900 Subject: [PATCH 029/112] Centralize ExcludeProperty filter application in ViewGenerator base class (#26574) --- .../common/FormatViewGenerator.cs | 46 +++++-- .../common/FormatViewGenerator_Complex.cs | 10 +- .../common/FormatViewGenerator_List.cs | 34 ++--- .../common/FormatViewGenerator_Table.cs | 124 +++++++++--------- .../common/FormatViewGenerator_Wide.cs | 82 ++++++------ 5 files changed, 150 insertions(+), 146 deletions(-) diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs index cb20e708cc..963b5a0f88 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator.cs @@ -348,19 +348,49 @@ namespace Microsoft.PowerShell.Commands.Internal.Format protected DataBaseInfo dataBaseInfo = new DataBaseInfo(); - protected List activeAssociationList = null; /// - /// Apply ExcludeProperty filter to activeAssociationList if specified. - /// This method filters and updates "activeAssociationList" instance property. + /// Builds the raw association list for the given object. + /// Subclasses override this to provide cmdlet-specific property expansion logic. /// - protected void ApplyExcludePropertyFilter() + /// The object to build the association list for. + /// The list of properties specified by the user, or null if not specified. + /// The raw association list, or null if not applicable. + protected virtual List BuildRawAssociationList(PSObject so, List propertyList) { - if (this.parameters is not null && this.parameters.excludePropertyFilter is not null) + return null; + } + + /// + /// Builds the active association list for the given object, with ExcludeProperty filter applied. + /// + /// The object to build the association list for. + /// The filtered association list. + protected List BuildActiveAssociationList(PSObject so) + { + var propertyList = parameters?.mshParameterList; + var excludeFilter = parameters?.excludePropertyFilter; + var rawList = BuildRawAssociationList(so, propertyList); + return ApplyExcludeFilter(rawList, excludeFilter); + } + + /// + /// Applies the ExcludeProperty filter to the given association list. + /// + /// The list to filter. + /// The exclude filter to apply. + /// The filtered list, or the original list if no filter is specified. + internal static List ApplyExcludeFilter( + List associationList, + PSPropertyExpressionFilter excludeFilter) + { + if (associationList is null || excludeFilter is null) { - this.activeAssociationList = this.activeAssociationList - .Where(item => !this.parameters.excludePropertyFilter.IsMatch(item.ResolvedExpression)) - .ToList(); + return associationList; } + + return associationList + .Where(item => !excludeFilter.IsMatch(item.ResolvedExpression)) + .ToList(); } protected string GetExpressionDisplayValue(PSObject so, int enumerationLimit, PSPropertyExpression ex, diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs index 972a716083..b81c0c0f86 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Complex.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Internal; @@ -514,13 +513,8 @@ namespace Microsoft.PowerShell.Commands.Internal.Format List activeAssociationList = AssociationManager.SetupActiveProperties(parameterList, so, _expressionFactory); - // Apply ExcludeProperty filter if specified - if (_parameters != null && _parameters.excludePropertyFilter != null) - { - activeAssociationList = activeAssociationList - .Where(item => !_parameters.excludePropertyFilter.IsMatch(item.ResolvedExpression)) - .ToList(); - } + // Apply ExcludeProperty filter using the centralized method + activeAssociationList = ViewGenerator.ApplyExcludeFilter(activeAssociationList, _parameters?.excludePropertyFilter); // create a format entry FormatEntry fe = new FormatEntry(); diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs index 6ef1b24ce1..35287d7c2e 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_List.cs @@ -30,9 +30,14 @@ namespace Microsoft.PowerShell.Commands.Internal.Format { _listBody = (ListControlBody)this.dataBaseInfo.view.mainControl; } + } - this.parameters = parameters; - SetUpActiveProperties(so); + /// + /// Builds the raw association list for list formatting. + /// + protected override List BuildRawAssociationList(PSObject so, List propertyList) + { + return AssociationManager.SetupActiveProperties(propertyList, so, this.expressionFactory); } /// @@ -178,17 +183,14 @@ namespace Microsoft.PowerShell.Commands.Internal.Format private ListViewEntry GenerateListViewEntryFromProperties(PSObject so, int enumerationLimit) { - // compute active properties every time - if (this.activeAssociationList == null) - { - SetUpActiveProperties(so); - } + // Build active association list (with ExcludeProperty filter applied) + var associationList = BuildActiveAssociationList(so); ListViewEntry lve = new ListViewEntry(); - for (int k = 0; k < this.activeAssociationList.Count; k++) + for (int k = 0; k < associationList.Count; k++) { - MshResolvedExpressionParameterAssociation a = this.activeAssociationList[k]; + MshResolvedExpressionParameterAssociation a = associationList[k]; ListViewField lvf = new ListViewField(); if (a.OriginatingParameter != null) @@ -218,21 +220,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format lvf.formatPropertyField.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, a.ResolvedExpression, directive); lve.listViewFieldList.Add(lvf); } - - this.activeAssociationList = null; return lve; } - - private void SetUpActiveProperties(PSObject so) - { - List mshParameterList = null; - - if (this.parameters != null) - mshParameterList = this.parameters.mshParameterList; - - this.activeAssociationList = AssociationManager.SetupActiveProperties(mshParameterList, so, this.expressionFactory); - - ApplyExcludePropertyFilter(); - } } } diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs index 33d2bd5350..3b14c0754b 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Table.cs @@ -14,6 +14,8 @@ namespace Microsoft.PowerShell.Commands.Internal.Format // tableBody to use for this instance of the ViewGenerator; private TableControlBody _tableBody; + private List _activeAssociationList; + internal override void Initialize(TerminatingErrorContext terminatingErrorContext, PSPropertyExpressionFactory mshExpressionFactory, TypeInfoDataBase db, ViewDefinition view, FormattingCommandLineParameters formatParameters) { base.Initialize(terminatingErrorContext, mshExpressionFactory, db, view, formatParameters); @@ -34,51 +36,48 @@ namespace Microsoft.PowerShell.Commands.Internal.Format _tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl; } - List rawMshParameterList = null; - - if (parameters != null) - rawMshParameterList = parameters.mshParameterList; + // Build the active association list (with ExcludeProperty filter applied) + _activeAssociationList = BuildActiveAssociationList(so); + } + /// + /// Builds the raw association list for table formatting. + /// + protected override List BuildRawAssociationList(PSObject so, List propertyList) + { // check if we received properties from the command line - if (rawMshParameterList is not null && rawMshParameterList.Count > 0) + if (propertyList is not null && propertyList.Count > 0) { - this.activeAssociationList = AssociationManager.ExpandTableParameters(rawMshParameterList, so); - } - else - { - // we did not get any properties: - // try to get properties from the default property set of the object - this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); - if (this.activeAssociationList.Count > 0) - { - // we got a valid set of properties from the default property set..add computername for - // remoteobjects (if available) - if (PSObjectHelper.ShouldShowComputerNameProperty(so)) - { - activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, - new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty))); - } - } - else - { - // we failed to get anything from the default property set - this.activeAssociationList = AssociationManager.ExpandAll(so); - if (this.activeAssociationList.Count > 0) - { - // Remove PSComputerName and PSShowComputerName from the display as needed. - AssociationManager.HandleComputerNameProperties(so, activeAssociationList); - FilterActiveAssociationList(); - } - else - { - // we were unable to retrieve any properties, so we leave an empty list - this.activeAssociationList = new List(); - return; - } - } + return AssociationManager.ExpandTableParameters(propertyList, so); } - ApplyExcludePropertyFilter(); + // we did not get any properties: + // try to get properties from the default property set of the object + var list = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); + if (list.Count > 0) + { + // we got a valid set of properties from the default property set..add computername for + // remoteobjects (if available) + if (PSObjectHelper.ShouldShowComputerNameProperty(so)) + { + list.Add(new MshResolvedExpressionParameterAssociation(null, + new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty))); + } + + return list; + } + + // we failed to get anything from the default property set + list = AssociationManager.ExpandAll(so); + if (list.Count > 0) + { + // Remove PSComputerName and PSShowComputerName from the display as needed. + AssociationManager.HandleComputerNameProperties(so, list); + return LimitAssociationListSize(list); + } + + // we were unable to retrieve any properties, so we leave an empty list + return new List(); } /// @@ -129,30 +128,29 @@ namespace Microsoft.PowerShell.Commands.Internal.Format } /// - /// Method to filter resolved expressions as per table view needs. + /// Limits the association list size for table view. /// For v1.0, table view supports only 10 properties. - /// - /// This method filters and updates "activeAssociationList" instance property. /// - /// None. - /// This method updates "activeAssociationList" instance property. - private void FilterActiveAssociationList() + /// The list to limit. + /// The limited list. + private static List LimitAssociationListSize( + List list) { - // we got a valid set of properties from the default property set - // make sure we do not have too many properties - // NOTE: this is an arbitrary number, chosen to be a sensitive default - const int nMax = 10; + const int maxCount = 10; - if (activeAssociationList.Count > nMax) + if (list.Count <= maxCount) { - List tmp = this.activeAssociationList; - this.activeAssociationList = new List(); - for (int k = 0; k < nMax; k++) - this.activeAssociationList.Add(tmp[k]); + return list; } - return; + var result = new List(maxCount); + for (int k = 0; k < maxCount; k++) + { + result.Add(list[k]); + } + + return result; } private TableHeaderInfo GenerateTableHeaderInfoFromDataBaseInfo(PSObject so) @@ -228,9 +226,9 @@ namespace Microsoft.PowerShell.Commands.Internal.Format thi.hideHeader = this.HideHeaders; thi.repeatHeader = this.RepeatHeader; - for (int k = 0; k < this.activeAssociationList.Count; k++) + for (int k = 0; k < _activeAssociationList.Count; k++) { - MshResolvedExpressionParameterAssociation a = this.activeAssociationList[k]; + MshResolvedExpressionParameterAssociation a = _activeAssociationList[k]; TableColumnInfo ci = new TableColumnInfo(); // set the label of the column @@ -241,7 +239,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format ci.propertyName = (string)key; } - ci.propertyName ??= this.activeAssociationList[k].ResolvedExpression.ToString(); + ci.propertyName ??= _activeAssociationList[k].ResolvedExpression.ToString(); // set the width of the table if (a.OriginatingParameter != null) @@ -473,13 +471,13 @@ namespace Microsoft.PowerShell.Commands.Internal.Format private TableRowEntry GenerateTableRowEntryFromFromProperties(PSObject so, int enumerationLimit) { TableRowEntry tre = new TableRowEntry(); - for (int k = 0; k < this.activeAssociationList.Count; k++) + for (int k = 0; k < _activeAssociationList.Count; k++) { FormatPropertyField fpf = new FormatPropertyField(); FieldFormattingDirective directive = null; - if (activeAssociationList[k].OriginatingParameter != null) + if (_activeAssociationList[k].OriginatingParameter != null) { - directive = activeAssociationList[k].OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.FormatStringEntryKey) as FieldFormattingDirective; + directive = _activeAssociationList[k].OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.FormatStringEntryKey) as FieldFormattingDirective; } if (directive is null) @@ -488,7 +486,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format directive.isTable = true; } - fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, this.activeAssociationList[k].ResolvedExpression, directive); + fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, _activeAssociationList[k].ResolvedExpression, directive); tre.formatPropertyFieldList.Add(fpf); } diff --git a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs index a45c006e7f..bfa364cc45 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/FormatViewGenerator_Wide.cs @@ -15,6 +15,40 @@ namespace Microsoft.PowerShell.Commands.Internal.Format base.Initialize(errorContext, expressionFactory, so, db, parameters); } + /// + /// Builds the raw association list for wide formatting. + /// + protected override List BuildRawAssociationList(PSObject so, List propertyList) + { + // check if we received properties from the command line + if (propertyList is not null && propertyList.Count > 0) + { + return AssociationManager.ExpandParameters(propertyList, so); + } + + // we did not get any properties: + // try to get the display property of the object + PSPropertyExpression displayNameExpression = PSObjectHelper.GetDisplayNameExpression(so, this.expressionFactory); + if (displayNameExpression is not null) + { + return new List + { + new MshResolvedExpressionParameterAssociation(null, displayNameExpression) + }; + } + + // try to get the default property set (we will use the first property) + var list = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); + if (list.Count == 0) + { + // we failed to get anything from the default property set + // just get all the properties + list = AssociationManager.ExpandAll(so); + } + + return list; + } + internal override FormatStartData GenerateStartData(PSObject so) { FormatStartData startFormat = base.GenerateStartData(so); @@ -129,20 +163,17 @@ namespace Microsoft.PowerShell.Commands.Internal.Format private WideViewEntry GenerateWideViewEntryFromProperties(PSObject so, int enumerationLimit) { - // compute active properties every time - if (this.activeAssociationList == null) - { - SetUpActiveProperty(so); - } + // Build active association list (with ExcludeProperty filter applied) + var associationList = BuildActiveAssociationList(so); WideViewEntry wve = new WideViewEntry(); FormatPropertyField fpf = new FormatPropertyField(); wve.formatPropertyField = fpf; - if (this.activeAssociationList.Count > 0) + if (associationList.Count > 0) { // get the first one - MshResolvedExpressionParameterAssociation a = this.activeAssociationList[0]; + MshResolvedExpressionParameterAssociation a = associationList[0]; FieldFormattingDirective directive = null; if (a.OriginatingParameter != null) { @@ -151,44 +182,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, a.ResolvedExpression, directive); } - - this.activeAssociationList = null; return wve; } - - private void SetUpActiveProperty(PSObject so) - { - List rawMshParameterList = this.parameters?.mshParameterList; - - // check if we received properties from the command line - if (rawMshParameterList is not null && rawMshParameterList.Count > 0) - { - this.activeAssociationList = AssociationManager.ExpandParameters(rawMshParameterList, so); - } - else - { - // we did not get any properties: - // try to get the display property of the object - PSPropertyExpression displayNameExpression = PSObjectHelper.GetDisplayNameExpression(so, this.expressionFactory); - if (displayNameExpression is not null) - { - this.activeAssociationList = new List(); - this.activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, displayNameExpression)); - } - else - { - // try to get the default property set (we will use the first property) - this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); - if (this.activeAssociationList.Count == 0) - { - // we failed to get anything from the default property set - // just get all the properties - this.activeAssociationList = AssociationManager.ExpandAll(so); - } - } - } - - ApplyExcludePropertyFilter(); - } } } From 7f92d588fa02d58e31ba7c86840f3f1c94bdc466 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Thu, 11 Dec 2025 03:21:32 +0900 Subject: [PATCH 030/112] Add ToRegex method to WildcardPattern class (#26515) --- .../engine/regex.cs | 41 +++++++--- .../engine/WildcardPattern.Tests.ps1 | 77 +++++++++++++++++++ 2 files changed, 106 insertions(+), 12 deletions(-) create mode 100644 test/powershell/engine/WildcardPattern.Tests.ps1 diff --git a/src/System.Management.Automation/engine/regex.cs b/src/System.Management.Automation/engine/regex.cs index d69f3bdb88..6e0ef9741d 100644 --- a/src/System.Management.Automation/engine/regex.cs +++ b/src/System.Management.Automation/engine/regex.cs @@ -73,18 +73,6 @@ namespace System.Management.Automation // Default is WildcardOptions.None. internal WildcardOptions Options { get; } - /// - /// Wildcard pattern converted to regex pattern. - /// - internal string PatternConvertedToRegex - { - get - { - var patternRegex = WildcardPatternToRegexParser.Parse(this); - return patternRegex.ToString(); - } - } - /// /// Initializes and instance of the WildcardPattern class /// for the specified wildcard pattern. @@ -205,6 +193,35 @@ namespace System.Management.Automation return input != null && _isMatch(input); } + /// + /// Converts the wildcard pattern to its regular expression equivalent. + /// + /// + /// A object that represents the regular expression equivalent of the wildcard pattern. + /// The regex is configured with options matching the wildcard pattern's options. + /// + /// + /// This method converts a wildcard pattern to a regular expression. + /// The conversion follows these rules: + /// + /// * (asterisk) converts to .* (matches any string) + /// ? (question mark) converts to . (matches any single character) + /// [abc] (bracket expression) converts to [abc] (matches any character in the set) + /// Literal characters are escaped as needed for regex + /// + /// + /// + /// + /// var pattern = new WildcardPattern("*.txt"); + /// Regex regex = pattern.ToRegex(); + /// // regex.ToString() returns: "\.txt$" + /// + /// + public Regex ToRegex() + { + return WildcardPatternToRegexParser.Parse(this); + } + /// /// Escape special chars, except for those specified in , in a string by replacing them with their escape codes. /// diff --git a/test/powershell/engine/WildcardPattern.Tests.ps1 b/test/powershell/engine/WildcardPattern.Tests.ps1 new file mode 100644 index 0000000000..81ced10f25 --- /dev/null +++ b/test/powershell/engine/WildcardPattern.Tests.ps1 @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "WildcardPattern.ToRegex Tests" -Tags "CI" { + It "Converts '' to regex pattern ''" -TestCases @( + @{ Pattern = '*.txt'; Expected = '\.txt$' } + @{ Pattern = 'test?.log'; Expected = '^test.\.log$' } + @{ Pattern = 'file[0-9].txt'; Expected = '^file[0-9]\.txt$' } + @{ Pattern = 'test.log'; Expected = '^test\.log$' } + @{ Pattern = '*test*file*.txt'; Expected = 'test.*file.*\.txt$' } + @{ Pattern = 'file[0-9][a-z].txt'; Expected = '^file[0-9][a-z]\.txt$' } + @{ Pattern = 'test*'; Expected = '^test' } + @{ Pattern = '*test*'; Expected = 'test' } + ) { + param($Pattern, $Expected) + $wildcardPattern = [System.Management.Automation.WildcardPattern]::new($Pattern) + $regex = $wildcardPattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly $Expected + } + + It "Converts '' with option" -TestCases @( + @{ Pattern = 'TEST'; OptionName = 'IgnoreCase'; Option = [System.Management.Automation.WildcardOptions]::IgnoreCase; Expected = '^TEST$'; ExpectedRegexOptions = 'IgnoreCase, Singleline'; TestString = 'test'; ExpectedMatch = $true } + @{ Pattern = 'test'; OptionName = 'CultureInvariant'; Option = [System.Management.Automation.WildcardOptions]::CultureInvariant; Expected = '^test$'; ExpectedRegexOptions = 'Singleline, CultureInvariant'; TestString = 'test'; ExpectedMatch = $true } + @{ Pattern = 'test*'; OptionName = 'Compiled'; Option = [System.Management.Automation.WildcardOptions]::Compiled; Expected = '^test'; ExpectedRegexOptions = 'Compiled, Singleline'; TestString = 'testing'; ExpectedMatch = $true } + ) { + param($Pattern, $OptionName, $Option, $Expected, $ExpectedRegexOptions, $TestString, $ExpectedMatch) + $wildcardPattern = [System.Management.Automation.WildcardPattern]::new($Pattern, $Option) + $regex = $wildcardPattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly $Expected + $regex.Options.ToString() | Should -BeExactly $ExpectedRegexOptions + $regex.IsMatch($TestString) | Should -Be $ExpectedMatch + } + + It "Regex from '' matches '': " -TestCases @( + @{ Pattern = '*test*file*.txt'; TestString = 'mytestmyfile123.txt'; ShouldMatch = $true } + @{ Pattern = 'file[0-9][a-z].txt'; TestString = 'file5a.txt'; ShouldMatch = $true } + @{ Pattern = 'file[0-9][a-z].txt'; TestString = 'file55.txt'; ShouldMatch = $false } + ) { + param($Pattern, $TestString, $ShouldMatch) + $regex = [System.Management.Automation.WildcardPattern]::new($Pattern).ToRegex() + $regex.IsMatch($TestString) | Should -Be $ShouldMatch + } + + Context "Edge cases" { + It "Handles empty pattern" { + $pattern = [System.Management.Automation.WildcardPattern]::new("") + $regex = $pattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -Be "^$" + } + + It "Handles pattern with only asterisk" { + $pattern = [System.Management.Automation.WildcardPattern]::new("*") + $regex = $pattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly "" + $regex.IsMatch("anything") | Should -BeTrue + $regex.IsMatch("") | Should -BeTrue + } + + It "Handles escaped '' wildcard character" -TestCases @( + @{ Char = '*'; Pattern = 'file`*.txt'; Expected = '^file\*\.txt$' } + @{ Char = '?'; Pattern = 'file`?.txt'; Expected = '^file\?\.txt$' } + @{ Char = '['; Pattern = 'file`[.txt'; Expected = '^file\[\.txt$' } + @{ Char = ']'; Pattern = 'file`].txt'; Expected = '^file]\.txt$' } + ) { + param($Char, $Pattern, $Expected) + $wildcardPattern = [System.Management.Automation.WildcardPattern]::new($Pattern) + $regex = $wildcardPattern.ToRegex() + $regex | Should -BeOfType ([regex]) + $regex.ToString() | Should -BeExactly $Expected + } + + } +} From 06ffbaeb443b0420052f31681de076216d73a84f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 15:58:06 -0800 Subject: [PATCH 031/112] Bump github/codeql-action from 4.31.6 to 4.31.7 (#26586) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 21bc1ed1e9..55275cbcc3 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 + uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 + uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 3c322397db..f6ca3ccac1 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v3.29.5 + uses: github/codeql-action/upload-sarif@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 with: sarif_file: results.sarif From 2e7765eef17ee274925a97efab1b9f52973eeb59 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 12 Dec 2025 09:26:46 +0500 Subject: [PATCH 032/112] Fix $PSDefaultParameterValues leak causing tests to skip unexpectedly (#26602) This fixes test isolation issues where six test files were leaking $PSDefaultParameterValues["It:Skip"] settings to subsequent tests, causing widespread test skipping on Linux. The fixes address two distinct patterns: using .Remove() which doesn't restore the original state (4 files), and imbalanced Push-DefaultParameterValueStack/Pop-DefaultParameterValueStack calls (2 files). --- .../Microsoft.PowerShell.Management/Rename-Computer.Tests.ps1 | 3 ++- .../Microsoft.PowerShell.Security/AclCmdlets.Tests.ps1 | 3 ++- test/powershell/engine/ETS/CimAdapter.Tests.ps1 | 4 +++- test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1 | 2 -- test/powershell/engine/Remoting/SessionOption.Tests.ps1 | 3 ++- test/powershell/engine/Security/FileSignature.Tests.ps1 | 1 + 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Rename-Computer.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Rename-Computer.Tests.ps1 index 82f944612f..f1937f824a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Rename-Computer.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Rename-Computer.Tests.ps1 @@ -7,6 +7,7 @@ $DefaultResultValue = 0 try { # set up for testing + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() $PSDefaultParameterValues["it:skip"] = ! $IsWindows Enable-Testhook -testhookName $RenameTesthook # we also set TestStopComputer @@ -73,7 +74,7 @@ try } finally { - $PSDefaultParameterValues.Remove("it:skip") + $global:PSDefaultParameterValues = $originalDefaultParameterValues Disable-Testhook -testhookName $RenameTestHook Disable-Testhook -testhookName TestStopComputer Set-TesthookResult -testhookName $RenameResultName -value 0 diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/AclCmdlets.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Security/AclCmdlets.Tests.ps1 index 0e50d7c360..52938a0b88 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Security/AclCmdlets.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Security/AclCmdlets.Tests.ps1 @@ -3,6 +3,7 @@ Describe "Acl cmdlets are available and operate properly" -Tag CI { Context "Windows ACL test" { BeforeAll { + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() $PSDefaultParameterValues["It:Skip"] = -not $IsWindows } @@ -103,7 +104,7 @@ Describe "Acl cmdlets are available and operate properly" -Tag CI { } AfterAll { - $PSDefaultParameterValues.Remove("It:Skip") + $global:PSDefaultParameterValues = $originalDefaultParameterValues } } } diff --git a/test/powershell/engine/ETS/CimAdapter.Tests.ps1 b/test/powershell/engine/ETS/CimAdapter.Tests.ps1 index 1e71bc28b9..3cfff4bd9e 100644 --- a/test/powershell/engine/ETS/CimAdapter.Tests.ps1 +++ b/test/powershell/engine/ETS/CimAdapter.Tests.ps1 @@ -3,6 +3,8 @@ Describe "CIM Objects are adapted properly" -Tag @("CI") { BeforeAll { + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() + function getIndex { param([string[]]$strings,[string]$pattern) @@ -32,7 +34,7 @@ Describe "CIM Objects are adapted properly" -Tag @("CI") { } } AfterAll { - $PSDefaultParameterValues.Remove("it:pending") + $global:PSDefaultParameterValues = $originalDefaultParameterValues } It "Namespace-qualified Win32_Process is present" -Skip:(!$IsWindows) { diff --git a/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1 b/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1 index a5d49b7500..2093eaa7ce 100644 --- a/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1 +++ b/test/powershell/engine/Remoting/RemoteSession.Basic.Tests.ps1 @@ -83,9 +83,7 @@ Describe "JEA session Transcript script test" -Tag @("Feature", 'RequireAdminOnW AfterAll { if ($skipTest) { Pop-DefaultParameterValueStack - return } - Pop-DefaultParameterValueStack } It "Configuration name should be in the transcript header" { diff --git a/test/powershell/engine/Remoting/SessionOption.Tests.ps1 b/test/powershell/engine/Remoting/SessionOption.Tests.ps1 index 1ff51e2a70..e85777e90b 100644 --- a/test/powershell/engine/Remoting/SessionOption.Tests.ps1 +++ b/test/powershell/engine/Remoting/SessionOption.Tests.ps1 @@ -1,6 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. try { + $originalDefaultParameterValues = $PSDefaultParameterValues.Clone() if ( ! $IsWindows ) { $PSDefaultParameterValues['it:skip'] = $true } @@ -52,5 +53,5 @@ try { } } finally { - $PSDefaultParameterValues.remove("it:skip") + $global:PSDefaultParameterValues = $originalDefaultParameterValues } diff --git a/test/powershell/engine/Security/FileSignature.Tests.ps1 b/test/powershell/engine/Security/FileSignature.Tests.ps1 index 51194903a9..9dd26f98ae 100644 --- a/test/powershell/engine/Security/FileSignature.Tests.ps1 +++ b/test/powershell/engine/Security/FileSignature.Tests.ps1 @@ -89,6 +89,7 @@ Describe "Windows file content signatures" -Tags @('Feature', 'RequireAdminOnWin AfterAll { if ($shouldSkip) { + Pop-DefaultParameterValueStack return } From 592668bd0b0e46e370e9ac1ad4b8f5420efb51ae Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 12 Dec 2025 09:43:08 -0800 Subject: [PATCH 033/112] Refactor the module path construction code to make it more robust and easier to maintain (#26565) --- .../host/msh/ConsoleHost.cs | 21 +- .../engine/Modules/ModuleIntrinsics.cs | 181 ++++++++---------- test/powershell/Host/ConsoleHost.Tests.ps1 | 27 +++ 3 files changed, 125 insertions(+), 104 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs index 5f59540ecf..2d9902d176 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs @@ -2201,7 +2201,6 @@ namespace Microsoft.PowerShell if (e1 != null) { // that didn't work. Write out the error ourselves as a last resort. - ReportExceptionFallback(e, null); } } @@ -2222,7 +2221,7 @@ namespace Microsoft.PowerShell Console.Error.WriteLine(header); } - if (e == null) + if (e is null) { return; } @@ -2230,7 +2229,9 @@ namespace Microsoft.PowerShell // See if the exception has an error record attached to it... ErrorRecord er = null; if (e is IContainsErrorRecord icer) + { er = icer.ErrorRecord; + } if (e is PSRemotingTransportException) { @@ -2247,8 +2248,22 @@ namespace Microsoft.PowerShell } // Add the position message for the error if it's available. - if (er != null && er.InvocationInfo != null) + if (er?.InvocationInfo is { }) + { Console.Error.WriteLine(er.InvocationInfo.PositionMessage); + } + + // Print the stack trace. + Console.Error.WriteLine($"\n--- {e.GetType().FullName} ---"); + Console.Error.WriteLine(e.StackTrace); + + Exception inner = e.InnerException; + while (inner is { }) + { + Console.Error.WriteLine($"--- inner {inner.GetType().FullName} ---"); + Console.Error.WriteLine(inner.StackTrace); + inner = inner.InnerException; + } } /// diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index b687e50276..301ee8c3b4 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -10,9 +10,7 @@ using System.Management.Automation.Internal; using System.Management.Automation.Language; using System.Text; using System.Threading; - using Microsoft.PowerShell.Commands; -using Microsoft.Win32; using Dbg = System.Management.Automation.Diagnostics; @@ -39,18 +37,22 @@ namespace System.Management.Automation private static readonly string s_windowsPowerShellPSHomeModulePath = Path.Combine(System.Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "Modules"); + static ModuleIntrinsics() + { + // Initialize the module path. + SetModulePath(); + } + internal ModuleIntrinsics(ExecutionContext context) { _context = context; - - // And initialize the module path... - SetModulePath(); + ModuleTable = new Dictionary(StringComparer.OrdinalIgnoreCase); } private readonly ExecutionContext _context; // Holds the module collection... - internal Dictionary ModuleTable { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + internal Dictionary ModuleTable { get; } private const int MaxModuleNestingDepth = 10; @@ -176,11 +178,9 @@ namespace System.Management.Automation sb.SessionState = ss; } - else + else if (moduleCode is string sbText) { - var sbText = moduleCode as string; - if (sbText != null) - sb = ScriptBlock.Create(_context, sbText); + sb = ScriptBlock.Create(_context, sbText); } } @@ -1082,91 +1082,80 @@ namespace System.Management.Automation return result; } - /// - /// Checks if a particular string (path) is a member of 'combined path' string (like %Path% or %PSModulePath%) - /// - /// 'Combined path' string to analyze; can not be null. - /// Path to search for; can not be another 'combined path' (semicolon-separated); can not be null. - /// Index of pathToLookFor in pathToScan; -1 if not found. - private static int PathContainsSubstring(string pathToScan, string pathToLookFor) - { - // we don't support if any of the args are null - parent function should ensure this; empty values are ok - Diagnostics.Assert(pathToScan != null, "pathToScan should not be null according to contract of the function"); - Diagnostics.Assert(pathToLookFor != null, "pathToLookFor should not be null according to contract of the function"); - - int pos = 0; // position of the current substring in pathToScan - string[] substrings = pathToScan.Split(Path.PathSeparator, StringSplitOptions.None); // we want to process empty entries - string goodPathToLookFor = pathToLookFor.Trim().TrimEnd(Path.DirectorySeparatorChar); // trailing backslashes and white-spaces will mess up equality comparison - foreach (string substring in substrings) - { - string goodSubstring = substring.Trim().TrimEnd(Path.DirectorySeparatorChar); // trailing backslashes and white-spaces will mess up equality comparison - - // We have to use equality comparison on individual substrings (as opposed to simple 'string.IndexOf' or 'string.Contains') - // because of cases like { pathToScan="C:\Temp\MyDir\MyModuleDir", pathToLookFor="C:\Temp" } - - if (string.Equals(goodSubstring, goodPathToLookFor, StringComparison.OrdinalIgnoreCase)) - { - return pos; // match found - return index of it in the 'pathToScan' string - } - else - { - pos += substring.Length + 1; // '1' is for trailing semicolon - } - } - // if we are here, that means a match was not found - return -1; - } - /// /// Adds paths to a 'combined path' string (like %Path% or %PSModulePath%) if they are not already there. /// /// Path string (like %Path% or %PSModulePath%). - /// Collection of individual paths to add. + /// An individual path to add, or multiple paths separated by the path separator character. /// -1 to append to the end; 0 to insert in the beginning of the string; etc... /// Result string. - private static string AddToPath(string basePath, string pathToAdd, int insertPosition) + private static string UpdatePath(string basePath, string pathToAdd, ref int insertPosition) { // we don't support if any of the args are null - parent function should ensure this; empty values are ok - Diagnostics.Assert(basePath != null, "basePath should not be null according to contract of the function"); - Diagnostics.Assert(pathToAdd != null, "pathToAdd should not be null according to contract of the function"); + Dbg.Assert(basePath != null, "basePath should not be null according to contract of the function"); + Dbg.Assert(pathToAdd != null, "pathToAdd should not be null according to contract of the function"); - StringBuilder result = new StringBuilder(basePath); + // The 'pathToAdd' could be a 'combined path' (path-separator-separated). + string[] newPaths = pathToAdd.Split( + Path.PathSeparator, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); - if (!string.IsNullOrEmpty(pathToAdd)) // we don't want to append empty paths + if (newPaths.Length is 0) { - foreach (string subPathToAdd in pathToAdd.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries)) // in case pathToAdd is a 'combined path' (semicolon-separated) - { - int position = PathContainsSubstring(result.ToString(), subPathToAdd); // searching in effective 'result' value ensures that possible duplicates in pathsToAdd are handled correctly - if (position == -1) // subPathToAdd not found - add it - { - if (insertPosition == -1 || insertPosition > basePath.Length) // append subPathToAdd to the end - { - bool endsWithPathSeparator = false; - if (result.Length > 0) - { - endsWithPathSeparator = (result[result.Length - 1] == Path.PathSeparator); - } + // The 'pathToAdd' doesn't really contain any paths to add. + return basePath; + } - if (endsWithPathSeparator) - { - result.Append(subPathToAdd); - } - else - { - result.Append(Path.PathSeparator + subPathToAdd); - } - } - else if (insertPosition > result.Length) - { - // handle case where path is a singleton with no path separator already - result.Append(Path.PathSeparator).Append(subPathToAdd); - } - else // insert at the requested location (this is used by DSC ( location) and by 'user-specific location' (SpecialFolder.MyDocuments or EVT.User)) - { - result.Insert(insertPosition, subPathToAdd + Path.PathSeparator); - } - } + var result = new StringBuilder(basePath, capacity: basePath.Length + pathToAdd.Length + newPaths.Length); + var addedPaths = new HashSet(StringComparer.OrdinalIgnoreCase); + string[] initialPaths = basePath.Split( + Path.PathSeparator, + StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + foreach (string p in initialPaths) + { + // Remove the trailing directory separators. + // Trailing white spaces were already removed by 'StringSplitOptions.TrimEntries'. + addedPaths.Add(Path.TrimEndingDirectorySeparator(p)); + } + + foreach (string subPathToAdd in newPaths) + { + // Remove the trailing directory separators. + // Trailing white spaces were already removed by 'StringSplitOptions.TrimEntries'. + string normalizedPath = Path.TrimEndingDirectorySeparator(subPathToAdd); + if (addedPaths.Contains(normalizedPath)) + { + // The normalized sub path was already added - skip it. + continue; } + + // The normalized sub path was not found - add it. + if (insertPosition is -1 || insertPosition >= result.Length) + { + // Append the normalized sub path to the end. + if (result.Length > 0 && result[^1] != Path.PathSeparator) + { + result.Append(Path.PathSeparator); + } + + result.Append(normalizedPath); + // Next insertion should happen at the end. + insertPosition = result.Length; + } + else + { + // Insert at the requested location. + // This is used by the user-specific module path, the shared module path ( location), and the PSHome module path. + string strToInsert = normalizedPath + Path.PathSeparator; + result.Insert(insertPosition, strToInsert); + + // Next insertion should happen after the just inserted string. + insertPosition += strToInsert.Length; + } + + // Add it to the set. + addedPaths.Add(normalizedPath); } return result.ToString(); @@ -1218,7 +1207,7 @@ namespace System.Management.Automation string psHomeModulePath = GetPSHomeModulePath(); // $PSHome\Modules location // If the variable isn't set, then set it to the default value - if (currentProcessModulePath == null) // EVT.Process does Not exist - really corner case + if (string.IsNullOrEmpty(currentProcessModulePath)) // EVT.Process does Not exist - really corner case { // Handle the default case... if (string.IsNullOrEmpty(hkcuUserModulePath)) // EVT.User does Not exist -> set to location @@ -1270,21 +1259,6 @@ namespace System.Management.Automation return currentProcessModulePath; } - private static string UpdatePath(string path, string pathToAdd, ref int insertIndex) - { - if (!string.IsNullOrEmpty(pathToAdd)) - { - path = AddToPath(path, pathToAdd, insertIndex); - insertIndex = path.IndexOf(Path.PathSeparator, PathContainsSubstring(path, pathToAdd)); - if (insertIndex != -1) - { - // advance past the path separator - insertIndex++; - } - } - return path; - } - /// /// Checks if $env:PSModulePath is not set and sets it as appropriate. Note - because these /// strings go through the provider, we need to escape any wildcards before passing them @@ -1358,11 +1332,16 @@ namespace System.Management.Automation { string currentModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Process); #if !UNIX - // if the current process and user env vars are the same, it means we need to append the machine one as it's incomplete - // otherwise, the user modified it and we should use the process one + // if the current process and user env vars are the same, it means we need to append the machine one as it's incomplete. + // Otherwise, the user modified it and we should use the process one. if (string.CompareOrdinal(GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.User), currentModulePath) == 0) { - currentModulePath = currentModulePath + Path.PathSeparator + GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Machine); + string machineScopeValue = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Machine); + currentModulePath = string.IsNullOrEmpty(currentModulePath) + ? machineScopeValue + : string.IsNullOrEmpty(machineScopeValue) + ? currentModulePath + : string.Concat(currentModulePath, Path.PathSeparator, machineScopeValue); } #endif string allUsersModulePath = PowerShellConfig.Instance.GetModulePath(ConfigScope.AllUsers); diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index ee501a83b6..534cce2d35 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -386,6 +386,33 @@ export $envVarName='$guid' } } + Context "-SettingsFile Commandline switch set 'PSModulePath'" { + + BeforeAll { + $CustomSettingsFile = Join-Path -Path $TestDrive -ChildPath 'powershell.test.json' + $mPath1 = Join-Path $PSHOME 'Modules' + $mPath2 = Join-Path $TestDrive 'NonExist' + $pathSep = [System.IO.Path]::PathSeparator + + ## Use multiple paths in the setting. + $ModulePath = "${mPath1}${pathSep}${mPath2}".Replace('\', "\\") + Set-Content -Path $CustomSettingsfile -Value "{`"Microsoft.PowerShell:ExecutionPolicy`":`"Unrestricted`", `"PSModulePath`": `"$ModulePath`" }" -ErrorAction Stop + } + + It "Verify PowerShell PSModulePath should contain paths from config file" { + $psModulePath = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command '$env:PSModulePath' + + ## $mPath1 already exists in the value of env PSModulePath, so it won't be added again. + $index = $psModulePath.IndexOf("${mPath1}${pathSep}", [System.StringComparison]::OrdinalIgnoreCase) + $index | Should -BeGreaterThan 0 + $index += $mPath1.Length + $psModulePath.IndexOf($mPath1, $index, [System.StringComparison]::OrdinalIgnoreCase) | Should -BeExactly -1 + + ## $mPath2 should be added at the index position 0. + $psModulePath.StartsWith("${mPath2}${pathSep}", [System.StringComparison]::OrdinalIgnoreCase) | Should -BeTrue + } + } + Context "Pipe to/from powershell" { BeforeAll { if ($null -ne $PSStyle) { From 15d0a2841379462b159c9203fb4849b03a0f3d4e Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 12 Dec 2025 10:40:16 -0800 Subject: [PATCH 034/112] Use consistent indentation in the file `HelpersCommon.psm1` (#26608) --- .../Modules/HelpersCommon/HelpersCommon.psm1 | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 index 5c05198ff8..02588b612d 100644 --- a/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 +++ b/test/tools/Modules/HelpersCommon/HelpersCommon.psm1 @@ -69,7 +69,7 @@ function Get-RandomFileName $SCRIPT:TesthookType = [system.management.automation.internal.internaltesthooks] function Test-TesthookIsSet { - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", '')] # , Justification = "an error message is not appropriate for this function")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", '')] # , Justification = "an error message is not appropriate for this function")] param ( [ValidateNotNullOrEmpty()] [Parameter(Mandatory=$true)] @@ -197,7 +197,7 @@ public class TestDynamic : DynamicObject # Upload an artifact in VSTS # On other systems will just log where the file was placed function Send-VstsLogFile { - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = "needed for VSO")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = "needed for VSO")] param ( [parameter(Mandatory,ParameterSetName='contents')] [string[]] @@ -324,8 +324,8 @@ function New-RandomHexString $script:CanWriteToPsHome = $null function Test-CanWriteToPsHome { - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = "an error message is not appropriate for this function")] - param () + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = "an error message is not appropriate for this function")] + param () if ($null -ne $script:CanWriteToPsHome) { return $script:CanWriteToPsHome } @@ -476,20 +476,20 @@ function Test-IsWindows2016 { # Ensure that the global:PSDefaultParameterValues variable is a hashtable function Initialize-PSDefaultParameterValue { - if ( $global:PSDefaultParameterValues -isnot [hashtable] ) { - $global:PSDefaultParameterValues = @{} - } + if ( $global:PSDefaultParameterValues -isnot [hashtable] ) { + $global:PSDefaultParameterValues = @{} + } } # reset the stack function Reset-DefaultParameterValueStack { - $script:DefaultParameterValueStack = [system.collections.generic.Stack[hashtable]]::new() + $script:DefaultParameterValueStack = [system.collections.generic.Stack[hashtable]]::new() Initialize-PSDefaultParameterValue } # return the current stack function Get-DefaultParameterValueStack { - $script:DefaultParameterValueStack + $script:DefaultParameterValueStack } # PSDefaultParameterValue may not have both skip and pending keys @@ -507,35 +507,35 @@ function Test-PSDefaultParameterValue { # if $ht is null, then the current value of $global:PSDefaultParameterValues is pushed # if $NewValue is used, then $ht is used as the new value of $global:PSDefaultParameterValues function Push-DefaultParameterValueStack { - param ([hashtable]$ht, [switch]$NewValue) + param ([hashtable]$ht, [switch]$NewValue) Initialize-PSDefaultParameterValue - $script:DefaultParameterValueStack.Push($global:PSDefaultParameterValues.Clone()) - if ( $ht ) { - if ( $NewValue ) { - $global:PSDefaultParameterValues = $ht - } - else { - foreach ($k in $ht.Keys) { - $global:PSDefaultParameterValues[$k] = $ht[$k] - } - } + $script:DefaultParameterValueStack.Push($global:PSDefaultParameterValues.Clone()) + if ( $ht ) { + if ( $NewValue ) { + $global:PSDefaultParameterValues = $ht + } + else { + foreach ($k in $ht.Keys) { + $global:PSDefaultParameterValues[$k] = $ht[$k] + } + } if ( ! (Test-PSDefaultParameterValue)) { Write-Warning -Message "PSDefaultParameterValues may not have both skip and pending keys, resetting." Pop-DefaultParameterValueStack } - } + } } function Pop-DefaultParameterValueStack { - try { - $global:PSDefaultParameterValues = $script:DefaultParameterValueStack.Pop() - return $true - } - catch { + try { + $global:PSDefaultParameterValues = $script:DefaultParameterValueStack.Pop() + return $true + } + catch { Initialize-PSDefaultParameterValue - return $false - } + return $false + } } function Get-HelpNetworkTestCases From 084ae755c6f154b8b2721c05ad16329fc913cedb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:58:53 -0800 Subject: [PATCH 035/112] Bump actions/upload-artifact from 5 to 6 (#26616) --- .github/workflows/macos-ci.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/windows-packaging-reusable.yml | 2 +- .github/workflows/xunit-tests.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 60596c75ed..8f15a2f3a6 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -228,7 +228,7 @@ jobs: testResultsFolder: "${{ runner.workspace }}/testResults" - name: Upload package artifact if: always() - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: macos-package path: "*.pkg" diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index f6ca3ccac1..1cc99ace9a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index f3a46cae90..bb4873adeb 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -81,7 +81,7 @@ jobs: - name: Upload Build Artifacts if: always() - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 with: name: windows-packaging-${{ matrix.architecture }}-${{ matrix.channel }} path: | diff --git a/.github/workflows/xunit-tests.yml b/.github/workflows/xunit-tests.yml index 940e1cb839..5d225446cb 100644 --- a/.github/workflows/xunit-tests.yml +++ b/.github/workflows/xunit-tests.yml @@ -46,7 +46,7 @@ jobs: Write-Host "Completed xUnit test run." - name: Upload xUnit results - uses: actions/upload-artifact@v5 + uses: actions/upload-artifact@v6 if: always() with: name: ${{ inputs.test_results_artifact_name }} From 4b2e8e00be4fb41f1e005aa45ff636479dd795fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Dec 2025 17:59:07 +0000 Subject: [PATCH 036/112] Bump github/codeql-action from 4.31.7 to 4.31.8 (#26615) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 55275cbcc3..3271b53479 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 1cc99ace9a..3d9ff2eba2 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v3.29.5 + uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 with: sarif_file: results.sarif From 3f54d4af1f0cdd087cdfc0e5cddf06f83cfbb889 Mon Sep 17 00:00:00 2001 From: PowerShell Team Bot <69177312+pwshBot@users.noreply.github.com> Date: Mon, 15 Dec 2025 10:33:44 -0800 Subject: [PATCH 037/112] Update package references and build to use the .NET SDK 10.0.101 (#26595) --- DotnetRuntimeMetadata.json | 2 +- global.json | 2 +- ...oft.PowerShell.Commands.Diagnostics.csproj | 2 +- ...soft.PowerShell.Commands.Management.csproj | 2 +- ...crosoft.PowerShell.Commands.Utility.csproj | 6 +- ...crosoft.PowerShell.CoreCLR.Eventing.csproj | 2 +- .../Microsoft.PowerShell.SDK.csproj | 10 +- .../Microsoft.WSMan.Management.csproj | 2 +- .../PSVersionInfoGenerator.csproj | 2 +- .../System.Management.Automation.csproj | 12 +- .../BenchmarkDotNet.Extensions.csproj | 4 +- .../ResultsComparer/ResultsComparer.csproj | 2 +- test/tools/TestService/TestService.csproj | 2 +- test/tools/WebListener/WebListener.csproj | 2 +- tools/cgmanifest.json | 108 +++++++++--------- 15 files changed, 80 insertions(+), 80 deletions(-) diff --git a/DotnetRuntimeMetadata.json b/DotnetRuntimeMetadata.json index 267f0bda7d..1ae71bf073 100644 --- a/DotnetRuntimeMetadata.json +++ b/DotnetRuntimeMetadata.json @@ -4,7 +4,7 @@ "quality": "daily", "qualityFallback": "preview", "packageVersionPattern": "9.0.0-preview.6", - "sdkImageVersion": "10.0.100", + "sdkImageVersion": "10.0.101", "nextChannel": "9.0.0-preview.7", "azureFeed": "", "sdkImageOverride": "" diff --git a/global.json b/global.json index 376af49c07..936a420a57 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "10.0.100" + "version": "10.0.101" } } diff --git a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj index a338c175f1..f63196d364 100644 --- a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj +++ b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj index ef94950834..b2622f0517 100644 --- a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj +++ b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 1f9f342b25..b99139eb68 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -8,7 +8,7 @@ - + @@ -32,8 +32,8 @@ - - + + diff --git a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj index 45b4d8c0a0..feea566528 100644 --- a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj +++ b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj index 87f71daada..d3446d28c9 100644 --- a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj +++ b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj @@ -16,19 +16,19 @@ - - + + - - + + - + diff --git a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj index 48b4d8ff29..178f0473f2 100644 --- a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj +++ b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj b/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj index 1e53a35f96..1099b2a3c1 100644 --- a/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj +++ b/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index e929166b20..542fa8526f 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -32,12 +32,12 @@ - - - - - - + + + + + + diff --git a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj index 91566f64fb..92c3f13d29 100644 --- a/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj +++ b/test/perf/dotnet-tools/BenchmarkDotNet.Extensions/BenchmarkDotNet.Extensions.csproj @@ -6,8 +6,8 @@ - - + + diff --git a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj index af3fc10ffc..c483192484 100644 --- a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj +++ b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj @@ -9,7 +9,7 @@ - + diff --git a/test/tools/TestService/TestService.csproj b/test/tools/TestService/TestService.csproj index 15a6e49864..03aa569726 100644 --- a/test/tools/TestService/TestService.csproj +++ b/test/tools/TestService/TestService.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/tools/WebListener/WebListener.csproj b/test/tools/WebListener/WebListener.csproj index 1dddf99ba5..db2fde8595 100644 --- a/test/tools/WebListener/WebListener.csproj +++ b/test/tools/WebListener/WebListener.csproj @@ -7,6 +7,6 @@ - + diff --git a/tools/cgmanifest.json b/tools/cgmanifest.json index d8165298b2..efbc103f25 100644 --- a/tools/cgmanifest.json +++ b/tools/cgmanifest.json @@ -26,7 +26,7 @@ "Type": "nuget", "Nuget": { "Name": "Humanizer.Core", - "Version": "2.14.1" + "Version": "3.0.1" } }, "DevelopmentDependency": false @@ -36,7 +36,7 @@ "Type": "nuget", "Nuget": { "Name": "Json.More.Net", - "Version": "2.1.1" + "Version": "2.2.0" } }, "DevelopmentDependency": false @@ -46,7 +46,7 @@ "Type": "nuget", "Nuget": { "Name": "JsonPointer.Net", - "Version": "5.3.1" + "Version": "6.0.0" } }, "DevelopmentDependency": false @@ -66,7 +66,7 @@ "Type": "nuget", "Nuget": { "Name": "Markdig.Signed", - "Version": "0.43.0" + "Version": "0.44.0" } }, "DevelopmentDependency": false @@ -86,7 +86,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Bcl.AsyncInterfaces", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -106,7 +106,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.CodeAnalysis.Common", - "Version": "4.14.0" + "Version": "5.0.0" } }, "DevelopmentDependency": false @@ -116,7 +116,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.CodeAnalysis.CSharp", - "Version": "4.14.0" + "Version": "5.0.0" } }, "DevelopmentDependency": false @@ -126,7 +126,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Extensions.ObjectPool", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -166,7 +166,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.Registry.AccessControl", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -176,7 +176,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.SystemEvents", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -186,7 +186,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Windows.Compatibility", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -206,7 +206,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -216,7 +216,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -226,7 +226,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -236,7 +236,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x86.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -246,7 +246,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -256,7 +256,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -266,7 +266,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -276,7 +276,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-x64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -286,7 +286,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -296,7 +296,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -306,7 +306,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-x64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -316,7 +316,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-x64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -326,7 +326,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -336,7 +336,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-x64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -356,7 +356,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -366,7 +366,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -376,7 +376,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-x64.runtime.native.System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -436,7 +436,7 @@ "Type": "nuget", "Nuget": { "Name": "System.CodeDom", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -446,7 +446,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition.Registration", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -456,7 +456,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -466,7 +466,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Configuration.ConfigurationManager", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -476,7 +476,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.Odbc", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -486,7 +486,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.OleDb", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -506,7 +506,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.EventLog", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -516,7 +516,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.PerformanceCounter", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -526,7 +526,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.AccountManagement", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -536,7 +536,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.Protocols", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -546,7 +546,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -556,7 +556,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Drawing.Common", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -566,7 +566,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Packaging", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -576,7 +576,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Ports", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -586,7 +586,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Management", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -596,7 +596,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Net.Http.WinHttpHandler", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -606,7 +606,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Reflection.Context", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -616,7 +616,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Runtime.Caching", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -626,7 +626,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Pkcs", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -636,7 +636,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.ProtectedData", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -646,7 +646,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Xml", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -656,7 +656,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Permissions", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -706,7 +706,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Syndication", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -716,7 +716,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceProcess.ServiceController", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -726,7 +726,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Speech", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false @@ -746,7 +746,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Windows.Extensions", - "Version": "10.0.0" + "Version": "10.0.1" } }, "DevelopmentDependency": false From cd0628641b2ab7c733c060386faf87f29439a833 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 16 Dec 2025 10:50:38 -0800 Subject: [PATCH 038/112] Update `Get-ChangeLog` to handle backport PRs and Copilot PRs correctly (#26610) --- tools/releaseTools.psm1 | 113 +++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/tools/releaseTools.psm1 b/tools/releaseTools.psm1 index b034e2863d..99edf93c08 100644 --- a/tools/releaseTools.psm1 +++ b/tools/releaseTools.psm1 @@ -37,6 +37,7 @@ $Script:powershell_team = @( "dependabot-preview[bot]" "dependabot[bot]" "github-actions[bot]" + "Copilot" "Anam Navied" "Andrew Schwartzmeyer" "Jason Helmick" @@ -46,6 +47,27 @@ $Script:powershell_team = @( "Justin Chung" ) +# The powershell team members GitHub logins. We use them to decide if the original author of a backport PR is from the team. +$script:psteam_logins = @( + 'andyleejordan' + 'TravisEz13' + 'daxian-dbw' + 'adityapatwardhan' + 'SteveL-MSFT' + 'dependabot[bot]' + 'pwshBot' + 'jshigetomi' + 'SeeminglyScience' + 'anamnavi' + 'sdwheeler' + 'Copilot' + 'copilot-swe-agent' + 'app/copilot-swe-agent' + 'StevenBucher98' + 'alerickson' + 'tgauth' +) + # They are very active contributors, so we keep their email-login mappings here to save a few queries to Github. $Script:community_login_map = @{ "darpa@yandex.ru" = "iSazonov" @@ -54,11 +76,6 @@ $Script:community_login_map = @{ "info@powercode-consulting.se" = "powercode" } -# Ignore dependency bumping bot (Dependabot): -$Script:attribution_ignore_list = @( - 'dependabot[bot]@users.noreply.github.com' -) - ############################## #.SYNOPSIS #In the release workflow, the release branch will be merged back to master after the release is done, @@ -262,25 +279,76 @@ function Get-ChangeLog $clExperimental = @() foreach ($commit in $new_commits) { + $commitSubject = $commit.Subject + $prNumber = $commit.PullRequest + Write-Verbose "subject: $commitSubject" Write-Verbose "authorname: $($commit.AuthorName)" - if ($commit.AuthorEmail.EndsWith("@microsoft.com") -or $powershell_team -contains $commit.AuthorName -or $Script:attribution_ignore_list -contains $commit.AuthorEmail) { - $commit.ChangeLogMessage = "- {0}" -f (Get-ChangeLogMessage $commit.Subject) + + try { + $pr = Invoke-RestMethod ` + -Uri "https://api.github.com/repos/PowerShell/PowerShell/pulls/$prNumber" ` + -Headers $header ` + -ErrorAction Stop ` + -Verbose:$false ## Always disable verbose to avoid noise when we debug this function. + } catch { + ## A commit may not have corresponding GitHub PRs. In that case, we will get status code 404 (Not Found). + ## Otherwise, let the error bubble up. + if ($_.Exception.Response.StatusCode -ne 404) { + throw + } + } + + if ($commitSubject -match '^\[release/v\d\.\d\] ') { + ## The commit was from a backport PR. We need to get the real author in this case. + if (-not $pr) { + throw "The commit is from a backport PR (#$prNumber), but the PR cannot be found.`nPR Title: $commitSubject" + } + + $userPattern = 'Triggered by @.+ on behalf of @(.+)' + if ($pr.body -match $userPattern) { + $commit.AuthorGitHubLogin = ($Matches.1).Trim() + Write-Verbose "backport PR. real author login: $($commit.AuthorGitHubLogin)" + } else { + throw "The commit is from a backport PR (#$prNumber), but the PR description failed to match the pattern '$userPattern'. Was the template for backport PRs changed?`nPR Title: $commitSubject" + } + } + + if ($commit.AuthorGitHubLogin) { + if ($script:psteam_logins -contains $commit.AuthorGitHubLogin) { + $commit.ChangeLogMessage = "- {0}" -f (Get-ChangeLogMessage $commitSubject) + } else { + $commit.ChangeLogMessage = ("- {0} (Thanks @{1}!)" -f (Get-ChangeLogMessage $commitSubject), $commit.AuthorGitHubLogin) + $commit.ThankYouMessage = ("@{0}" -f ($commit.AuthorGitHubLogin)) + } + } elseif ($commit.AuthorEmail.EndsWith("@microsoft.com") -or $powershell_team -contains $commit.AuthorName) { + $commit.ChangeLogMessage = "- {0}" -f (Get-ChangeLogMessage $commitSubject) } else { if ($community_login_map.ContainsKey($commit.AuthorEmail)) { $commit.AuthorGitHubLogin = $community_login_map[$commit.AuthorEmail] } else { - $uri = "https://api.github.com/repos/PowerShell/PowerShell/commits/$($commit.Hash)" try{ - $response = Invoke-WebRequest -Uri $uri -Method Get -Headers $header -ErrorAction Ignore - } catch{} + ## Always disable verbose to avoid noise when we debug this function. + $response = Invoke-RestMethod ` + -Uri "https://api.github.com/repos/PowerShell/PowerShell/commits/$($commit.Hash)" ` + -Headers $header ` + -ErrorAction Stop ` + -Verbose:$false + } catch { + ## A commit could be available in ADO only. In that case, we will get status code 422 (UnprocessableEntity). + ## Otherwise, let the error bubble up. + if ($_.Exception.Response.StatusCode -ne 422) { + throw + } + } + if($response) { - $content = ConvertFrom-Json -InputObject $response.Content - $commit.AuthorGitHubLogin = $content.author.login + $commit.AuthorGitHubLogin = $response.author.login $community_login_map[$commit.AuthorEmail] = $commit.AuthorGitHubLogin } } - $commit.ChangeLogMessage = ("- {0} (Thanks @{1}!)" -f (Get-ChangeLogMessage $commit.Subject), $commit.AuthorGitHubLogin) + + $commit.ChangeLogMessage = ("- {0} (Thanks @{1}!)" -f (Get-ChangeLogMessage $commitSubject), $commit.AuthorGitHubLogin) $commit.ThankYouMessage = ("@{0}" -f ($commit.AuthorGitHubLogin)) } @@ -289,16 +357,6 @@ function Get-ChangeLog } ## Get the labels for the PR - try { - $pr = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/PowerShell/pulls/$($commit.PullRequest)" -Headers $header -ErrorAction SilentlyContinue - } - catch { - if ($_.Exception.Response.StatusCode -eq '404') { - $pr = $null - #continue - } - } - if($pr) { $clLabel = $pr.labels | Where-Object { $_.Name -match "^CL-"} @@ -328,7 +386,7 @@ function Get-ChangeLog "CL-Tools" { $clTools += $commit } "CL-Untagged" { $clUntagged += $commit } "CL-NotInBuild" { continue } - Default { throw "unknown tag '$cLabel' for PR: '$($commit.PullRequest)'" } + Default { throw "unknown tag '$cLabel' for PR: '$prNumber'" } } } } @@ -426,6 +484,9 @@ function Get-ChangeLogMessage '^Build\(deps\): ' { return $OriginalMessage.replace($Matches.0,'') } + '^\[release/v\d\.\d\] ' { + return $OriginalMessage.replace($Matches.0,'') + } default { return $OriginalMessage } @@ -867,8 +928,8 @@ function Invoke-PRBackport { ) $continue = $false while(!$continue) { - $input= Read-Host -Prompt ($Message + "`nType 'Yes' to continue 'No' to exit") - switch($input) { + $value = Read-Host -Prompt ($Message + "`nType 'Yes' to continue 'No' to exit") + switch($value) { 'yes' { $continue= $true } From b14cd0330c525f48fa12fa03b0ff02d4564c2984 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 16 Dec 2025 12:07:14 -0800 Subject: [PATCH 039/112] Fix the DSC test by skipping `AfterAll` cleanup if the initial setup in `BeforeAll` failed (#26621) --- .../dsc/dsc.profileresource.Tests.ps1 | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 index f361e67ea8..cb357c7350 100644 --- a/test/powershell/dsc/dsc.profileresource.Tests.ps1 +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -4,8 +4,10 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { BeforeAll { $DSC_ROOT = $env:DSC_ROOT + $skipCleanup = $false if (-not (Test-Path -Path $DSC_ROOT)) { + $skipCleanup = $true throw "DSC_ROOT environment variable is not set or path does not exist." } @@ -40,6 +42,10 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { New-Item -Path $testProfilePathCurrentUserAllHosts -Value $testProfileContent -Force -ItemType File } AfterAll { + if ($skipCleanup) { + return + } + # Restore original profile $testProfilePathCurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost if (Test-Path "$TestDrive/currentuser-currenthost-profile.bak") { @@ -118,16 +124,19 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdminOnWindows', 'RequireSudoOnUnix' { BeforeAll { $DSC_ROOT = $env:DSC_ROOT + $skipCleanup = $false if (-not (Test-Path -Path $DSC_ROOT)) { + $skipCleanup = $true throw "DSC_ROOT environment variable is not set or path does not exist." } Write-Verbose "DSC_ROOT is set to $DSC_ROOT" -Verbose + + $originalPath = $env:PATH + $pathSeparator = [System.IO.Path]::PathSeparator - $env:PATH += "$pathSeparator$DSC_ROOT" - $env:PATH += "$pathSeparator$PSHome" Write-Verbose "Updated PATH to include DSC_ROOT: $env:PATH" -Verbose @@ -151,11 +160,12 @@ Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdm $testProfilePathAllUsersAllHosts = $PROFILE.AllUsersAllHosts Copy-Item -Path $testProfilePathAllUsersAllHosts -Destination "$TestDrive/allusers-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue New-Item -Path $testProfilePathAllUsersAllHosts -Value $testProfileContent -Force -ItemType File - - $originalPath = $env:PATH - $env:PATH += "$pathSeparator$PSHome" } AfterAll { + if ($skipCleanup) { + return + } + $env:PATH = $originalPath $testProfilePathAllUsersCurrentHost = $PROFILE.AllUsersCurrentHost From d89da9b2fc4043a8b2fca84090b1b5e6d08bdddb Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 16 Dec 2025 15:58:36 -0800 Subject: [PATCH 040/112] Update changelog for the release v7.6.0-preview.6 (#26597) --- CHANGELOG/preview.md | 71 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/CHANGELOG/preview.md b/CHANGELOG/preview.md index 3648f4cc12..f1c5f56628 100644 --- a/CHANGELOG/preview.md +++ b/CHANGELOG/preview.md @@ -1,5 +1,76 @@ # Preview Changelog +## [7.6.0-preview.6] - 2025-12-11 + +### Engine Updates and Fixes + +- Properly Expand Aliases to their actual ResolvedCommand (#26571) (Thanks @kilasuit!) + +### General Cmdlet Updates and Fixes + +- Update `Microsoft.PowerShell.PSResourceGet` to `v1.2.0-preview5` (#26590) +- Make the experimental feature `PSFeedbackProvider` stable (#26502) +- Fix a regression in the API `CompletionCompleters.CompleteFilename()` that causes null reference exception (#26487) +- Add Delimiter parameter to `Get-Clipboard` (#26572) (Thanks @MartinGC94!) +- Close pipe client handles after creating the child ssh process (#26564) +- Make some experimental features stable (#26490) +- DSC v3 resource for PowerShell Profile (#26447) + +### Tools + +- Add merge conflict marker detection to linux-ci workflow and refactor existing actions to use reusable get-changed-files action (#26530) +- Add reusable get-changed-files action and refactor existing actions (#26529) +- Refactor analyze job to reusable workflow and enable on Windows CI (#26494) + +### Tests + +- Fix merge conflict checker for empty file lists and filter *.cs files (#26556) +- Add markdown link verification for PRs (#26445) + +### Build and Packaging Improvements + +
+ + + +

Expand to see details.

+ +
+ +
    +
  • Fix template path for rebuild branch check in package.yml (#26560)
  • +
  • Update the macos package name for preview releases to match the previous pattern (#26576)
  • +
  • Add rebuild branch support with conditional MSIX signing (#26573)
  • +
  • Update the WCF packages to the latest version that is compatible with v4.10.3 (#26503)
  • +
  • Improve ADO package build and validation across platforms (#26532)
  • +
  • Mirror .NET/runtime ICU version range in PowerShell (#26563) (Thanks @kasperk81!)
  • +
  • Update the macos package name for preview releases to match the previous pattern (#26562)
  • +
  • Fix condition syntax for StoreBroker package tasks in MSIX pipeline (#26561)
  • +
  • Move package validation to package pipeline (#26558)
  • +
  • Optimize/split windows package signing (#26557)
  • +
  • Remove usage of fpm for DEB package generation (#26504)
  • +
  • Add log grouping to build.psm1 for collapsible GitHub Actions logs (#26524)
  • +
  • Replace fpm with native macOS packaging tools (pkgbuild/productbuild) (#26501)
  • +
  • Replace fpm with native rpmbuild for RPM package generation (#26441)
  • +
  • Fix GitHub API rate limit errors in test actions (#26492)
  • +
  • Convert Azure DevOps Linux Packaging pipeline to GitHub Actions workflow (#26493)
  • +
  • Refactor: Centralize xUnit tests into reusable workflow and remove legacy verification (#26488)
  • +
  • Fix build to only enable ready-to-run for the Release configuration (#26481)
  • +
  • Integrate Windows packaging into windows-ci workflow using reusable workflow (#26468)
  • +
  • Update outdated package references (#26471)
  • +
  • GitHub Workflow cleanup (#26439)
  • +
  • Update PSResourceGet package version to preview4 (#26438)
  • +
  • Update PSReadLine to v2.4.5 (#26446)
  • +
  • Add network isolation policy parameter to vPack pipeline (#26444)
  • +
  • Fix a couple more lint errors
  • +
  • Fix lint errors in preview.md
  • +
  • Make MSIX publish stage dependent on SetReleaseTagandContainerName stage
  • +
+ +
+ +[7.6.0-preview.6]: https://github.com/PowerShell/PowerShell/compare/v7.6.0-preview.5...v7.6.0-preview.6 + ## [7.6.0-preview.5] - 2025-09-30 ### Engine Updates and Fixes From d0d39c03b12c9b485e739a448ae6a0eaa74d6384 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Wed, 17 Dec 2025 18:15:36 +0900 Subject: [PATCH 041/112] Fix Test-Json false positive errors when using oneOf or anyOf in schema (#26618) --- .../commands/utility/TestJsonCommand.cs | 39 +++- .../Test-Json.Tests.ps1 | 195 ++++++++++++++++++ 2 files changed, 224 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs index 32603f160f..909cbff3c8 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/TestJsonCommand.cs @@ -264,19 +264,11 @@ namespace Microsoft.PowerShell.Commands if (_jschema != null) { - EvaluationResults evaluationResults = _jschema.Evaluate(parsedJson, new EvaluationOptions { OutputFormat = OutputFormat.List }); + EvaluationResults evaluationResults = _jschema.Evaluate(parsedJson, new EvaluationOptions { OutputFormat = OutputFormat.Hierarchical }); result = evaluationResults.IsValid; if (!result) { - HandleValidationErrors(evaluationResults); - - if (evaluationResults.HasDetails) - { - foreach (var nestedResult in evaluationResults.Details) - { - HandleValidationErrors(nestedResult); - } - } + ReportValidationErrors(evaluationResults); } } } @@ -298,6 +290,33 @@ namespace Microsoft.PowerShell.Commands WriteObject(result); } + /// + /// Recursively reports validation errors from hierarchical evaluation results. + /// Skips nodes (and their children) where IsValid is true to avoid false positives + /// from constructs like OneOf or AnyOf. + /// + /// The evaluation result to process. + private void ReportValidationErrors(EvaluationResults evaluationResult) + { + // Skip this node and all children if validation passed + if (evaluationResult.IsValid) + { + return; + } + + // Report errors at this level + HandleValidationErrors(evaluationResult); + + // Recursively process child results + if (evaluationResult.HasDetails) + { + foreach (var nestedResult in evaluationResult.Details) + { + ReportValidationErrors(nestedResult); + } + } + } + private void HandleValidationErrors(EvaluationResults evaluationResult) { if (!evaluationResult.HasErrors) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 index 32a6cf3ce6..7b1b58d825 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Test-Json.Tests.ps1 @@ -86,6 +86,141 @@ Describe "Test-Json" -Tags "CI" { } '@ + # Schema using oneOf to allow either integer or string pattern for port items + $oneOfSchema = @' + { + "type": "object", + "properties": { + "ports": { + "type": "array", + "items": { + "oneOf": [ + { "type": "integer", "minimum": 0, "maximum": 65535 }, + { "type": "string", "pattern": "^\\d+-\\d+$" } + ] + } + } + } + } +'@ + + # Valid JSON where ports are integers (first oneOf choice matches) + $validOneOfJson = '{ "ports": [80, 443, 8080] }' + + # Invalid JSON where a port value matches neither oneOf choice + $invalidOneOfJson = '{ "ports": [80, "invalid-port", 8080] }' + + # Schema using oneOf to allow either smartphone or laptop device types + $oneOfDeviceSchema = @' + { + "type": "object", + "properties": { + "Devices": { + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { "type": "string" }, + "deviceType": { "const": "smartphone" }, + "os": { "type": "string", "enum": ["iOS", "Android"] } + }, + "required": ["deviceType", "os"] + }, + { + "properties": { + "id": { "type": "string" }, + "deviceType": { "const": "laptop" }, + "arch": { "type": "string", "enum": ["x86", "x64", "arm64"] } + }, + "required": ["deviceType", "arch"] + } + ] + } + } + }, + "required": ["Devices"] + } +'@ + + # Valid JSON with mixed device types (all matching their respective oneOf choice) + $validOneOfDeviceJson = @' + { + "Devices": [ + { "id": "0", "deviceType": "laptop", "arch": "x64" }, + { "id": "1", "deviceType": "smartphone", "os": "iOS" }, + { "id": "2", "deviceType": "laptop", "arch": "arm64" }, + { "id": "3", "deviceType": "smartphone", "os": "Android" } + ] + } +'@ + + # Invalid JSON where only Devices/3 has an invalid os value + $invalidOneOfDeviceJson = @' + { + "Devices": [ + { "id": "0", "deviceType": "laptop", "arch": "x64" }, + { "id": "1", "deviceType": "smartphone", "os": "iOS" }, + { "id": "2", "deviceType": "laptop", "arch": "arm64" }, + { "id": "3", "deviceType": "smartphone", "os": "WindowsPhone" } + ] + } +'@ + + # Schema using anyOf to allow either smartphone or laptop device types + $anyOfDeviceSchema = @' + { + "type": "object", + "properties": { + "Devices": { + "type": "array", + "items": { + "type": "object", + "anyOf": [ + { + "properties": { + "deviceType": { "const": "smartphone" }, + "os": { "type": "string", "enum": ["iOS", "Android"] } + }, + "required": ["deviceType", "os"] + }, + { + "properties": { + "deviceType": { "const": "laptop" }, + "arch": { "type": "string", "enum": ["x86", "x64", "arm64"] } + }, + "required": ["deviceType", "arch"] + } + ] + } + } + }, + "required": ["Devices"] + } +'@ + + # Valid JSON with mixed device types (all matching their respective anyOf choice) + $validAnyOfDeviceJson = @' + { + "Devices": [ + { "deviceType": "laptop", "arch": "x64" }, + { "deviceType": "smartphone", "os": "iOS" } + ] + } +'@ + + # Invalid JSON where only Devices/2 has an invalid os value + $invalidAnyOfDeviceJson = @' + { + "Devices": [ + { "deviceType": "laptop", "arch": "x64" }, + { "deviceType": "smartphone", "os": "iOS" }, + { "deviceType": "smartphone", "os": "WindowsPhone" } + ] + } +'@ + $validJsonPath = Join-Path -Path $TestDrive -ChildPath 'validJson.json' $validLiteralJsonPath = Join-Path -Path $TestDrive -ChildPath "[valid]Json.json" $invalidNodeInJsonPath = Join-Path -Path $TestDrive -ChildPath 'invalidNodeInJson.json' @@ -343,4 +478,64 @@ Describe "Test-Json" -Tags "CI" { # With options should pass ($Json | Test-Json -Option $Options -ErrorAction SilentlyContinue) | Should -BeTrue } + + It "Test-Json does not report false positives for valid oneOf matches" { + $errorVar = $null + $result = Test-Json -Json $validOneOfJson -Schema $oneOfSchema -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $result | Should -BeTrue + $errorVar.Count | Should -Be 0 + } + + It "Test-Json reports only relevant errors for invalid oneOf values" { + $errorVar = $null + $result = Test-Json -Json $invalidOneOfJson -Schema $oneOfSchema -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $result | Should -BeFalse + # Should report error only for the invalid item, not for valid items + $errorVar.Count | Should -BeGreaterThan 0 + $errorVar[0].Exception.Message | Should -Match "/ports/1" + } + + It "Test-Json does not report false positives for valid oneOf device matches" { + $errorVar = $null + $result = Test-Json -Json $validOneOfDeviceJson -Schema $oneOfDeviceSchema -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $result | Should -BeTrue + $errorVar.Count | Should -Be 0 + } + + It "Test-Json reports errors only for the invalid device in oneOf schema" { + $errorVar = $null + $result = Test-Json -Json $invalidOneOfDeviceJson -Schema $oneOfDeviceSchema -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $result | Should -BeFalse + # Should not report errors for valid devices (Devices/0, /1, /2) + $falsePositives = $errorVar | Where-Object { $_.Exception.Message -match '/Devices/(0|1|2)' } + $falsePositives.Count | Should -Be 0 + # Should report errors only for the invalid device (Devices/3) + $relevantErrors = $errorVar | Where-Object { $_.Exception.Message -match '/Devices/3' } + $relevantErrors.Count | Should -BeGreaterThan 0 + } + + It "Test-Json does not report false positives for valid anyOf device matches" { + $errorVar = $null + $result = Test-Json -Json $validAnyOfDeviceJson -Schema $anyOfDeviceSchema -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $result | Should -BeTrue + $errorVar.Count | Should -Be 0 + } + + It "Test-Json reports errors only for the invalid device in anyOf schema" { + $errorVar = $null + $result = Test-Json -Json $invalidAnyOfDeviceJson -Schema $anyOfDeviceSchema -ErrorVariable errorVar -ErrorAction SilentlyContinue + + $result | Should -BeFalse + # Should not report errors for valid devices (Devices/0, /1) + $falsePositives = $errorVar | Where-Object { $_.Exception.Message -match '/Devices/(0|1)' } + $falsePositives.Count | Should -Be 0 + # Should report errors only for the invalid device (Devices/2) + $relevantErrors = $errorVar | Where-Object { $_.Exception.Message -match '/Devices/2' } + $relevantErrors.Count | Should -BeGreaterThan 0 + } } From 9ef5ec4734313193f248818c5fb4b43803e06c43 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Wed, 17 Dec 2025 18:22:06 -0600 Subject: [PATCH 042/112] Bring release changes from the v7.6.0-preview.6 release branch (#26627) 1. Fix the conditions used in `release-MSIX-Publish.yml` 2. Update `build.psm1` to not install dotnet format tool for ADO build. --- .pipelines/templates/release-MSIX-Publish.yml | 4 ++-- build.psm1 | 22 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.pipelines/templates/release-MSIX-Publish.yml b/.pipelines/templates/release-MSIX-Publish.yml index 1735cd9f71..2bf1e13010 100644 --- a/.pipelines/templates/release-MSIX-Publish.yml +++ b/.pipelines/templates/release-MSIX-Publish.yml @@ -100,7 +100,7 @@ jobs: - task: MS-RDX-MRO.windows-store-publish.publish-task.store-publish@3 displayName: 'Publish StoreBroker Package (Stable/LTS)' - condition: and(ne('${{ parameters.skipMSIXPublish }}', 'true'), or(eq('$(STABLE)', 'true'), eq('$(LTS)', 'true'))) + condition: and(ne('${{ parameters.skipMSIXPublish }}', 'true'), or(eq(variables['STABLE'], 'true'), eq(variables['LTS'], 'true'))) inputs: serviceEndpoint: 'StoreAppPublish-Stable' appId: '$(AppID)' @@ -113,7 +113,7 @@ jobs: - task: MS-RDX-MRO.windows-store-publish.publish-task.store-publish@3 displayName: 'Publish StoreBroker Package (Preview)' - condition: and(ne('${{ parameters.skipMSIXPublish }}', 'true'), eq('$(PREVIEW)', 'true')) + condition: and(ne('${{ parameters.skipMSIXPublish }}', 'true'), eq(variables['PREVIEW'], 'true')) inputs: serviceEndpoint: 'StoreAppPublish-Preview' appId: '$(AppID)' diff --git a/build.psm1 b/build.psm1 index dd2cf0f351..d09b7af925 100644 --- a/build.psm1 +++ b/build.psm1 @@ -2485,19 +2485,21 @@ function Start-PSBootstrap { Write-LogGroupEnd -Title "Install Windows Dependencies" } - if ($Scenario -in 'All', 'Tools') { - Write-LogGroupStart -Title "Install .NET Global Tools" - Write-Log -message "Installing .NET global tools" + # Ensure dotnet is available + Find-Dotnet - # Ensure dotnet is available - Find-Dotnet + if (-not $env:TF_BUILD) { + if ($Scenario -in 'All', 'Tools') { + Write-LogGroupStart -Title "Install .NET Global Tools" + Write-Log -message "Installing .NET global tools" - # Install dotnet-format - Write-Verbose -Verbose "Installing dotnet-format global tool" - Start-NativeExecution { - dotnet tool install --global dotnet-format + # Install dotnet-format + Write-Verbose -Verbose "Installing dotnet-format global tool" + Start-NativeExecution { + dotnet tool install --global dotnet-format + } + Write-LogGroupEnd -Title "Install .NET Global Tools" } - Write-LogGroupEnd -Title "Install .NET Global Tools" } if ($env:TF_BUILD) { From 1df30e0c6d12018f0a20064cd522400d182f01fc Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Mon, 5 Jan 2026 11:35:14 -0500 Subject: [PATCH 043/112] Update SECURITY.md to remove email reporting option (#26653) --- .github/SECURITY.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/SECURITY.md b/.github/SECURITY.md index f941d308b1..797f700385 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -12,9 +12,7 @@ If you believe you have found a security vulnerability in any Microsoft-owned re Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report). -If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp). - -You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). +You should receive a response within 24 hours. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: From 867d799e6fadffa0d2676faa328ad37151904528 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Tue, 6 Jan 2026 10:27:45 -0500 Subject: [PATCH 044/112] Add default CODEOWNERS entry for maintainers (#26660) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6de13fd8da..14569b8192 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,6 +3,9 @@ # Areas are not limited to the filters defined in this file # First, let's start with areas with no filters or paths +# Default +* @PowerShell/powershell-maintainers + # Area: Performance # @adityapatwardhan From 4838a8d5f6c95339d4b44c8b402165e69e6ff929 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 09:44:22 -0800 Subject: [PATCH 045/112] Bump github/codeql-action from 4.31.8 to 4.31.9 (#26623) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 3271b53479..07a63c2e34 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 + uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 + uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 3d9ff2eba2..9b09c23280 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@1b168cd39490f61582a9beae412bb7057a6b2c4e # v3.29.5 + uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5 with: sarif_file: results.sarif From 7ace9e0f5e998c02151b5264148dacc58de76f87 Mon Sep 17 00:00:00 2001 From: Jonathan Gilbert Date: Fri, 9 Jan 2026 17:48:37 -0600 Subject: [PATCH 046/112] Improve error message from `Start-NativeExecution` (#26500) --- tools/buildCommon/startNativeExecution.ps1 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/buildCommon/startNativeExecution.ps1 b/tools/buildCommon/startNativeExecution.ps1 index ee7b00d04c..2e44d86bd6 100644 --- a/tools/buildCommon/startNativeExecution.ps1 +++ b/tools/buildCommon/startNativeExecution.ps1 @@ -16,6 +16,8 @@ function script:Start-NativeExecution { $ErrorActionPreference = "Continue" Write-Verbose "Executing: $ScriptBlock" try { + $cwd = Get-Location + if ($VerboseOutputOnError.IsPresent) { $output = & $ScriptBlock 2>&1 } else { @@ -36,10 +38,10 @@ function script:Start-NativeExecution { $callerFile = $callerLocationParts[0] $callerLine = $callerLocationParts[1] - $errorMessage = "Execution of {$ScriptBlock} by ${callerFile}: line $callerLine failed with exit code $LASTEXITCODE" + $errorMessage = "Execution of {$ScriptBlock} in '$cwd' by ${callerFile}: line $callerLine failed with exit code $LASTEXITCODE" throw $errorMessage } - throw "Execution of {$ScriptBlock} failed with exit code $LASTEXITCODE" + throw "Execution of {$ScriptBlock} in '$cwd' failed with exit code $LASTEXITCODE" } } finally { $ErrorActionPreference = $backupEAP From 5ebba3ca331be57baf8cdc0d7021e7f3ff3678d7 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Mon, 12 Jan 2026 13:05:19 -0500 Subject: [PATCH 047/112] Delete unused winget release script (#26683) --- tools/releaseToWinget.ps1 | 166 -------------------------------------- 1 file changed, 166 deletions(-) delete mode 100644 tools/releaseToWinget.ps1 diff --git a/tools/releaseToWinget.ps1 b/tools/releaseToWinget.ps1 deleted file mode 100644 index e3c1af080e..0000000000 --- a/tools/releaseToWinget.ps1 +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -param( - [Parameter(Mandatory)] - [semver] - $ReleaseVersion, - - [Parameter()] - [string] - $WingetRepoPath = "$PSScriptRoot/../../winget-pkgs", - - [Parameter()] - [string] - $FromRepository = 'rjmholt', - - [Parameter()] - [string] - $GitHubToken -) - -function GetMsiHash -{ - param( - [Parameter(Mandatory)] - [string] - $ReleaseVersion, - - [Parameter(Mandatory)] - $MsiName - ) - - $releaseParams = @{ - Tag = "v$ReleaseVersion" - OwnerName = 'PowerShell' - RepositoryName = 'PowerShell' - } - - if ($GitHubToken) { $releaseParams.AccessToken = $GitHubToken } - - $releaseDescription = (Get-GitHubRelease @releaseParams).body - - $regex = [regex]::new("powershell-$ReleaseVersion-win-x64.msi.*?([0-9A-F]{64})", 'SingleLine,IgnoreCase') - - return $regex.Match($releaseDescription).Groups[1].Value -} - -function GetThisScriptRepoUrl -{ - # Find the root of the repo - $prefix = $PSScriptRoot - while ($prefix) - { - if (Test-Path "$prefix/LICENSE.txt") - { - break - } - - $prefix = Split-Path $prefix - } - - $stem = $PSCommandPath.Substring($prefix.Length + 1).Replace('\', '/') - - return "https://github.com/PowerShell/PowerShell/blob/master/$stem" -} - -function Exec -{ - param([scriptblock]$sb) - - & $sb - - if ($LASTEXITCODE -ne 0) - { - throw "Invocation failed for '$sb'. See above errors for details" - } -} - -$ErrorActionPreference = 'Stop' - -$wingetPath = (Resolve-Path $WingetRepoPath).Path - -# Ensure we have git and PowerShellForGitHub installed -Import-Module -Name PowerShellForGitHub -$null = Get-Command git - -# Get the MSI hash from the release body -$msiName = "PowerShell-$ReleaseVersion-win-x64.msi" -$msiHash = GetMsiHash -ReleaseVersion $ReleaseVersion -MsiName $msiName - -$publisherName = 'Microsoft' - -# Create the manifest -$productName = if ($ReleaseVersion.PreReleaseLabel) -{ - 'PowerShell-Preview' -} -else -{ - 'PowerShell' -} - -$manifestDir = Join-Path $wingetPath 'manifests' 'm' $publisherName $productName $ReleaseVersion -$manifestPath = Join-Path $manifestDir "$publisherName.$productName.yaml" - -$manifestContent = @" -PackageIdentifier: $publisherName.$productName -PackageVersion: $ReleaseVersion -PackageName: $productName -Publisher: $publisherName -PackageUrl: https://microsoft.com/PowerShell -License: MIT -LicenseUrl: https://github.com/PowerShell/PowerShell/blob/master/LICENSE.txt -Moniker: $($productName.ToLower()) -ShortDescription: $publisherName.$productName -Description: PowerShell is a cross-platform (Windows, Linux, and macOS) automation and configuration tool/framework that works well with your existing tools and is optimized for dealing with structured data (e.g. JSON, CSV, XML, etc.), REST APIs, and object models. It includes a command-line shell, an associated scripting language and a framework for processing cmdlets. -Tags: -- powershell -- pwsh -Homepage: https://github.com/PowerShell/PowerShell -Installers: -- Architecture: x64 - InstallerUrl: https://github.com/PowerShell/PowerShell/releases/download/v$ReleaseVersion/$msiName - InstallerSha256: $msiHash - InstallerType: msi -PackageLocale: en-US -ManifestType: singleton -ManifestVersion: 1.0.0 - -"@ - -Push-Location $wingetPath -try -{ - $branch = "pwsh-$ReleaseVersion" - - Exec { git checkout master } - Exec { git checkout -b $branch } - - New-Item -Path $manifestDir -ItemType Directory - Set-Content -Path $manifestPath -Value $manifestContent -Encoding utf8NoBOM - - Exec { git add $manifestPath } - Exec { git commit -m "Add $productName $ReleaseVersion" } - Exec { git push origin $branch } - - $prParams = @{ - Title = "Add $productName $ReleaseVersion" - Body = "This pull request is automatically generated. See $(GetThisScriptRepoUrl)." - Head = $branch - HeadOwner = $FromRepository - Base = 'master' - Owner = 'Microsoft' - RepositoryName = 'winget-pkgs' - MaintainerCanModify = $true - } - - if ($GitHubToken) { $prParams.AccessToken = $GitHubToken } - - New-GitHubPullRequest @prParams -} -finally -{ - git checkout master - Pop-Location -} From bf03a14b6dcf6f399451f45b01460c6a7280de1d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 12 Jan 2026 10:44:55 -0800 Subject: [PATCH 048/112] Fix the CLR internal error and null ref exception when running `show-command` with PowerShell API (#26669) --- .../commandHelpers/ShowCommandHelper.cs | 3 ++- .../resources/HelpErrors.resx | 2 +- .../utils/GraphicalHostReflectionWrapper.cs | 13 +++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs b/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs index 32c68e961c..10690b65dc 100644 --- a/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs +++ b/src/Microsoft.Management.UI.Internal/commandHelpers/ShowCommandHelper.cs @@ -679,7 +679,8 @@ Function PSGetSerializedShowCommandInfo /// The host window, if it is present or null if it is not. internal static Window GetHostWindow(PSCmdlet cmdlet) { - PSPropertyInfo windowProperty = cmdlet.Host.PrivateData.Properties["Window"]; + // The value of 'PrivateData' property may be null for the default host or a custom host. + PSPropertyInfo windowProperty = cmdlet.Host.PrivateData?.Properties["Window"]; if (windowProperty == null) { return null; diff --git a/src/System.Management.Automation/resources/HelpErrors.resx b/src/System.Management.Automation/resources/HelpErrors.resx index 27634de299..ae797e8af1 100644 --- a/src/System.Management.Automation/resources/HelpErrors.resx +++ b/src/System.Management.Automation/resources/HelpErrors.resx @@ -179,7 +179,7 @@ To update these Help topics, start PowerShell by using the "Run as Administrator" command, and try running Update-Help again. - To use the {0}, install Windows PowerShell ISE by using Server Manager, and then restart this application. ({1}) + To use the {0}, make sure your application uses 'Microsoft.NET.Sdk.WindowsDesktop' as the project SDK and the corresponding assembly 'Microsoft.PowerShell.GraphicalHost' is available. ({1}) {0} does not work in a remote session. diff --git a/src/System.Management.Automation/utils/GraphicalHostReflectionWrapper.cs b/src/System.Management.Automation/utils/GraphicalHostReflectionWrapper.cs index a0b12f986d..ec78022234 100644 --- a/src/System.Management.Automation/utils/GraphicalHostReflectionWrapper.cs +++ b/src/System.Management.Automation/utils/GraphicalHostReflectionWrapper.cs @@ -55,7 +55,7 @@ namespace System.Management.Automation.Internal /// When it was not possible to load Microsoft.PowerShell.GraphicalHost.dlly. internal static GraphicalHostReflectionWrapper GetGraphicalHostReflectionWrapper(PSCmdlet parentCmdlet, string graphicalHostHelperTypeName) { - return GraphicalHostReflectionWrapper.GetGraphicalHostReflectionWrapper(parentCmdlet, graphicalHostHelperTypeName, parentCmdlet.CommandInfo.Name); + return GetGraphicalHostReflectionWrapper(parentCmdlet, graphicalHostHelperTypeName, parentCmdlet.CommandInfo.Name); } /// @@ -73,9 +73,9 @@ namespace System.Management.Automation.Internal [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Assembly.Load has been found to throw unadvertised exceptions")] internal static GraphicalHostReflectionWrapper GetGraphicalHostReflectionWrapper(PSCmdlet parentCmdlet, string graphicalHostHelperTypeName, string featureName) { - GraphicalHostReflectionWrapper returnValue = new GraphicalHostReflectionWrapper(); + GraphicalHostReflectionWrapper returnValue = new(); - if (GraphicalHostReflectionWrapper.IsInputFromRemoting(parentCmdlet)) + if (IsInputFromRemoting(parentCmdlet)) { ErrorRecord error = new ErrorRecord( new NotSupportedException(StringUtil.Format(HelpErrors.RemotingNotSupportedForFeature, featureName)), @@ -87,9 +87,10 @@ namespace System.Management.Automation.Internal } // Prepare the full assembly name. - AssemblyName graphicalHostAssemblyName = new AssemblyName(); + AssemblyName smaAssemblyName = typeof(PSObject).Assembly.GetName(); + AssemblyName graphicalHostAssemblyName = new(); graphicalHostAssemblyName.Name = "Microsoft.PowerShell.GraphicalHost"; - graphicalHostAssemblyName.Version = new Version(3, 0, 0, 0); + graphicalHostAssemblyName.Version = smaAssemblyName.Version; graphicalHostAssemblyName.CultureInfo = new CultureInfo(string.Empty); // Neutral culture graphicalHostAssemblyName.SetPublicKeyToken(new byte[] { 0x31, 0xbf, 0x38, 0x56, 0xad, 0x36, 0x4e, 0x35 }); @@ -124,7 +125,7 @@ namespace System.Management.Automation.Internal returnValue._graphicalHostHelperType = returnValue._graphicalHostAssembly.GetType(graphicalHostHelperTypeName); - Diagnostics.Assert(returnValue._graphicalHostHelperType != null, "the type exists in Microsoft.PowerShell.GraphicalHost"); + Diagnostics.Assert(returnValue._graphicalHostHelperType != null, "the type should exist in Microsoft.PowerShell.GraphicalHost"); ConstructorInfo constructor = returnValue._graphicalHostHelperType.GetConstructor( BindingFlags.NonPublic | BindingFlags.Instance, null, From c792b3c0ba609776ce6eba308495f76601b2c04f Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 12 Jan 2026 10:45:41 -0800 Subject: [PATCH 049/112] Fix to use accurate message for validating a string argument is not null and not an empty string (#26668) --- .../CommandCompletion/CompletionResult.cs | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/src/System.Management.Automation/engine/CommandCompletion/CompletionResult.cs b/src/System.Management.Automation/engine/CommandCompletion/CompletionResult.cs index ffb09e2a50..0554a52ba1 100644 --- a/src/System.Management.Automation/engine/CommandCompletion/CompletionResult.cs +++ b/src/System.Management.Automation/engine/CommandCompletion/CompletionResult.cs @@ -168,26 +168,15 @@ namespace System.Management.Automation /// The text for the tooltip with details to be displayed about the object. public CompletionResult(string completionText, string listItemText, CompletionResultType resultType, string toolTip) { - if (string.IsNullOrEmpty(completionText)) - { - throw PSTraceSource.NewArgumentNullException(nameof(completionText)); - } - - if (string.IsNullOrEmpty(listItemText)) - { - throw PSTraceSource.NewArgumentNullException(nameof(listItemText)); - } + ArgumentException.ThrowIfNullOrEmpty(completionText); + ArgumentException.ThrowIfNullOrEmpty(listItemText); + ArgumentException.ThrowIfNullOrEmpty(toolTip); if (resultType < CompletionResultType.Text || resultType > CompletionResultType.DynamicKeyword) { throw PSTraceSource.NewArgumentOutOfRangeException(nameof(resultType), resultType); } - if (string.IsNullOrEmpty(toolTip)) - { - throw PSTraceSource.NewArgumentNullException(nameof(toolTip)); - } - _completionText = completionText; _listItemText = listItemText; _toolTip = toolTip; From 9bc47950e20d1d85710dbf424270e54a649fd3ac Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 12 Jan 2026 10:47:41 -0800 Subject: [PATCH 050/112] Add `ValidateNotNullOrEmpty` attribute to the `-Property` of `Format-Table/List/Custom` (#26552) --- .../FormatAndOutput/format-object/Format-Object.cs | 1 + .../common/BaseFormattingCommand.cs | 1 + test/powershell/engine/Formatting/BugFix.Tests.ps1 | 14 ++++++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs index c2c16ff2cd..693a799c80 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/FormatAndOutput/format-object/Format-Object.cs @@ -32,6 +32,7 @@ namespace Microsoft.PowerShell.Commands /// will be determined using property sets, etc. /// [Parameter(Position = 0)] + [ValidateNotNullOrEmpty] public object[] Property { get { return _props; } diff --git a/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs b/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs index 683553a3b3..be31a1db9a 100644 --- a/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs +++ b/src/System.Management.Automation/FormatAndOutput/common/BaseFormattingCommand.cs @@ -728,6 +728,7 @@ namespace Microsoft.PowerShell.Commands.Internal.Format /// will be determined using property sets, etc. ///
[Parameter(Position = 0)] + [ValidateNotNullOrEmpty] public object[] Property { get; set; } /// diff --git a/test/powershell/engine/Formatting/BugFix.Tests.ps1 b/test/powershell/engine/Formatting/BugFix.Tests.ps1 index 5be7797134..8b1ecdaccd 100644 --- a/test/powershell/engine/Formatting/BugFix.Tests.ps1 +++ b/test/powershell/engine/Formatting/BugFix.Tests.ps1 @@ -63,3 +63,17 @@ Describe "Hidden properties should not be returned by the 'FirstOrDefault' primi $outstring.Trim() | Should -BeExactly "Param$([System.Environment]::NewLine)-----$([System.Environment]::NewLine)Foo" } } + +Describe "'Format-Table/List/Custom -Property' should not throw NullRef exception" -Tag CI { + + It "' -Property' requires value to be not null and not empty" -TestCases @( + @{ Command = "Format-Table"; NameInErrorId = "FormatTableCommand" } + @{ Command = "Format-List"; NameInErrorId = "FormatListCommand" } + @{ Command = "Format-Custom"; NameInErrorId = "FormatCustomCommand" } + ) { + param($Command, $NameInErrorId) + + { Get-Process -Id $PID | & $Command -Property @() } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.$NameInErrorId" + { Get-Process -Id $PID | & $Command -Property $null } | Should -Throw -ErrorId "ParameterArgumentValidationError,Microsoft.PowerShell.Commands.$NameInErrorId" + } +} From 20382c85654caa53d2403db17857952775cf7ecd Mon Sep 17 00:00:00 2001 From: PowerShell Team Bot <69177312+pwshBot@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:50:04 -0800 Subject: [PATCH 051/112] Update outdated package references (#26656) --- test/xUnit/xUnit.tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/xUnit/xUnit.tests.csproj b/test/xUnit/xUnit.tests.csproj index 0863a23d44..de5b04b8c6 100644 --- a/test/xUnit/xUnit.tests.csproj +++ b/test/xUnit/xUnit.tests.csproj @@ -24,12 +24,12 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + From 76bc34e08114639bf2640cc0a708fac900f1e160 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 15:26:44 -0800 Subject: [PATCH 052/112] Bump github/codeql-action from 4.31.9 to 4.31.10 (#26686) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 07a63c2e34..04c5b6cb58 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5 + uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5 + uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 9b09c23280..87b593164a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v3.29.5 + uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5 with: sarif_file: results.sarif From 1241ad2794cc84c18554e36ce658bfd86781cf8e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:58:38 -0500 Subject: [PATCH 053/112] Remove unused runCodesignValidationInjection variable from pipeline templates (#26412) 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 (HE/HIM) --- .pipelines/templates/compliance/apiscan.yml | 2 -- .pipelines/templates/compliance/generateNotice.yml | 2 -- .pipelines/templates/linux-package-build.yml | 2 -- .pipelines/templates/linux.yml | 4 ---- .pipelines/templates/mac-package-build.yml | 2 -- .pipelines/templates/mac.yml | 2 -- .pipelines/templates/nupkg.yml | 2 -- .pipelines/templates/packaging/windows/package.yml | 2 -- .pipelines/templates/release-symbols.yml | 2 -- .pipelines/templates/release-upload-buildinfo.yml | 2 -- .pipelines/templates/testartifacts.yml | 4 ---- .pipelines/templates/uploadToAzure.yml | 2 -- .pipelines/templates/variable/release-shared.yml | 2 -- .pipelines/templates/windows-hosted-build.yml | 2 -- 14 files changed, 32 deletions(-) diff --git a/.pipelines/templates/compliance/apiscan.yml b/.pipelines/templates/compliance/apiscan.yml index 5809af8e28..b5a1569902 100644 --- a/.pipelines/templates/compliance/apiscan.yml +++ b/.pipelines/templates/compliance/apiscan.yml @@ -4,8 +4,6 @@ jobs: - job: APIScan variables: - - name: runCodesignValidationInjection - value : false - name: NugetSecurityAnalysisWarningLevel value: none - name: ReleaseTagVar diff --git a/.pipelines/templates/compliance/generateNotice.yml b/.pipelines/templates/compliance/generateNotice.yml index 7de316e8b4..0a38ed8f8e 100644 --- a/.pipelines/templates/compliance/generateNotice.yml +++ b/.pipelines/templates/compliance/generateNotice.yml @@ -10,8 +10,6 @@ parameters: jobs: - job: generateNotice variables: - - name: runCodesignValidationInjection - value : false - name: NugetSecurityAnalysisWarningLevel value: none - name: ob_outputDirectory diff --git a/.pipelines/templates/linux-package-build.yml b/.pipelines/templates/linux-package-build.yml index bcf332b377..ea49b9fea5 100644 --- a/.pipelines/templates/linux-package-build.yml +++ b/.pipelines/templates/linux-package-build.yml @@ -13,8 +13,6 @@ jobs: type: linux variables: - - name: runCodesignValidationInjection - value: false - name: nugetMultiFeedWarnLevel value: none - name: NugetSecurityAnalysisWarningLevel diff --git a/.pipelines/templates/linux.yml b/.pipelines/templates/linux.yml index aef0b53381..97b594830b 100644 --- a/.pipelines/templates/linux.yml +++ b/.pipelines/templates/linux.yml @@ -10,8 +10,6 @@ jobs: pool: type: linux variables: - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - name: DOTNET_NOLOGO @@ -139,8 +137,6 @@ jobs: pool: type: windows variables: - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - name: DOTNET_NOLOGO diff --git a/.pipelines/templates/mac-package-build.yml b/.pipelines/templates/mac-package-build.yml index 4e7040d4ac..def866173a 100644 --- a/.pipelines/templates/mac-package-build.yml +++ b/.pipelines/templates/mac-package-build.yml @@ -15,8 +15,6 @@ jobs: variables: - name: HOMEBREW_NO_ANALYTICS value: 1 - - name: runCodesignValidationInjection - value: false - name: nugetMultiFeedWarnLevel value: none - name: NugetSecurityAnalysisWarningLevel diff --git a/.pipelines/templates/mac.yml b/.pipelines/templates/mac.yml index ff4787b9bc..1699207c65 100644 --- a/.pipelines/templates/mac.yml +++ b/.pipelines/templates/mac.yml @@ -13,8 +13,6 @@ jobs: variables: - name: HOMEBREW_NO_ANALYTICS value: 1 - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - group: DotNetPrivateBuildAccess diff --git a/.pipelines/templates/nupkg.yml b/.pipelines/templates/nupkg.yml index 3a2aa4f317..3558c94940 100644 --- a/.pipelines/templates/nupkg.yml +++ b/.pipelines/templates/nupkg.yml @@ -6,8 +6,6 @@ jobs: type: windows variables: - - name: runCodesignValidationInjection - value: false - name: nugetMultiFeedWarnLevel value: none - name: NugetSecurityAnalysisWarningLevel diff --git a/.pipelines/templates/packaging/windows/package.yml b/.pipelines/templates/packaging/windows/package.yml index 03673d61c7..1f03d65ab2 100644 --- a/.pipelines/templates/packaging/windows/package.yml +++ b/.pipelines/templates/packaging/windows/package.yml @@ -9,8 +9,6 @@ jobs: type: windows variables: - - name: runCodesignValidationInjection - value: false - name: ob_sdl_codeSignValidation_enabled value: false # Skip signing validation in build-only stage - name: ob_signing_setup_enabled diff --git a/.pipelines/templates/release-symbols.yml b/.pipelines/templates/release-symbols.yml index 1023dcf525..a628f4d712 100644 --- a/.pipelines/templates/release-symbols.yml +++ b/.pipelines/templates/release-symbols.yml @@ -10,8 +10,6 @@ jobs: pool: type: windows variables: - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - name: DOTNET_NOLOGO diff --git a/.pipelines/templates/release-upload-buildinfo.yml b/.pipelines/templates/release-upload-buildinfo.yml index c18c96fc64..e32272c630 100644 --- a/.pipelines/templates/release-upload-buildinfo.yml +++ b/.pipelines/templates/release-upload-buildinfo.yml @@ -14,8 +14,6 @@ jobs: demands: - ImageOverride -equals PSMMS2019-Secure variables: - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - name: DOTNET_NOLOGO diff --git a/.pipelines/templates/testartifacts.yml b/.pipelines/templates/testartifacts.yml index 751c9d5a53..3a6bec4a85 100644 --- a/.pipelines/templates/testartifacts.yml +++ b/.pipelines/templates/testartifacts.yml @@ -1,8 +1,6 @@ jobs: - job: build_testartifacts_win variables: - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - group: DotNetPrivateBuildAccess @@ -73,8 +71,6 @@ jobs: - job: build_testartifacts_nonwin variables: - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - group: DotNetPrivateBuildAccess diff --git a/.pipelines/templates/uploadToAzure.yml b/.pipelines/templates/uploadToAzure.yml index 20842de81c..ce7f26131c 100644 --- a/.pipelines/templates/uploadToAzure.yml +++ b/.pipelines/templates/uploadToAzure.yml @@ -7,8 +7,6 @@ jobs: variables: - name: ob_sdl_sbom_enabled value: true - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - name: DOTNET_NOLOGO diff --git a/.pipelines/templates/variable/release-shared.yml b/.pipelines/templates/variable/release-shared.yml index 325f72224f..70d3dd2df9 100644 --- a/.pipelines/templates/variable/release-shared.yml +++ b/.pipelines/templates/variable/release-shared.yml @@ -17,8 +17,6 @@ variables: value: false - name: ob_sdl_sbom_enabled value: ${{ parameters.SBOM }} - - name: runCodesignValidationInjection - value: false - name: DOTNET_NOLOGO value: 1 - group: 'mscodehub-code-read-akv' diff --git a/.pipelines/templates/windows-hosted-build.yml b/.pipelines/templates/windows-hosted-build.yml index 44b83ad20a..83c6b32cdf 100644 --- a/.pipelines/templates/windows-hosted-build.yml +++ b/.pipelines/templates/windows-hosted-build.yml @@ -10,8 +10,6 @@ jobs: pool: type: windows variables: - - name: runCodesignValidationInjection - value: false - name: NugetSecurityAnalysisWarningLevel value: none - name: DOTNET_NOLOGO From bd33c2d9878a46af65968da42fc4411bcd7bc46d Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 14 Jan 2026 14:38:37 -0800 Subject: [PATCH 054/112] Update metadata.json to update the Latest attribute with a better name (#26380) --- .pipelines/templates/channelSelection.yml | 8 ++++---- .pipelines/templates/release-prep-for-ev2.yml | 8 ++++---- .pipelines/templates/release-upload-buildinfo.yml | 4 ++-- tools/metadata.json | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.pipelines/templates/channelSelection.yml b/.pipelines/templates/channelSelection.yml index 9dd0f3fb21..d6ddb53256 100644 --- a/.pipelines/templates/channelSelection.yml +++ b/.pipelines/templates/channelSelection.yml @@ -2,6 +2,10 @@ steps: - pwsh: | # Determine LTS, Preview, or Stable $metadata = Get-Content "$(Build.SourcesDirectory)/PowerShell/tools/metadata.json" -Raw | ConvertFrom-Json + + $LTS = $metadata.LTSRelease.PublishToChannels + $Stable = $metadata.StableRelease.PublishToChannels + $isPreview = '$(OutputReleaseTag.releaseTag)' -match '-' $releaseTag = '$(OutputReleaseTag.releaseTag)' # Rebuild branches should be treated as preview builds @@ -11,10 +15,6 @@ steps: # If you update this regex, also update it in rebuild-branch-check.yml to keep them in sync. $isRebuildBranch = '$(Build.SourceBranch)' -match 'refs/heads/rebuild/.*-rebuild\.' - $LTS = $metadata.LTSRelease.Latest - $Stable = $metadata.StableRelease.Latest - $isPreview = $releaseTag -match '-' - # If this is a rebuild branch, force preview mode and ignore LTS metadata if ($isRebuildBranch) { $IsLTS = $false diff --git a/.pipelines/templates/release-prep-for-ev2.yml b/.pipelines/templates/release-prep-for-ev2.yml index cf7982cd5e..e644bece68 100644 --- a/.pipelines/templates/release-prep-for-ev2.yml +++ b/.pipelines/templates/release-prep-for-ev2.yml @@ -33,7 +33,7 @@ stages: - template: release-SetReleaseTagandContainerName.yml parameters: restorePhase: true - + - pwsh: | $packageVersion = '$(OutputReleaseTag.ReleaseTag)'.ToLowerInvariant() -replace '^v','' $vstsCommandString = "vso[task.setvariable variable=packageVersion]$packageVersion" @@ -42,7 +42,7 @@ stages: displayName: Set Package version env: ob_restore_phase: true - + - pwsh: | $branch = 'mirror-target' $gitArgs = "clone", @@ -151,7 +151,7 @@ stages: $metadataHash = @{} $skipPublishValue = '${{ parameters.skipPublish }}' $metadataHash["ReleaseTag"] = '$(OutputReleaseTag.ReleaseTag)' - $metadataHash["LTS"] = $metadata.LTSRelease.Latest + $metadataHash["LTS"] = $metadata.LTSRelease.PublishToChannels $metadataHash["ForProduction"] = $true $metadataHash["SkipPublish"] = [System.Convert]::ToBoolean($skipPublishValue) @@ -222,7 +222,7 @@ stages: files_to_sign: '*.ps1' search_root: '$(repoRoot)/.pipelines/EV2Specs/ServiceGroupRoot/Shell/Run' displayName: Sign Run.ps1 - + - pwsh: | # folder to tar must have: Run.ps1, settings.toml, python_dl $srcPath = Join-Path '$(ev2ServiceGroupRootFolder)' -ChildPath 'Shell' diff --git a/.pipelines/templates/release-upload-buildinfo.yml b/.pipelines/templates/release-upload-buildinfo.yml index e32272c630..bcb2a43396 100644 --- a/.pipelines/templates/release-upload-buildinfo.yml +++ b/.pipelines/templates/release-upload-buildinfo.yml @@ -56,8 +56,8 @@ jobs: $dateTime = [datetime]::new($dateTime.Ticks - ($dateTime.Ticks % [timespan]::TicksPerSecond), $dateTime.Kind) $metadata = Get-Content -LiteralPath "$toolsDirectory/metadata.json" -ErrorAction Stop | ConvertFrom-Json - $stableRelease = $metadata.StableRelease.Latest - $ltsRelease = $metadata.LTSRelease.Latest + $stableRelease = $metadata.StableRelease.PublishToChannels + $ltsRelease = $metadata.LTSRelease.PublishToChannels Write-Verbose -Verbose "Writing $jsonFile contents:" $buildInfoJsonContent = Get-Content $jsonFile -Encoding UTF8NoBom -Raw diff --git a/tools/metadata.json b/tools/metadata.json index bbe64d0cef..e0f4952c58 100644 --- a/tools/metadata.json +++ b/tools/metadata.json @@ -5,6 +5,6 @@ "ReleaseTag": "v7.5.4", "LTSReleaseTag" : ["v7.4.13"], "NextReleaseTag": "v7.6.0-preview.6", - "LTSRelease": { "Latest": false, "Package": false }, - "StableRelease": { "Latest": false, "Package": false } + "LTSRelease": { "PublishToChannels": false, "Package": false }, + "StableRelease": { "PublishToChannels": false, "Package": false } } From 5fdc2a023f9591bc467cdeff17dfaea6820f40bd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 15 Jan 2026 10:40:58 -0500 Subject: [PATCH 055/112] Fix macOS preview package identifier detection to use version string (#26690) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- test/packaging/packaging.tests.ps1 | 67 +++++++++++++++++++++++++++++ tools/packaging/packaging.psm1 | 69 ++++++++++++++++++++++-------- 2 files changed, 119 insertions(+), 17 deletions(-) create mode 100644 test/packaging/packaging.tests.ps1 diff --git a/test/packaging/packaging.tests.ps1 b/test/packaging/packaging.tests.ps1 new file mode 100644 index 0000000000..cc4bbe0d72 --- /dev/null +++ b/test/packaging/packaging.tests.ps1 @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "Packaging Module Functions" { + BeforeAll { + Import-Module $PSScriptRoot/../../build.psm1 -Force + Import-Module $PSScriptRoot/../../tools/packaging/packaging.psm1 -Force + } + + Context "Test-IsPreview function" { + It "Should return True for preview versions" { + Test-IsPreview -Version "7.6.0-preview.6" | Should -Be $true + Test-IsPreview -Version "7.5.0-rc.1" | Should -Be $true + } + + It "Should return False for stable versions" { + Test-IsPreview -Version "7.6.0" | Should -Be $false + Test-IsPreview -Version "7.5.0" | Should -Be $false + } + + It "Should return False for LTS builds regardless of version string" { + Test-IsPreview -Version "7.6.0-preview.6" -IsLTS | Should -Be $false + Test-IsPreview -Version "7.5.0" -IsLTS | Should -Be $false + } + } + + Context "Get-MacOSPackageIdentifierInfo function (New-MacOSPackage logic)" { + It "Should detect preview builds and return preview identifier" { + $result = Get-MacOSPackageIdentifierInfo -Version "7.6.0-preview.6" -LTS:$false + + $result.IsPreview | Should -Be $true + $result.PackageIdentifier | Should -Be "com.microsoft.powershell-preview" + } + + It "Should detect stable builds and return stable identifier" { + $result = Get-MacOSPackageIdentifierInfo -Version "7.6.0" -LTS:$false + + $result.IsPreview | Should -Be $false + $result.PackageIdentifier | Should -Be "com.microsoft.powershell" + } + + It "Should treat LTS builds as stable even with preview version string" { + $result = Get-MacOSPackageIdentifierInfo -Version "7.4.0-preview.1" -LTS:$true + + $result.IsPreview | Should -Be $false + $result.PackageIdentifier | Should -Be "com.microsoft.powershell" + } + + It "Should NOT use package name for preview detection (bug fix verification)" { + # This test verifies the fix for issue #26673 + # The bug was using ($Name -like '*-preview') which always returned false + # because preview builds use Name="powershell" not "powershell-preview" + + $Version = "7.6.0-preview.6" + $Name = "powershell" # Preview builds use "powershell" not "powershell-preview" + + # The INCORRECT logic (the bug): $Name -like '*-preview' + $incorrectCheck = $Name -like '*-preview' + $incorrectCheck | Should -Be $false -Because "Package name is 'powershell' not 'powershell-preview'" + + # The CORRECT logic (the fix): uses version string + $result = Get-MacOSPackageIdentifierInfo -Version $Version -LTS:$false + $result.IsPreview | Should -Be $true -Because "Version string correctly identifies preview" + $result.PackageIdentifier | Should -Be "com.microsoft.powershell-preview" + } + } +} diff --git a/tools/packaging/packaging.psm1 b/tools/packaging/packaging.psm1 index 8f9cec7179..cddde65aa9 100644 --- a/tools/packaging/packaging.psm1 +++ b/tools/packaging/packaging.psm1 @@ -1371,6 +1371,7 @@ function New-UnixPackage { AppsFolder = $AppsFolder HostArchitecture = $HostArchitecture CurrentLocation = $CurrentLocation + LTS = $LTS } try { @@ -1515,7 +1516,12 @@ function New-MacOsDistributionPackage # Get package ID if not provided if (-not $PackageIdentifier) { - $PackageIdentifier = Get-MacOSPackageId -IsPreview:$IsPreview.IsPresent + if ($IsPreview.IsPresent) { + $PackageIdentifier = 'com.microsoft.powershell-preview' + } + else { + $PackageIdentifier = 'com.microsoft.powershell' + } } # Minimum OS version @@ -1984,7 +1990,9 @@ function New-MacOSPackage [Parameter(Mandatory)] [string]$HostArchitecture, - [string]$CurrentLocation = (Get-Location) + [string]$CurrentLocation = (Get-Location), + + [switch]$LTS ) Write-Log "Creating macOS package using pkgbuild and productbuild..." @@ -2059,8 +2067,10 @@ function New-MacOSPackage Copy-Item -Path "$AppsFolder/*" -Destination $appsInPkg -Recurse -Force } - # Build the component package using pkgbuild - $pkgIdentifier = Get-MacOSPackageId -IsPreview:($Name -like '*-preview') + # Get package identifier info based on version and LTS flag + $packageInfo = Get-MacOSPackageIdentifierInfo -Version $Version -LTS:$LTS + $IsPreview = $packageInfo.IsPreview + $pkgIdentifier = $packageInfo.PackageIdentifier if ($PSCmdlet.ShouldProcess("Build component package with pkgbuild")) { Write-Log "Running pkgbuild to create component package..." @@ -2085,7 +2095,7 @@ function New-MacOSPackage -OutputDirectory $CurrentLocation ` -HostArchitecture $HostArchitecture ` -PackageIdentifier $pkgIdentifier ` - -IsPreview:($Name -like '*-preview') + -IsPreview:$IsPreview return $distributionPackage } @@ -2292,20 +2302,44 @@ function New-ManGzip } } -# Returns the macOS Package Identifier -function Get-MacOSPackageId +<# + .SYNOPSIS + Determines the package identifier and preview status for macOS packages. + .DESCRIPTION + This function determines if a package is a preview build based on the version string + and LTS flag, then returns the appropriate package identifier. + .PARAMETER Version + The version string (e.g., "7.6.0-preview.6" or "7.6.0") + .PARAMETER LTS + Whether this is an LTS build + .OUTPUTS + Hashtable with IsPreview (boolean) and PackageIdentifier (string) properties + .EXAMPLE + Get-MacOSPackageIdentifierInfo -Version "7.6.0-preview.6" -LTS:$false + Returns @{ IsPreview = $true; PackageIdentifier = "com.microsoft.powershell-preview" } +#> +function Get-MacOSPackageIdentifierInfo { param( - [switch] - $IsPreview + [Parameter(Mandatory)] + [string]$Version, + + [switch]$LTS ) - if ($IsPreview.IsPresent) - { - return 'com.microsoft.powershell-preview' + + $IsPreview = Test-IsPreview -Version $Version -IsLTS:$LTS + + # Determine package identifier based on preview status + if ($IsPreview) { + $PackageIdentifier = 'com.microsoft.powershell-preview' } - else - { - return 'com.microsoft.powershell' + else { + $PackageIdentifier = 'com.microsoft.powershell' + } + + return @{ + IsPreview = $IsPreview + PackageIdentifier = $PackageIdentifier } } @@ -2319,8 +2353,9 @@ function New-MacOSLauncher [switch]$LTS ) - $IsPreview = Test-IsPreview -Version $Version -IsLTS:$LTS - $packageId = Get-MacOSPackageId -IsPreview:$IsPreview + $packageInfo = Get-MacOSPackageIdentifierInfo -Version $Version -LTS:$LTS + $IsPreview = $packageInfo.IsPreview + $packageId = $packageInfo.PackageIdentifier # Define folder for launcher application. $suffix = if ($IsPreview) { "-preview" } elseif ($LTS) { "-lts" } From 276989fa55e1e48279467719d873657e337f63a6 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Tue, 20 Jan 2026 14:36:04 -0500 Subject: [PATCH 056/112] Update Microsoft.PowerShell.PSResourceGet version to 1.2.0-rc1 (#26691) --- src/Modules/PSGalleryModules.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/PSGalleryModules.csproj b/src/Modules/PSGalleryModules.csproj index b6e5506e0b..cbc47ef200 100644 --- a/src/Modules/PSGalleryModules.csproj +++ b/src/Modules/PSGalleryModules.csproj @@ -13,7 +13,7 @@ - + From 0931a75d3e4a177c2f1691e50d4124ca229f234b Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Tue, 20 Jan 2026 18:26:59 -0600 Subject: [PATCH 057/112] Fix `buildinfo.json` uploading for preview, LTS, and stable releases (#25571) --- .../templates/release-upload-buildinfo.yml | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/.pipelines/templates/release-upload-buildinfo.yml b/.pipelines/templates/release-upload-buildinfo.yml index bcb2a43396..c470af1fd6 100644 --- a/.pipelines/templates/release-upload-buildinfo.yml +++ b/.pipelines/templates/release-upload-buildinfo.yml @@ -56,6 +56,9 @@ jobs: $dateTime = [datetime]::new($dateTime.Ticks - ($dateTime.Ticks % [timespan]::TicksPerSecond), $dateTime.Kind) $metadata = Get-Content -LiteralPath "$toolsDirectory/metadata.json" -ErrorAction Stop | ConvertFrom-Json + $stableReleaseTag = $metadata.StableReleaseTag -Replace 'v','' + + $currentReleaseTag = $buildInfo.ReleaseTag -Replace 'v','' $stableRelease = $metadata.StableRelease.PublishToChannels $ltsRelease = $metadata.LTSRelease.PublishToChannels @@ -65,40 +68,44 @@ jobs: $buildInfo = $buildInfoJsonContent | ConvertFrom-Json $buildInfo.ReleaseDate = $dateTime + $currentReleaseTag = $buildInfo.ReleaseTag -Replace 'v','' $targetFile = "$ENV:PIPELINE_WORKSPACE/$fileName" ConvertTo-Json -InputObject $buildInfo | Out-File $targetFile -Encoding ascii - if ($stableRelease -or $fileName -eq "preview.json") { - Set-BuildVariable -Name CopyMainBuildInfo -Value YES + if ($fileName -eq "preview.json") { + Set-BuildVariable -Name UploadPreview -Value YES } else { - Set-BuildVariable -Name CopyMainBuildInfo -Value NO + Set-BuildVariable -Name UploadPreview -Value NO } - Set-BuildVariable -Name BuildInfoJsonFile -Value $targetFile - - ## Create 'lts.json' if it's the latest stable and also a LTS release. + Set-BuildVariable -Name PreviewBuildInfoFile -Value $targetFile + ## Create 'lts.json' if marked as a LTS release. if ($fileName -eq "stable.json") { + [System.Management.Automation.SemanticVersion] $stableVersion = $stableReleaseTag + [System.Management.Automation.SemanticVersion] $currentVersion = $currentReleaseTag if ($ltsRelease) { $ltsFile = "$ENV:PIPELINE_WORKSPACE/lts.json" Copy-Item -Path $targetFile -Destination $ltsFile -Force - Set-BuildVariable -Name LtsBuildInfoJsonFile -Value $ltsFile - Set-BuildVariable -Name CopyLTSBuildInfo -Value YES + Set-BuildVariable -Name LTSBuildInfoFile -Value $ltsFile + Set-BuildVariable -Name UploadLTS -Value YES } else { - Set-BuildVariable -Name CopyLTSBuildInfo -Value NO + Set-BuildVariable -Name UploadLTS -Value NO } - $releaseTag = $buildInfo.ReleaseTag - $version = $releaseTag -replace '^v' - $semVersion = [System.Management.Automation.SemanticVersion] $version + ## Only update the stable.json if the current version is greater than the stable version. + if ($currentVersion -gt $stableVersion) { + $versionFile = "$ENV:PIPELINE_WORKSPACE/$($currentVersion.Major)-$($currentVersion.Minor).json" + Copy-Item -Path $targetFile -Destination $versionFile -Force + Set-BuildVariable -Name StableBuildInfoFile -Value $versionFile + Set-BuildVariable -Name UploadStable -Value YES + } else { + Set-BuildVariable -Name UploadStable -Value NO + } - $versionFile = "$ENV:PIPELINE_WORKSPACE/$($semVersion.Major)-$($semVersion.Minor).json" - Copy-Item -Path $targetFile -Destination $versionFile -Force - Set-BuildVariable -Name VersionBuildInfoJsonFile -Value $versionFile - Set-BuildVariable -Name CopyVersionBuildInfo -Value YES } else { - Set-BuildVariable -Name CopyVersionBuildInfo -Value NO + Set-BuildVariable -Name UploadStable -Value NO } displayName: Create json files @@ -116,24 +123,27 @@ jobs: $storageContext = New-AzStorageContext -StorageAccountName $storageAccount -UseConnectedAccount - if ($env:CopyMainBuildInfo -eq 'YES') { - $jsonFile = "$env:BuildInfoJsonFile" + #preview + if ($env:UploadPreview -eq 'YES') { + $jsonFile = "$env:PreviewBuildInfoFile" $blobName = Get-Item $jsonFile | Split-Path -Leaf Write-Verbose -Verbose "Uploading $jsonFile to $containerName/$prefix/$blobName" Set-AzStorageBlobContent -File $jsonFile -Container $containerName -Blob "$prefix/$blobName" -Context $storageContext -Force } - if ($env:CopyLTSBuildInfo -eq 'YES') { - $jsonFile = "$env:LtsBuildInfoJsonFile" + #LTS + if ($env:UploadLTS -eq 'YES') { + $jsonFile = "$env:LTSBuildInfoFile" $blobName = Get-Item $jsonFile | Split-Path -Leaf Write-Verbose -Verbose "Uploading $jsonFile to $containerName/$prefix/$blobName" Set-AzStorageBlobContent -File $jsonFile -Container $containerName -Blob "$prefix/$blobName" -Context $storageContext -Force } - if ($env:CopyVersionBuildInfo -eq 'YES') { - $jsonFile = "$env:VersionBuildInfoJsonFile" + #stable + if ($env:UploadStable -eq 'YES') { + $jsonFile = "$env:StableBuildInfoFile" $blobName = Get-Item $jsonFile | Split-Path -Leaf Write-Verbose -Verbose "Uploading $jsonFile to $containerName/$prefix/$blobName" Set-AzStorageBlobContent -File $jsonFile -Container $containerName -Blob "$prefix/$blobName" -Context $storageContext -Force } - condition: and(succeeded(), eq(variables['CopyMainBuildInfo'], 'YES')) + condition: and(succeeded(), or(eq(variables['UploadPreview'], 'YES'), eq(variables['UploadLTS'], 'YES'), eq(variables['UploadStable'], 'YES'))) From dab346a6b7426e3b7bb4a074ac5b856129d4692d Mon Sep 17 00:00:00 2001 From: PowerShell Team Bot <69177312+pwshBot@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:50:23 -0800 Subject: [PATCH 058/112] Move PowerShell build to depend on .NET SDK 10.0.102 (#26697) Co-authored-by: Dongbo Wang --- global.json | 2 +- ...oft.PowerShell.Commands.Diagnostics.csproj | 2 +- ...soft.PowerShell.Commands.Management.csproj | 2 +- ...crosoft.PowerShell.Commands.Utility.csproj | 2 +- ...crosoft.PowerShell.CoreCLR.Eventing.csproj | 2 +- .../Microsoft.PowerShell.SDK.csproj | 10 +- .../Microsoft.WSMan.Management.csproj | 2 +- .../System.Management.Automation.csproj | 12 +-- test/tools/TestService/TestService.csproj | 2 +- test/tools/WebListener/WebListener.csproj | 2 +- test/xUnit/xUnit.tests.csproj | 2 +- tools/cgmanifest.json | 102 +++++++++--------- 12 files changed, 71 insertions(+), 71 deletions(-) diff --git a/global.json b/global.json index 936a420a57..c2af57a3fe 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "10.0.101" + "version": "10.0.102" } } diff --git a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj index f63196d364..32e0d32988 100644 --- a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj +++ b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj index b2622f0517..7d9d61ede9 100644 --- a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj +++ b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index b99139eb68..5ee999c2d9 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj index feea566528..78232a2f1a 100644 --- a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj +++ b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj index d3446d28c9..161170355e 100644 --- a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj +++ b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj @@ -16,19 +16,19 @@ - - + + - - + + - + diff --git a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj index 178f0473f2..da5ea7ddd0 100644 --- a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj +++ b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 542fa8526f..9b5e21811c 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -32,12 +32,12 @@ - - - - - - + + + + + + diff --git a/test/tools/TestService/TestService.csproj b/test/tools/TestService/TestService.csproj index 03aa569726..af62ecaaca 100644 --- a/test/tools/TestService/TestService.csproj +++ b/test/tools/TestService/TestService.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/tools/WebListener/WebListener.csproj b/test/tools/WebListener/WebListener.csproj index db2fde8595..cbb01a67da 100644 --- a/test/tools/WebListener/WebListener.csproj +++ b/test/tools/WebListener/WebListener.csproj @@ -7,6 +7,6 @@ - + diff --git a/test/xUnit/xUnit.tests.csproj b/test/xUnit/xUnit.tests.csproj index de5b04b8c6..6ce2ee32ce 100644 --- a/test/xUnit/xUnit.tests.csproj +++ b/test/xUnit/xUnit.tests.csproj @@ -29,7 +29,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/tools/cgmanifest.json b/tools/cgmanifest.json index efbc103f25..1001370ade 100644 --- a/tools/cgmanifest.json +++ b/tools/cgmanifest.json @@ -1,5 +1,4 @@ { - "$schema": "https://json.schemastore.org/component-detection-manifest.json", "Registrations": [ { "Component": { @@ -46,7 +45,7 @@ "Type": "nuget", "Nuget": { "Name": "JsonPointer.Net", - "Version": "6.0.0" + "Version": "6.0.1" } }, "DevelopmentDependency": false @@ -86,7 +85,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Bcl.AsyncInterfaces", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -126,7 +125,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Extensions.ObjectPool", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -166,7 +165,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.Registry.AccessControl", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -176,7 +175,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.SystemEvents", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -186,7 +185,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Windows.Compatibility", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -206,7 +205,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -216,7 +215,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -226,7 +225,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -236,7 +235,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x86.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -246,7 +245,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -256,7 +255,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -266,7 +265,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -276,7 +275,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-x64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -286,7 +285,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -296,7 +295,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -306,7 +305,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-x64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -316,7 +315,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-x64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -326,7 +325,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -336,7 +335,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-x64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -356,7 +355,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -366,7 +365,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -376,7 +375,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-x64.runtime.native.System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -436,7 +435,7 @@ "Type": "nuget", "Nuget": { "Name": "System.CodeDom", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -446,7 +445,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition.Registration", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -456,7 +455,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -466,7 +465,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Configuration.ConfigurationManager", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -476,7 +475,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.Odbc", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -486,7 +485,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.OleDb", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -506,7 +505,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.EventLog", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -516,7 +515,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.PerformanceCounter", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -526,7 +525,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.AccountManagement", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -536,7 +535,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.Protocols", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -546,7 +545,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -556,7 +555,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Drawing.Common", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -566,7 +565,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Packaging", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -576,7 +575,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Ports", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -586,7 +585,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Management", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -596,7 +595,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Net.Http.WinHttpHandler", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -606,7 +605,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Reflection.Context", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -616,7 +615,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Runtime.Caching", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -626,7 +625,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Pkcs", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -636,7 +635,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.ProtectedData", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -646,7 +645,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Xml", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -656,7 +655,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Permissions", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -706,7 +705,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Syndication", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -716,7 +715,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceProcess.ServiceController", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -726,7 +725,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Speech", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false @@ -746,10 +745,11 @@ "Type": "nuget", "Nuget": { "Name": "System.Windows.Extensions", - "Version": "10.0.1" + "Version": "10.0.2" } }, "DevelopmentDependency": false } - ] + ], + "$schema": "https://json.schemastore.org/component-detection-manifest.json" } From f5c290ccd6463d63968dda734a282c10a5b499ec Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 22 Jan 2026 04:52:08 +1000 Subject: [PATCH 059/112] Fix WSManInstance COM interface with ResourceURI (#26692) --- src/Microsoft.WSMan.Management/Interop.cs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.WSMan.Management/Interop.cs b/src/Microsoft.WSMan.Management/Interop.cs index e5dd462e2f..3abb938a57 100644 --- a/src/Microsoft.WSMan.Management/Interop.cs +++ b/src/Microsoft.WSMan.Management/Interop.cs @@ -734,18 +734,19 @@ namespace Microsoft.WSMan.Management [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")] string ResourceUri { + // IDL: HRESULT resourceUri (BSTR value); + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1212:PropertyAccessorsMustFollowOrder", Justification = "COM interface defines put_ before get_.")] + [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "resource")] + [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")] + [DispId(1)] + set; + // IDL: HRESULT resourceUri ([out, retval] BSTR* ReturnValue); [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "resource")] [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")] [DispId(1)] [return: MarshalAs(UnmanagedType.BStr)] get; - - // IDL: HRESULT resourceUri (BSTR value); - [SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "resource")] - [SuppressMessage("Microsoft.Design", "CA1056:UriPropertiesShouldNotBeStrings")] - [DispId(1)] - set; } /// AddSelector method of IWSManResourceLocator interface. Add selector to resource locator @@ -818,14 +819,16 @@ namespace Microsoft.WSMan.Management int MustUnderstandOptions { + // IDL: HRESULT MustUnderstandOptions (long value); + + [SuppressMessage("StyleCop.CSharp.OrderingRules", "SA1212:PropertyAccessorsMustFollowOrder", Justification = "COM interface defines put_ before get_.")] + [DispId(7)] + set; + // IDL: HRESULT MustUnderstandOptions ([out, retval] long* ReturnValue); [DispId(7)] get; - // IDL: HRESULT MustUnderstandOptions (long value); - - [DispId(7)] - set; } /// ClearOptions method of IWSManResourceLocator interface. Clear all options From 7f9c7463f203393f06cbc82f4154d1d8420019bb Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 22 Jan 2026 04:55:42 +1000 Subject: [PATCH 060/112] Support TargetObject position in ParserErrors (#26649) --- .../PowerShellCore_format_ps1xml.cs | 29 ++ .../engine/Formatting/ErrorView.Tests.ps1 | 381 ++++++++++++++++++ 2 files changed, 410 insertions(+) diff --git a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs index ef0d506d12..24d99d4dd4 100644 --- a/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs +++ b/src/System.Management.Automation/FormatAndOutput/DefaultFormatters/PowerShellCore_format_ps1xml.cs @@ -1190,8 +1190,37 @@ namespace System.Management.Automation.Runspaces $highlightLine = '' if ($useTargetObject) { $line = $_.TargetObject.LineText.Trim() + $offsetLength = 0 $offsetInLine = 0 + $startColumn = 0 + if ( + ([System.Collections.IDictionary]$_.TargetObject).Contains('StartColumn') -and + [System.Management.Automation.LanguagePrimitives]::TryConvertTo[int]($_.TargetObject.StartColumn, [ref]$startColumn) -and + $null -ne $startColumn -and + $startColumn -gt 0 -and + $startColumn -le $line.Length + ) { + $endColumn = 0 + if (-not ( + ([System.Collections.IDictionary]$_.TargetObject).Contains('EndColumn') -and + [System.Management.Automation.LanguagePrimitives]::TryConvertTo[int]($_.TargetObject.EndColumn, [ref]$endColumn) -and + $null -ne $endColumn -and + $endColumn -gt $startColumn -and + $endColumn -le ($line.Length + 1) + )) { + $endColumn = $line.Length + 1 + } + + # Input is expected to be 1-based index to match the extent positioning + # but we use 0-based indexing below. + $startColumn -= 1 + $endColumn -= 1 + + $highlightLine = "$(" " * $startColumn)$("~" * ($endColumn - $startColumn))" + $offsetLength = $endColumn - $startColumn + $offsetInLine = $startColumn + } } else { $positionMessage = $myinv.PositionMessage.Split($newline) diff --git a/test/powershell/engine/Formatting/ErrorView.Tests.ps1 b/test/powershell/engine/Formatting/ErrorView.Tests.ps1 index 519b9403b9..6af2b5fb50 100644 --- a/test/powershell/engine/Formatting/ErrorView.Tests.ps1 +++ b/test/powershell/engine/Formatting/ErrorView.Tests.ps1 @@ -165,6 +165,387 @@ Describe 'Tests for $ErrorView' -Tag CI { $e | Out-String | Should -BeLike '*ParserError*' } + It 'Parser TargetObject shows Line information' { + $expected = (@( + ": " + "Line |" + " 1 | This is the line with the error" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + Line = 1 + LineText = 'This is the line with the error' + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject shows File information' { + $expected = (@( + ": MyFile.ps1" + "Line |" + " 1 | This is the line with the error" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + File = 'MyFile.ps1' + Line = 1 + LineText = 'This is the line with the error' + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject has StartColumn' { + $expected = (@( + ": " + "Line |" + " 5 | This is the line with the error" + " | ~~~~~~~~~~~~~~" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + + Line = 5 + LineText = 'This is the line with the error' + StartColumn = 18 + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject has StartColumn and EndColumn' { + $expected = (@( + ": " + "Line |" + " 5 | This is the line with the error" + " | ~~~~" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + + Line = 5 + LineText = 'This is the line with the error' + StartColumn = 18 + EndColumn = 22 + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject has StartColumn at end of the line' { + $expected = (@( + ": " + "Line |" + " 5 | This is the line with the error" + " | ~" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + + Line = 5 + LineText = 'This is the line with the error' + StartColumn = 31 + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + + It 'Parser TargetObject has StartColumn at end of the line with EndColumn' { + $expected = (@( + ": " + "Line |" + " 5 | This is the line with the error" + " | ~" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + + Line = 5 + LineText = 'This is the line with the error' + StartColumn = 31 + EndColumn = 32 + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject ignores EndColumn if no StartColumn' { + $expected = (@( + ": " + "Line |" + " 1 | This is the line with the error" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + Line = 1 + LineText = 'This is the line with the error' + EndColumn = 22 + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject converts StartColumn and EndColumn from string' { + $expected = (@( + ": " + "Line |" + " 5 | This is the line with the error" + " | ~~~~" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + + Line = 5 + LineText = 'This is the line with the error' + StartColumn = "18" + EndColumn = "22" + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject ignores StartColumn if it cannot be converted' { + $expected = (@( + ": " + "Line |" + " 1 | This is the line with the error" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + Line = 1 + LineText = 'This is the line with the error' + StartColumn = 'abc' + EndColumn = 22 + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject ignores EndColumn if it cannot be converted' { + $expected = (@( + ": " + "Line |" + " 5 | This is the line with the error" + " | ~~~~~~~~~~~~~~" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + + Line = 5 + LineText = 'This is the line with the error' + StartColumn = 18 + EndColumn = 'abc' + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject ignores StartColumn with invalid value ' -TestCases @( + @{ Value = -1 } + @{ Value = 0 } + @{ Value = 32 } # Beyond end of line + ) { + param ($Value) + + $expected = (@( + ": " + "Line |" + " 1 | This is the line with the error" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + Line = 1 + LineText = 'This is the line with the error' + StartColumn = $Value + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + + It 'Parser TargetObject ignores EndColumn with invalid value ' -TestCases @( + @{ Value = -1 } + @{ Value = 0 } + @{ Value = 17 } # Before StartColumn + @{ Value = 18 } # Equal to StartColumn + @{ Value = 33 } # Beyond end of line + 1 + ) { + param ($Value) + + $expected = (@( + ": " + "Line |" + " 1 | This is the line with the error" + " | ~~~~~~~~~~~~~~" + " | Test Parser Error" + ) -join ([Environment]::NewLine)).TrimEnd() + $e = { + [CmdletBinding()] + param () + + $PSCmdlet.ThrowTerminatingError( + [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new('Test Parser Error'), + 'ParserErrorText', + [System.Management.Automation.ErrorCategory]::ParserError, + @{ + Line = 1 + LineText = 'This is the line with the error' + StartColumn = 18 + EndColumn = $Value + } + ) + ) + } | Should -Throw -PassThru + + $actual = ($e | Out-String).TrimEnd() + $actual | Should -BeExactly $expected + } + It 'Exception thrown from Enumerator.MoveNext in a pipeline shows information' { $e = { $l = [System.Collections.Generic.List[string]] @('one', 'two') From f57643fb9f9dab8e0a6afdad8dc0f5921375cc85 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:10:48 -0500 Subject: [PATCH 061/112] Bump github/codeql-action from 4.31.10 to 4.32.0 (#26726) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 04c5b6cb58..44000b71d5 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5 + uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5 + uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 87b593164a..f2c806750a 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v3.29.5 + uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 with: sarif_file: results.sarif From b3a38645af47133e2b19d8a5bdf535c9f2333b64 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:02:48 -0800 Subject: [PATCH 062/112] Add policy to restrict the `Approved-LowRisk` label (#26728) --- .../policies/labelAdded.approvedLowRisk.yml | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 .github/policies/labelAdded.approvedLowRisk.yml diff --git a/.github/policies/labelAdded.approvedLowRisk.yml b/.github/policies/labelAdded.approvedLowRisk.yml new file mode 100644 index 0000000000..bdeea5265a --- /dev/null +++ b/.github/policies/labelAdded.approvedLowRisk.yml @@ -0,0 +1,48 @@ +id: labelAdded.approvedLowRisk +name: GitOps.PullRequestIssueManagement +description: Remove Approved-LowRisk if applied by an unauthorized user +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + eventResponderTasks: + - description: Remove Approved-LowRisk if label was added by someone not authorized + if: + - payloadType: Pull_Request + - isOpen + - labelAdded: + label: Approved-LowRisk + # Unauthorized = NOT admin AND NOT in explicit allowlist + - not: + or: + - activitySenderHasPermission: + permission: Admin + + # Allowlist (enabled) + - isActivitySender: + user: iSazonov + issueAuthor: False + - isActivitySender: + user: daxian-dbw + issueAuthor: False + + # Allowlist (commented out for now) + # - isActivitySender: + # user: TravisEz13 + # issueAuthor: False + # - isActivitySender: + # user: adityapatwardhan + # issueAuthor: False + # - isActivitySender: + # user: jshigetomi + # issueAuthor: False + then: + - removeLabel: + label: Approved-LowRisk + - addReply: + reply: >- + The `Approved-LowRisk` label is restricted to authorized maintainers and was removed. +onFailure: +onSuccess: From 8489f8fc34669170c5099d0aacd9d3fc8b3ea255 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Thu, 29 Jan 2026 07:06:15 +0900 Subject: [PATCH 063/112] Mark `-NoTypeInformation` as obsolete no-op and evaluate `-IncludeTypeInformation` on by value on Csv cmdlets (#26719) --- .../commands/utility/CsvCommands.cs | 22 +++++-------------- .../resources/CsvCommandStrings.resx | 3 --- .../ConvertTo-Csv.Tests.ps1 | 5 ----- .../Export-Csv.Tests.ps1 | 5 ----- 4 files changed, 6 insertions(+), 29 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs index e84a79b99b..cb45541753 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CsvCommands.cs @@ -45,17 +45,19 @@ namespace Microsoft.PowerShell.Commands public abstract PSObject InputObject { get; set; } /// - /// IncludeTypeInformation : The #TYPE line should be generated. Default is false. Cannot specify with NoTypeInformation. + /// IncludeTypeInformation : The #TYPE line should be generated. Default is false. /// [Parameter] [Alias("ITI")] public SwitchParameter IncludeTypeInformation { get; set; } /// - /// NoTypeInformation : The #TYPE line should not be generated. Default is true. Cannot specify with IncludeTypeInformation. + /// Gets or sets a value indicating whether to suppress the #TYPE line. + /// This parameter is obsolete and has no effect. It is retained for backward compatibility only. /// [Parameter(DontShow = true)] [Alias("NTI")] + [Obsolete("This parameter is obsolete and has no effect. The default behavior is to not include type information. Use -IncludeTypeInformation to include type information.")] public SwitchParameter NoTypeInformation { get; set; } = true; /// @@ -120,18 +122,6 @@ namespace Microsoft.PowerShell.Commands this.ThrowTerminatingError(errorRecord); } - if (this.MyInvocation.BoundParameters.ContainsKey(nameof(IncludeTypeInformation)) && this.MyInvocation.BoundParameters.ContainsKey(nameof(NoTypeInformation))) - { - InvalidOperationException exception = new(CsvCommandStrings.CannotSpecifyIncludeTypeInformationAndNoTypeInformation); - ErrorRecord errorRecord = new(exception, "CannotSpecifyIncludeTypeInformationAndNoTypeInformation", ErrorCategory.InvalidData, null); - this.ThrowTerminatingError(errorRecord); - } - - if (this.MyInvocation.BoundParameters.ContainsKey(nameof(IncludeTypeInformation))) - { - NoTypeInformation = !IncludeTypeInformation; - } - Delimiter = ImportExportCSVHelper.SetDelimiter(this, ParameterSetName, Delimiter, UseCulture); } } @@ -317,7 +307,7 @@ namespace Microsoft.PowerShell.Commands // write headers (row1: typename + row2: column names) if (!_isActuallyAppending && !NoHeader.IsPresent) { - if (NoTypeInformation == false) + if (IncludeTypeInformation) { WriteCsvLine(ExportCsvHelper.GetTypeString(InputObject)); } @@ -742,7 +732,7 @@ namespace Microsoft.PowerShell.Commands if (!NoHeader.IsPresent) { - if (NoTypeInformation == false) + if (IncludeTypeInformation) { WriteCsvLine(ExportCsvHelper.GetTypeString(InputObject)); } diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx index 8c5ded1346..dde1be6ab4 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx @@ -130,9 +130,6 @@ You must specify either the -UseQuotes or -QuoteFields parameters, but not both. - - You must specify either the -IncludeTypeInformation or -NoTypeInformation parameters, but not both. - You must specify either the -Path or -LiteralPath parameters, but not both. diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 index 298b814046..bfba771827 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 @@ -101,11 +101,6 @@ Describe "ConvertTo-Csv" -Tags "CI" { Should -Throw -ErrorId "CannotSpecifyQuoteFieldsAndUseQuotes,Microsoft.PowerShell.Commands.ConvertToCsvCommand" } - It "Does not support -IncludeTypeInformation and -NoTypeInformation at the same time" { - { $testObject | ConvertTo-Csv -IncludeTypeInformation -NoTypeInformation } | - Should -Throw -ErrorId "CannotSpecifyIncludeTypeInformationAndNoTypeInformation,Microsoft.PowerShell.Commands.ConvertToCsvCommand" - } - Context "QuoteFields parameter" { It "QuoteFields" { # Use 'FiRstCoLumn' to test case insensitivity diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 index ea05d66d39..aae457f2f8 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 @@ -91,11 +91,6 @@ Describe "Export-Csv" -Tags "CI" { $results[0] | Should -BeExactly "#TYPE System.String" } - It "Does not support -IncludeTypeInformation and -NoTypeInformation at the same time" { - { $testObject | Export-Csv -Path $testCsv -IncludeTypeInformation -NoTypeInformation } | - Should -Throw -ErrorId "CannotSpecifyIncludeTypeInformationAndNoTypeInformation,Microsoft.PowerShell.Commands.ExportCsvCommand" - } - It "Should support -LiteralPath parameter" { $testObject | Export-Csv -LiteralPath $testCsv $results = Import-Csv -Path $testCsv From 34f6e267a4a779616f0f441ee034dbf4747c482b Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Mon, 2 Feb 2026 19:45:16 -0500 Subject: [PATCH 064/112] Add CodeQL suppressions for NativeCommandProcessor (#26729) --- .../engine/NativeCommandProcessor.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/NativeCommandProcessor.cs b/src/System.Management.Automation/engine/NativeCommandProcessor.cs index 75e3b46881..145fe968fd 100644 --- a/src/System.Management.Automation/engine/NativeCommandProcessor.cs +++ b/src/System.Management.Automation/engine/NativeCommandProcessor.cs @@ -831,6 +831,7 @@ namespace System.Management.Automation bool useSpecialArgumentPassing = UseSpecialArgumentPassing(oldFileName); if (useSpecialArgumentPassing) { + // codeql[cs/microsoft/command-line-injection] - This is expected PowerShell behavior where user inputted paths are supported for the context of this method and the path portion of the argument is escaped. The user assumes trust for the file path specified on the user's system to start process for, and in the case of remoting, restricted remoting security guidelines should be used. startInfo.Arguments = "\"" + oldFileName + "\" " + startInfo.Arguments; } else @@ -855,7 +856,7 @@ namespace System.Management.Automation startInfo.ArgumentList.RemoveAt(0); } - // codeql[cs/microsoft/command-line-injection-shell-execution] - This is expected Poweshell behavior where user inputted paths are supported for the context of this method. The user assumes trust for the file path specified on the user's system to retrieve process info for, and in the case of remoting, restricted remoting security guidelines should be used. + // codeql[cs/microsoft/command-line-injection-shell-execution] - This is expected PowerShell behavior where user inputted paths are supported for the context of this method. The user assumes trust for the file path specified on the user's system to retrieve process info for, and in the case of remoting, restricted remoting security guidelines should be used. startInfo.FileName = oldFileName; } } @@ -1607,7 +1608,7 @@ namespace System.Management.Automation { var startInfo = new ProcessStartInfo { - // codeql[cs/microsoft/command-line-injection-shell-execution] - This is expected Poweshell behavior where user inputted paths are supported for the context of this method. The user assumes trust for the file path specified on the user's system to retrieve process info for, and in the case of remoting, restricted remoting security guidelines should be used. + // codeql[cs/microsoft/command-line-injection-shell-execution] - This is expected PowerShell behavior where user inputted paths are supported for the context of this method. The user assumes trust for the file path specified on the user's system to retrieve process info for, and in the case of remoting, restricted remoting security guidelines should be used. FileName = this.Path }; From 467059c9cbe3e40fb1026312b8c3f9711260938f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:46:51 -0500 Subject: [PATCH 065/112] Bump github/codeql-action from 4.32.0 to 4.32.1 (#26741) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 44000b71d5..b56d2d6635 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 + uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 + uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index f2c806750a..d1d974f8ef 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 + uses: github/codeql-action/upload-sarif@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5 with: sarif_file: results.sarif From d54977441f96cebb2b93fbb2364010d02fb00ffa Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 4 Feb 2026 11:38:48 -0800 Subject: [PATCH 066/112] Update `Microsoft.PowerShell.Native` package version (#26746) --- .../System.Management.Automation.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 9b5e21811c..8c77c770a8 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -40,7 +40,7 @@ - + From dd4f2fecfaf2e9dc1fbb8831fa47c8b0907c03a3 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Fri, 6 Feb 2026 08:51:22 +0900 Subject: [PATCH 067/112] Add comprehensive scalar type tests for `ConvertTo-Json` (#26736) --- .../ConvertTo-Json.Tests.ps1 | 429 ++++++++++++++++++ 1 file changed, 429 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 1f2abe05c6..4ac0818faf 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -156,4 +156,433 @@ Describe 'ConvertTo-Json' -tags "CI" { $actual = ConvertTo-Json -Compress -InputObject $obj $actual | Should -Be '{"Positive":18446744073709551615,"Negative":-18446744073709551615}' } + #region Comprehensive Scalar Type Tests (Phase 1) + # Test coverage for ConvertTo-Json scalar serialization + # Covers: Pipeline vs InputObject, ETS vs no ETS, all primitive and special types + + Context 'Primitive scalar types' { + It 'Should serialize value correctly via Pipeline and InputObject' -TestCases @( + # Byte types + @{ TypeName = 'byte'; Value = [byte]0; Expected = '0' } + @{ TypeName = 'byte'; Value = [byte]255; Expected = '255' } + @{ TypeName = 'sbyte'; Value = [sbyte]-128; Expected = '-128' } + @{ TypeName = 'sbyte'; Value = [sbyte]127; Expected = '127' } + # Short types + @{ TypeName = 'short'; Value = [short]-32768; Expected = '-32768' } + @{ TypeName = 'short'; Value = [short]32767; Expected = '32767' } + @{ TypeName = 'ushort'; Value = [ushort]0; Expected = '0' } + @{ TypeName = 'ushort'; Value = [ushort]65535; Expected = '65535' } + # Integer types + @{ TypeName = 'int'; Value = 42; Expected = '42' } + @{ TypeName = 'int'; Value = -42; Expected = '-42' } + @{ TypeName = 'int'; Value = 0; Expected = '0' } + @{ TypeName = 'int'; Value = [int]::MaxValue; Expected = '2147483647' } + @{ TypeName = 'int'; Value = [int]::MinValue; Expected = '-2147483648' } + @{ TypeName = 'uint'; Value = [uint]0; Expected = '0' } + @{ TypeName = 'uint'; Value = [uint]::MaxValue; Expected = '4294967295' } + # Long types + @{ TypeName = 'long'; Value = [long]::MaxValue; Expected = '9223372036854775807' } + @{ TypeName = 'long'; Value = [long]::MinValue; Expected = '-9223372036854775808' } + @{ TypeName = 'ulong'; Value = [ulong]0; Expected = '0' } + @{ TypeName = 'ulong'; Value = [ulong]::MaxValue; Expected = '18446744073709551615' } + # Floating-point types + @{ TypeName = 'float'; Value = [float]3.14; Expected = '3.14' } + @{ TypeName = 'float'; Value = [float]::NaN; Expected = '"NaN"' } + @{ TypeName = 'float'; Value = [float]::PositiveInfinity; Expected = '"Infinity"' } + @{ TypeName = 'float'; Value = [float]::NegativeInfinity; Expected = '"-Infinity"' } + @{ TypeName = 'double'; Value = 3.14159; Expected = '3.14159' } + @{ TypeName = 'double'; Value = -3.14159; Expected = '-3.14159' } + @{ TypeName = 'double'; Value = 0.0; Expected = '0.0' } + @{ TypeName = 'double'; Value = [double]::NaN; Expected = '"NaN"' } + @{ TypeName = 'double'; Value = [double]::PositiveInfinity; Expected = '"Infinity"' } + @{ TypeName = 'double'; Value = [double]::NegativeInfinity; Expected = '"-Infinity"' } + @{ TypeName = 'decimal'; Value = 123.456d; Expected = '123.456' } + # BigInteger + @{ TypeName = 'BigInteger'; Value = 18446744073709551615n; Expected = '18446744073709551615' } + # Boolean + @{ TypeName = 'bool'; Value = $true; Expected = 'true' } + @{ TypeName = 'bool'; Value = $false; Expected = 'false' } + # Null + @{ TypeName = 'null'; Value = $null; Expected = 'null' } + ) { + param($TypeName, $Value, $Expected) + $jsonPipeline = $Value | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $Value -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should include ETS properties on ' -TestCases @( + @{ TypeName = 'int'; Value = 42; Expected = '{"value":42,"MyProp":"test"}' } + @{ TypeName = 'double'; Value = 3.14; Expected = '{"value":3.14,"MyProp":"test"}' } + ) { + param($TypeName, $Value, $Expected) + $valueWithEts = Add-Member -InputObject $Value -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $valueWithEts | ConvertTo-Json -Compress + $json | Should -BeExactly $Expected + } + } + + Context 'String scalar types' { + It 'Should serialize string correctly via Pipeline and InputObject' -TestCases @( + @{ Description = 'regular'; Value = 'hello'; Expected = '"hello"' } + @{ Description = 'empty'; Value = ''; Expected = '""' } + @{ Description = 'with spaces'; Value = 'hello world'; Expected = '"hello world"' } + @{ Description = 'with newline'; Value = "line1`nline2"; Expected = '"line1\nline2"' } + @{ Description = 'with tab'; Value = "col1`tcol2"; Expected = '"col1\tcol2"' } + @{ Description = 'with quotes'; Value = 'say "hello"'; Expected = '"say \"hello\""' } + @{ Description = 'with backslash'; Value = 'c:\path'; Expected = '"c:\\path"' } + @{ Description = 'unicode'; Value = '???'; Expected = '"???"' } + @{ Description = 'emoji'; Value = '??'; Expected = '"??"' } + ) { + param($Description, $Value, $Expected) + $jsonPipeline = $Value | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $Value -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should ignore ETS properties on string' { + $str = Add-Member -InputObject 'hello' -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $str | ConvertTo-Json -Compress + $json | Should -BeExactly '"hello"' + } + } + + Context 'DateTime and related types' { + It 'Should serialize DateTime with UTC kind via Pipeline and InputObject' { + $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) + $jsonPipeline = $dt | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dt -Compress + $jsonPipeline | Should -BeExactly '"2024-06-15T10:30:00Z"' + $jsonInputObject | Should -BeExactly '"2024-06-15T10:30:00Z"' + } + + It 'Should serialize DateTime with Local kind' { + $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Local) + $json = $dt | ConvertTo-Json -Compress + $offset = $dt.ToString('zzz') + $expected = '"2024-06-15T10:30:00' + $offset + '"' + $json | Should -BeExactly $expected + } + + It 'Should serialize DateTime with Unspecified kind via Pipeline and InputObject' { + $dt = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Unspecified) + $jsonPipeline = $dt | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dt -Compress + $jsonPipeline | Should -BeExactly '"2024-06-15T10:30:00"' + $jsonInputObject | Should -BeExactly '"2024-06-15T10:30:00"' + } + + It 'Should serialize DateTimeOffset correctly via Pipeline and InputObject' { + $dto = [DateTimeOffset]::new(2024, 6, 15, 10, 30, 0, [TimeSpan]::FromHours(9)) + $jsonPipeline = $dto | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dto -Compress + $jsonPipeline | Should -BeExactly '"2024-06-15T10:30:00+09:00"' + $jsonInputObject | Should -BeExactly '"2024-06-15T10:30:00+09:00"' + } + + It 'Should serialize DateOnly as object with properties' { + $d = [DateOnly]::new(2024, 6, 15) + $json = $d | ConvertTo-Json -Compress + $json | Should -BeExactly '{"Year":2024,"Month":6,"Day":15,"DayOfWeek":6,"DayOfYear":167,"DayNumber":739051}' + } + + It 'Should serialize TimeOnly as object with properties' { + $t = [TimeOnly]::new(10, 30, 45) + $json = $t | ConvertTo-Json -Compress + $json | Should -BeExactly '{"Hour":10,"Minute":30,"Second":45,"Millisecond":0,"Microsecond":0,"Nanosecond":0,"Ticks":378450000000}' + } + + It 'Should serialize TimeSpan as object with properties' { + $ts = [TimeSpan]::new(1, 2, 3, 4, 5) + $json = $ts | ConvertTo-Json -Compress + $json | Should -BeExactly '{"Ticks":937840050000,"Days":1,"Hours":2,"Milliseconds":5,"Microseconds":0,"Nanoseconds":0,"Minutes":3,"Seconds":4,"TotalDays":1.0854630208333333,"TotalHours":26.0511125,"TotalMilliseconds":93784005.0,"TotalMicroseconds":93784005000.0,"TotalNanoseconds":93784005000000.0,"TotalMinutes":1563.06675,"TotalSeconds":93784.005}' + } + + It 'Should ignore ETS properties on DateTime' { + $dt = [DateTime]::new(2024, 6, 15, 0, 0, 0, [DateTimeKind]::Utc) + $dt = Add-Member -InputObject $dt -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $dt | ConvertTo-Json -Compress + $json | Should -BeExactly '"2024-06-15T00:00:00Z"' + } + + It 'Should include ETS properties on DateTimeOffset' { + $dto = [DateTimeOffset]::new(2024, 6, 15, 10, 30, 0, [TimeSpan]::Zero) + $dto = Add-Member -InputObject $dto -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $dto | ConvertTo-Json -Compress + $json | Should -BeExactly '{"value":"2024-06-15T10:30:00+00:00","MyProp":"test"}' + } + + It 'Should include ETS properties on DateOnly' { + $d = [DateOnly]::new(2024, 6, 15) + $d = Add-Member -InputObject $d -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $d | ConvertTo-Json -Compress + $json | Should -BeExactly '{"Year":2024,"Month":6,"Day":15,"DayOfWeek":6,"DayOfYear":167,"DayNumber":739051,"MyProp":"test"}' + } + + It 'Should include ETS properties on TimeOnly' { + $t = [TimeOnly]::new(10, 30, 45) + $t = Add-Member -InputObject $t -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $t | ConvertTo-Json -Compress + $json | Should -BeExactly '{"Hour":10,"Minute":30,"Second":45,"Millisecond":0,"Microsecond":0,"Nanosecond":0,"Ticks":378450000000,"MyProp":"test"}' + } + } + + Context 'Guid type' { + It 'Should serialize Guid as string via InputObject' { + $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') + $json = ConvertTo-Json -InputObject $guid -Compress + $json | Should -BeExactly '"12345678-1234-1234-1234-123456789abc"' + } + + It 'Should serialize Guid with Extended properties via Pipeline' { + $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') + $json = $guid | ConvertTo-Json -Compress + $json | Should -BeExactly '{"value":"12345678-1234-1234-1234-123456789abc","Guid":"12345678-1234-1234-1234-123456789abc"}' + } + + It 'Should serialize empty Guid correctly via InputObject' { + $json = ConvertTo-Json -InputObject ([Guid]::Empty) -Compress + $json | Should -BeExactly '"00000000-0000-0000-0000-000000000000"' + } + + It 'Should include ETS properties on Guid via Pipeline' { + $guid = [Guid]::new('12345678-1234-1234-1234-123456789abc') + $guid = Add-Member -InputObject $guid -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $guid | ConvertTo-Json -Compress + $json | Should -BeExactly '{"value":"12345678-1234-1234-1234-123456789abc","MyProp":"test","Guid":"12345678-1234-1234-1234-123456789abc"}' + } + } + + Context 'Uri type' { + It 'Should serialize Uri correctly via Pipeline and InputObject' -TestCases @( + @{ Description = 'http'; UriString = 'http://example.com'; Expected = '"http://example.com"' } + @{ Description = 'https with path'; UriString = 'https://example.com/path'; Expected = '"https://example.com/path"' } + @{ Description = 'with query'; UriString = 'https://example.com/search?q=test'; Expected = '"https://example.com/search?q=test"' } + @{ Description = 'file'; UriString = 'file:///c:/temp/file.txt'; Expected = '"file:///c:/temp/file.txt"' } + ) { + param($Description, $UriString, $Expected) + $uri = [Uri]$UriString + $jsonPipeline = $uri | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $uri -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should include ETS properties on Uri' { + $uri = [Uri]'https://example.com' + $uri = Add-Member -InputObject $uri -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $uri | ConvertTo-Json -Compress + $json | Should -BeExactly '{"value":"https://example.com","MyProp":"test"}' + } + } + + Context 'Enum types' { + It 'Should serialize enum :: as via Pipeline and InputObject' -TestCases @( + @{ EnumType = 'System.DayOfWeek'; Value = 'Sunday'; Expected = '0' } + @{ EnumType = 'System.DayOfWeek'; Value = 'Monday'; Expected = '1' } + @{ EnumType = 'System.DayOfWeek'; Value = 'Saturday'; Expected = '6' } + @{ EnumType = 'System.ConsoleColor'; Value = 'Red'; Expected = '12' } + @{ EnumType = 'System.IO.FileAttributes'; Value = 'ReadOnly'; Expected = '1' } + @{ EnumType = 'System.IO.FileAttributes'; Value = 'Hidden'; Expected = '2' } + ) { + param($EnumType, $Value, $Expected) + $enumValue = [Enum]::Parse($EnumType, $Value) + $jsonPipeline = $enumValue | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $enumValue -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should serialize enum as "" with -EnumsAsStrings' -TestCases @( + @{ EnumType = 'System.DayOfWeek'; Value = 'Sunday'; Expected = 'Sunday' } + @{ EnumType = 'System.DayOfWeek'; Value = 'Monday'; Expected = 'Monday' } + @{ EnumType = 'System.ConsoleColor'; Value = 'Red'; Expected = 'Red' } + ) { + param($EnumType, $Value, $Expected) + $enumValue = [Enum]::Parse($EnumType, $Value) + $json = $enumValue | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly "`"$Expected`"" + } + + It 'Should serialize flags enum correctly via Pipeline and InputObject' { + $flags = [System.IO.FileAttributes]::ReadOnly -bor [System.IO.FileAttributes]::Hidden + $jsonPipeline = $flags | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $flags -Compress + $jsonPipeline | Should -BeExactly '3' + $jsonInputObject | Should -BeExactly '3' + } + + It 'Should serialize flags enum as string with -EnumsAsStrings' { + $flags = [System.IO.FileAttributes]::ReadOnly -bor [System.IO.FileAttributes]::Hidden + $json = $flags | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly '"ReadOnly, Hidden"' + } + + It 'Should include ETS properties on Enum' { + $enum = Add-Member -InputObject ([DayOfWeek]::Monday) -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $enum | ConvertTo-Json -Compress + $json | Should -BeExactly '{"value":1,"MyProp":"test"}' + } + } + + Context 'IPAddress type' { + It 'Should serialize IPAddress v4 correctly via InputObject' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $json = ConvertTo-Json -InputObject $ip -Compress + $json | Should -BeExactly '{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952}' + } + + It 'Should serialize IPAddress v4 correctly via Pipeline' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $json = $ip | ConvertTo-Json -Compress + $json | Should -BeExactly '{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952,"IPAddressToString":"192.168.1.1"}' + } + + It 'Should serialize IPAddress v6 correctly via InputObject' { + $ip = [System.Net.IPAddress]::Parse('::1') + $json = ConvertTo-Json -InputObject $ip -Compress + $json | Should -BeExactly '{"AddressFamily":23,"ScopeId":0,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":null}' + } + + It 'Should serialize IPAddress v6 correctly via Pipeline' { + $ip = [System.Net.IPAddress]::Parse('::1') + $json = $ip | ConvertTo-Json -Compress + $json | Should -BeExactly '{"AddressFamily":23,"ScopeId":0,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":null,"IPAddressToString":"::1"}' + } + + It 'Should include ETS properties on IPAddress' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $ip = Add-Member -InputObject $ip -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = $ip | ConvertTo-Json -Compress + $json | Should -BeExactly '{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952,"MyProp":"test","IPAddressToString":"192.168.1.1"}' + } + } + + Context 'Scalars as elements of arrays' { + It 'Should serialize array of correctly via Pipeline and InputObject' -TestCases @( + @{ TypeName = 'int'; Values = @(1, 2, 3); Expected = '[1,2,3]' } + @{ TypeName = 'string'; Values = @('a', 'b', 'c'); Expected = '["a","b","c"]' } + @{ TypeName = 'double'; Values = @(1.1, 2.2, 3.3); Expected = '[1.1,2.2,3.3]' } + ) { + param($TypeName, $Values, $Expected) + $jsonPipeline = $Values | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $Values -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + # Note: bool array test uses InputObject only because $true/$false are singletons + # and ETS properties added in other tests would affect Pipeline serialization + It 'Should serialize array of bool correctly via InputObject' { + $bools = @($true, $false, $true) + $json = ConvertTo-Json -InputObject $bools -Compress + $json | Should -BeExactly '[true,false,true]' + } + + It 'Should serialize array of Guid with Extended properties via Pipeline' { + $guids = @( + [Guid]'11111111-1111-1111-1111-111111111111', + [Guid]'22222222-2222-2222-2222-222222222222' + ) + $json = $guids | ConvertTo-Json -Compress + $json | Should -BeExactly '[{"value":"11111111-1111-1111-1111-111111111111","Guid":"11111111-1111-1111-1111-111111111111"},{"value":"22222222-2222-2222-2222-222222222222","Guid":"22222222-2222-2222-2222-222222222222"}]' + } + + It 'Should serialize array of enum correctly via Pipeline and InputObject' { + $enums = @([DayOfWeek]::Monday, [DayOfWeek]::Wednesday, [DayOfWeek]::Friday) + $jsonPipeline = $enums | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $enums -Compress + $jsonPipeline | Should -BeExactly '[1,3,5]' + $jsonInputObject | Should -BeExactly '[1,3,5]' + } + + It 'Should serialize array of enum as strings with -EnumsAsStrings' { + $enums = @([DayOfWeek]::Monday, [DayOfWeek]::Wednesday, [DayOfWeek]::Friday) + $json = $enums | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly '["Monday","Wednesday","Friday"]' + } + + # Note: mixed array test uses InputObject only due to $true singleton issue + It 'Should serialize mixed type array correctly via InputObject' { + $mixed = @(1, 'two', $true, 3.14) + $json = ConvertTo-Json -InputObject $mixed -Compress + $json | Should -BeExactly '[1,"two",true,3.14]' + } + + It 'Should serialize array with null elements correctly' { + $arr = @(1, $null, 'three') + $json = $arr | ConvertTo-Json -Compress + $json | Should -BeExactly '[1,null,"three"]' + } + + It 'Should include ETS properties on array via InputObject' { + $arr = @(1, 2, 3) + $arr = Add-Member -InputObject $arr -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = ConvertTo-Json -InputObject $arr -Compress + $json | Should -BeExactly '{"value":[1,2,3],"MyProp":"test"}' + } + } + + Context 'Scalars as values in hashtables and PSCustomObject' { + It 'Should serialize hashtable with scalar values correctly via Pipeline and InputObject' { + $hash = [ordered]@{ + intVal = 42 + strVal = 'hello' + boolVal = $true + doubleVal = 3.14 + nullVal = $null + } + $expected = '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14,"nullVal":null}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with scalar values correctly via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + intVal = 42 + strVal = 'hello' + boolVal = $true + doubleVal = 3.14 + } + $expected = '{"intVal":42,"strVal":"hello","boolVal":true,"doubleVal":3.14}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize hashtable with value correctly' -TestCases @( + @{ TypeName = 'DateTime'; Value = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc); Expected = '{"val":"2024-06-15T10:30:00Z"}' } + @{ TypeName = 'Guid'; Value = [Guid]'12345678-1234-1234-1234-123456789abc'; Expected = '{"val":"12345678-1234-1234-1234-123456789abc"}' } + @{ TypeName = 'Enum'; Value = [DayOfWeek]::Monday; Expected = '{"val":1}' } + @{ TypeName = 'Uri'; Value = [Uri]'https://example.com'; Expected = '{"val":"https://example.com"}' } + @{ TypeName = 'BigInteger'; Value = 18446744073709551615n; Expected = '{"val":18446744073709551615}' } + ) { + param($TypeName, $Value, $Expected) + $hash = @{ val = $Value } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $Expected + $jsonInputObject | Should -BeExactly $Expected + } + + It 'Should serialize hashtable with enum as string correctly' { + $hash = @{ day = [DayOfWeek]::Monday } + $json = $hash | ConvertTo-Json -Compress -EnumsAsStrings + $json | Should -BeExactly '{"day":"Monday"}' + } + + It 'Should include ETS properties on hashtable via InputObject' { + $hash = @{ a = 1 } + $hash = Add-Member -InputObject $hash -MemberType NoteProperty -Name MyProp -Value 'test' -PassThru + $json = ConvertTo-Json -InputObject $hash -Compress + $json | Should -BeExactly '{"a":1,"MyProp":"test"}' + } + } + + #endregion Comprehensive Scalar Type Tests (Phase 1) } From 438254412760ff24cdfebeed09bfeb3a24882866 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Sat, 7 Feb 2026 02:41:03 +0900 Subject: [PATCH 068/112] Add comprehensive array and dictionary tests for `ConvertTo-Json` (#26742) --- .../ConvertTo-Json.Tests.ps1 | 457 ++++++++++++++++++ 1 file changed, 457 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 4ac0818faf..d368695333 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -585,4 +585,461 @@ Describe 'ConvertTo-Json' -tags "CI" { } #endregion Comprehensive Scalar Type Tests (Phase 1) + + #region Comprehensive Array and Dictionary Tests (Phase 2) + # Test coverage for ConvertTo-Json array and dictionary serialization + # Covers: Pipeline vs InputObject, ETS vs no ETS, nested structures + + Context 'Array basic serialization' { + It 'Should serialize empty array correctly via Pipeline and InputObject' { + $arr = @() + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[]' + $jsonInputObject | Should -BeExactly '[]' + } + + It 'Should serialize single element array correctly via Pipeline and InputObject' { + $arr = @(42) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[42]' + $jsonInputObject | Should -BeExactly '[42]' + } + + It 'Should serialize multi-element array correctly via Pipeline and InputObject' { + $arr = @(1, 2, 3, 4, 5) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[1,2,3,4,5]' + $jsonInputObject | Should -BeExactly '[1,2,3,4,5]' + } + + It 'Should serialize string array correctly via Pipeline and InputObject' { + $arr = @('apple', 'banana', 'cherry') + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '["apple","banana","cherry"]' + $jsonInputObject | Should -BeExactly '["apple","banana","cherry"]' + } + + It 'Should serialize typed array correctly via Pipeline and InputObject' { + [int[]]$arr = @(10, 20, 30) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[10,20,30]' + $jsonInputObject | Should -BeExactly '[10,20,30]' + } + + It 'Should serialize array with single null element correctly via Pipeline and InputObject' { + $arr = @($null) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[null]' + $jsonInputObject | Should -BeExactly '[null]' + } + + It 'Should serialize array with multiple null elements correctly via Pipeline and InputObject' { + $arr = @($null, $null, $null) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[null,null,null]' + $jsonInputObject | Should -BeExactly '[null,null,null]' + } + } + + Context 'Nested arrays' { + It 'Should serialize 2D array correctly via Pipeline and InputObject' { + $arr = @(@(1, 2), @(3, 4)) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[[1,2],[3,4]]' + $jsonInputObject | Should -BeExactly '[[1,2],[3,4]]' + } + + It 'Should serialize 3D array correctly via Pipeline and InputObject' { + $arr = @(@(@(1, 2), @(3, 4)), @(@(5, 6), @(7, 8))) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[[[1,2],[3,4]],[[5,6],[7,8]]]' + $jsonInputObject | Should -BeExactly '[[[1,2],[3,4]],[[5,6],[7,8]]]' + } + + It 'Should serialize jagged array correctly via Pipeline and InputObject' { + $arr = @(@(1), @(2, 3), @(4, 5, 6)) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[[1],[2,3],[4,5,6]]' + $jsonInputObject | Should -BeExactly '[[1],[2,3],[4,5,6]]' + } + + It 'Should serialize array containing empty arrays correctly via Pipeline and InputObject' { + $arr = @(@(), @(1), @()) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[[],[1],[]]' + $jsonInputObject | Should -BeExactly '[[],[1],[]]' + } + + It 'Should serialize deeply nested array with Depth limit using ToString via Pipeline and InputObject' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $arr = ,(,(,(,($ip)))) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress -Depth 2 + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress -Depth 2 + $jsonPipeline | Should -BeExactly '[[["192.168.1.1"]]]' + $jsonInputObject | Should -BeExactly '[[["192.168.1.1"]]]' + } + + It 'Should serialize deeply nested array with sufficient Depth as full object via Pipeline and InputObject' { + $ip = [System.Net.IPAddress]::Parse('192.168.1.1') + $arr = ,(,(,(,($ip)))) + $expected = '[[[[{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952}]]]]' + $jsonPipeline = ,$arr | ConvertTo-Json -Compress -Depth 10 + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress -Depth 10 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Array with mixed content types' { + It 'Should serialize array with mixed scalars correctly via Pipeline and InputObject' { + $arr = @(1, 'two', 3.14, $true, $null) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[1,"two",3.14,true,null]' + $jsonInputObject | Should -BeExactly '[1,"two",3.14,true,null]' + } + + It 'Should serialize array with nested array and scalars correctly via Pipeline and InputObject' { + $arr = @(1, @(2, 3), 4) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[1,[2,3],4]' + $jsonInputObject | Should -BeExactly '[1,[2,3],4]' + } + + It 'Should serialize array with PSCustomObject elements correctly via Pipeline and InputObject' { + $arr = @([PSCustomObject]@{x = 1}, [PSCustomObject]@{y = 2}) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[{"x":1},{"y":2}]' + $jsonInputObject | Should -BeExactly '[{"x":1},{"y":2}]' + } + + It 'Should serialize array with DateTime elements correctly via Pipeline and InputObject' { + $date1 = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) + $date2 = [DateTime]::new(2024, 12, 25, 0, 0, 0, [DateTimeKind]::Utc) + $arr = @($date1, $date2) + $expected = '["2024-06-15T10:30:00Z","2024-12-25T00:00:00Z"]' + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize array with Guid elements correctly via Pipeline and InputObject' { + $guid1 = [Guid]'12345678-1234-1234-1234-123456789abc' + $guid2 = [Guid]'87654321-4321-4321-4321-cba987654321' + $arr = @($guid1, $guid2) + $expected = '["12345678-1234-1234-1234-123456789abc","87654321-4321-4321-4321-cba987654321"]' + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize array with enum elements correctly via Pipeline and InputObject' { + $arr = @([DayOfWeek]::Monday, [DayOfWeek]::Friday) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '[1,5]' + $jsonInputObject | Should -BeExactly '[1,5]' + } + + It 'Should serialize array with enum as string correctly via Pipeline and InputObject' { + $arr = @([DayOfWeek]::Monday, [DayOfWeek]::Friday) + $jsonPipeline = ,$arr | ConvertTo-Json -Compress -EnumsAsStrings + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress -EnumsAsStrings + $jsonPipeline | Should -BeExactly '["Monday","Friday"]' + $jsonInputObject | Should -BeExactly '["Monday","Friday"]' + } + } + + Context 'Array ETS properties' { + It 'Should include ETS properties on array via Pipeline and InputObject' { + $arr = @(1, 2, 3) + $arr = Add-Member -InputObject $arr -MemberType NoteProperty -Name ArrayName -Value 'MyArray' -PassThru + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '{"value":[1,2,3],"ArrayName":"MyArray"}' + $jsonInputObject | Should -BeExactly '{"value":[1,2,3],"ArrayName":"MyArray"}' + } + + It 'Should include multiple ETS properties on array via Pipeline and InputObject' { + $arr = @('a', 'b') + $arr = Add-Member -InputObject $arr -MemberType NoteProperty -Name Prop1 -Value 'val1' -PassThru + $arr = Add-Member -InputObject $arr -MemberType NoteProperty -Name Prop2 -Value 'val2' -PassThru + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly '{"value":["a","b"],"Prop1":"val1","Prop2":"val2"}' + $jsonInputObject | Should -BeExactly '{"value":["a","b"],"Prop1":"val1","Prop2":"val2"}' + } + } + + Context 'Hashtable basic serialization' { + It 'Should serialize empty hashtable correctly via Pipeline and InputObject' { + $hash = @{} + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{}' + $jsonInputObject | Should -BeExactly '{}' + } + + It 'Should serialize single key hashtable correctly via Pipeline and InputObject' { + $hash = @{ key = 'value' } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"key":"value"}' + $jsonInputObject | Should -BeExactly '{"key":"value"}' + } + + It 'Should serialize hashtable with null value correctly via Pipeline and InputObject' { + $hash = @{ nullKey = $null } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"nullKey":null}' + $jsonInputObject | Should -BeExactly '{"nullKey":null}' + } + + It 'Should serialize hashtable with various scalar types correctly via Pipeline and InputObject' { + $hash = [ordered]@{ + intKey = 42 + strKey = 'hello' + boolKey = $true + doubleKey = 3.14 + } + $expected = '{"intKey":42,"strKey":"hello","boolKey":true,"doubleKey":3.14}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'OrderedDictionary serialization' { + It 'Should preserve order in OrderedDictionary via Pipeline and InputObject' { + $ordered = [ordered]@{ + z = 1 + a = 2 + m = 3 + } + $expected = '{"z":1,"a":2,"m":3}' + $jsonPipeline = $ordered | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $ordered -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize large OrderedDictionary preserving order via Pipeline and InputObject' { + $ordered = [ordered]@{} + 1..5 | ForEach-Object { $ordered["key$_"] = $_ } + $expected = '{"key1":1,"key2":2,"key3":3,"key4":4,"key5":5}' + $jsonPipeline = $ordered | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $ordered -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Nested dictionaries' { + It 'Should serialize nested hashtable correctly via Pipeline and InputObject' { + $hash = @{ + outer = @{ + inner = 'value' + } + } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"outer":{"inner":"value"}}' + $jsonInputObject | Should -BeExactly '{"outer":{"inner":"value"}}' + } + + It 'Should serialize deeply nested hashtable correctly via Pipeline and InputObject' { + $hash = @{ + level1 = @{ + level2 = @{ + level3 = 'deep' + } + } + } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"level1":{"level2":{"level3":"deep"}}}' + $jsonInputObject | Should -BeExactly '{"level1":{"level2":{"level3":"deep"}}}' + } + + It 'Should serialize nested hashtable with Depth limit via Pipeline and InputObject' { + $hash = @{ + level1 = @{ + level2 = @{ + level3 = 'deep' + } + } + } + $jsonPipeline = $hash | ConvertTo-Json -Compress -Depth 1 + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress -Depth 1 + $jsonPipeline | Should -BeExactly '{"level1":{"level2":"System.Collections.Hashtable"}}' + $jsonInputObject | Should -BeExactly '{"level1":{"level2":"System.Collections.Hashtable"}}' + } + + It 'Should serialize hashtable with array value correctly via Pipeline and InputObject' { + $hash = @{ arr = @(1, 2, 3) } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"arr":[1,2,3]}' + $jsonInputObject | Should -BeExactly '{"arr":[1,2,3]}' + } + + It 'Should serialize hashtable with nested array of hashtables correctly via Pipeline and InputObject' { + $hash = @{ + items = @( + @{ id = 1 }, + @{ id = 2 } + ) + } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"items":[{"id":1},{"id":2}]}' + $jsonInputObject | Should -BeExactly '{"items":[{"id":1},{"id":2}]}' + } + } + + Context 'Dictionary key types' { + It 'Should serialize hashtable with string keys correctly via Pipeline and InputObject' { + $hash = @{ 'string-key' = 'value' } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"string-key":"value"}' + $jsonInputObject | Should -BeExactly '{"string-key":"value"}' + } + + It 'Should serialize hashtable with special character keys correctly via Pipeline and InputObject' { + $hash = [ordered]@{ + 'key with space' = 1 + 'key-with-dash' = 2 + 'key_with_underscore' = 3 + } + $expected = '{"key with space":1,"key-with-dash":2,"key_with_underscore":3}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize hashtable with unicode keys correctly via Pipeline and InputObject' { + $hash = @{ "`u{65E5}`u{672C}`u{8A9E}" = 'Japanese' } + $expected = "{`"`u{65E5}`u{672C}`u{8A9E}`":`"Japanese`"}" + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize hashtable with empty string key correctly via Pipeline and InputObject' { + $hash = @{ '' = 'empty key' } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"":"empty key"}' + $jsonInputObject | Should -BeExactly '{"":"empty key"}' + } + } + + Context 'Dictionary with complex values' { + It 'Should serialize hashtable with DateTime value correctly via Pipeline and InputObject' { + $hash = @{ date = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"date":"2024-06-15T10:30:00Z"}' + $jsonInputObject | Should -BeExactly '{"date":"2024-06-15T10:30:00Z"}' + } + + It 'Should serialize hashtable with Guid value correctly via Pipeline and InputObject' { + $hash = @{ guid = [Guid]'12345678-1234-1234-1234-123456789abc' } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"guid":"12345678-1234-1234-1234-123456789abc"}' + $jsonInputObject | Should -BeExactly '{"guid":"12345678-1234-1234-1234-123456789abc"}' + } + + It 'Should serialize hashtable with enum value correctly via Pipeline and InputObject' { + $hash = @{ day = [DayOfWeek]::Monday } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"day":1}' + $jsonInputObject | Should -BeExactly '{"day":1}' + } + + It 'Should serialize hashtable with enum as string correctly via Pipeline and InputObject' { + $hash = @{ day = [DayOfWeek]::Monday } + $jsonPipeline = $hash | ConvertTo-Json -Compress -EnumsAsStrings + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress -EnumsAsStrings + $jsonPipeline | Should -BeExactly '{"day":"Monday"}' + $jsonInputObject | Should -BeExactly '{"day":"Monday"}' + } + + It 'Should serialize hashtable with PSCustomObject value correctly via Pipeline and InputObject' { + $hash = @{ obj = [PSCustomObject]@{ prop = 'value' } } + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"obj":{"prop":"value"}}' + $jsonInputObject | Should -BeExactly '{"obj":{"prop":"value"}}' + } + } + + Context 'Dictionary ETS properties' { + It 'Should include ETS properties on hashtable via Pipeline and InputObject' { + $hash = @{ a = 1 } + $hash = Add-Member -InputObject $hash -MemberType NoteProperty -Name ETSProp -Value 'ets' -PassThru + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly '{"a":1,"ETSProp":"ets"}' + $jsonInputObject | Should -BeExactly '{"a":1,"ETSProp":"ets"}' + } + + It 'Should include ETS properties on OrderedDictionary via Pipeline and InputObject' { + $ordered = [ordered]@{ a = 1 } + $ordered = Add-Member -InputObject $ordered -MemberType NoteProperty -Name ETSProp -Value 'ets' -PassThru + $jsonPipeline = $ordered | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $ordered -Compress + $jsonPipeline | Should -BeExactly '{"a":1,"ETSProp":"ets"}' + $jsonInputObject | Should -BeExactly '{"a":1,"ETSProp":"ets"}' + } + } + + Context 'Generic Dictionary types' { + It 'Should serialize Generic Dictionary correctly via Pipeline and InputObject' { + $dict = [System.Collections.Generic.Dictionary[string,int]]::new() + $dict['one'] = 1 + $dict['two'] = 2 + $jsonPipeline = $dict | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dict -Compress + $jsonPipeline | Should -Match '"one":1' + $jsonPipeline | Should -Match '"two":2' + $jsonInputObject | Should -Match '"one":1' + $jsonInputObject | Should -Match '"two":2' + } + + It 'Should serialize SortedDictionary correctly via Pipeline and InputObject' { + $dict = [System.Collections.Generic.SortedDictionary[string,int]]::new() + $dict['b'] = 2 + $dict['a'] = 1 + $dict['c'] = 3 + $jsonPipeline = $dict | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $dict -Compress + $jsonPipeline | Should -BeExactly '{"a":1,"b":2,"c":3}' + $jsonInputObject | Should -BeExactly '{"a":1,"b":2,"c":3}' + } + } + + #endregion Comprehensive Array and Dictionary Tests (Phase 2) } From e58d395dbb84f783dc7fbc9dd74e444e8a455de3 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Fri, 6 Feb 2026 18:11:18 -0500 Subject: [PATCH 069/112] Create es-metadata (#26759) Co-authored-by: MerlinBot --- es-metadata.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 es-metadata.yml diff --git a/es-metadata.yml b/es-metadata.yml new file mode 100644 index 0000000000..24da115c11 --- /dev/null +++ b/es-metadata.yml @@ -0,0 +1,12 @@ +schemaVersion: 1.0.0 +providers: +- provider: InventoryAsCode + version: 1.0.0 + metadata: + isProduction: true + accountableOwners: + service: cef1de07-99d6-45df-b907-77d0066032ec + routing: + defaultAreaPath: + org: msazure + path: One\MGMT\Compute\Powershell\Powershell\Powershell Core\pwsh From 012529a967f6f4a0b2e874c2d98dc6efe79506cb Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Mon, 9 Feb 2026 13:13:17 -0500 Subject: [PATCH 070/112] Update Microsoft.PowerShell.PSResourceGet version to 1.2.0-rc3 (#26753) --- src/Modules/PSGalleryModules.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Modules/PSGalleryModules.csproj b/src/Modules/PSGalleryModules.csproj index cbc47ef200..7008fe6a08 100644 --- a/src/Modules/PSGalleryModules.csproj +++ b/src/Modules/PSGalleryModules.csproj @@ -13,7 +13,7 @@ - + From 664ac75f4fe8ef4008d40e11965b0df90180f022 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Tue, 10 Feb 2026 09:43:02 +0900 Subject: [PATCH 071/112] Add comprehensive depth and multilevel composition tests for `ConvertTo-Json` (#26744) --- .../ConvertTo-Json.Tests.ps1 | 452 ++++++++++++++++++ 1 file changed, 452 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index d368695333..4fe4b0d59b 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -1042,4 +1042,456 @@ Describe 'ConvertTo-Json' -tags "CI" { } #endregion Comprehensive Array and Dictionary Tests (Phase 2) + + #region Comprehensive Depth Truncation and Multilevel Composition Tests (Phase 4) + # Test coverage for ConvertTo-Json depth truncation and complex nested structures + # Covers: -Depth parameter behavior, multilevel type compositions + + Context 'Depth parameter basic behavior' { + It 'Should use default depth of 2 via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + L0 = [PSCustomObject]@{ + L1 = [PSCustomObject]@{ + L2 = [PSCustomObject]@{ + L3 = 'deep' + } + } + } + } + $expected = '{"L0":{"L1":{"L2":"@{L3=deep}"}}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should truncate at Depth 0 via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + L0 = [PSCustomObject]@{ L1 = 1 } + } + $expected = '{"L0":"@{L1=1}"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 0 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 0 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should truncate at Depth 1 via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + L0 = [PSCustomObject]@{ + L1 = [PSCustomObject]@{ + L2 = 'deep' + } + } + } + $expected = '{"L0":{"L1":"@{L2=deep}"}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 1 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 1 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize fully with sufficient Depth via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + L0 = [PSCustomObject]@{ + L1 = [PSCustomObject]@{ + L2 = [PSCustomObject]@{ + L3 = 'very deep' + } + } + } + } + $expected = '{"L0":{"L1":{"L2":{"L3":"very deep"}}}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 10 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 10 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should handle Depth 100 for deeply nested structures via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ L0 = [PSCustomObject]@{ L1 = [PSCustomObject]@{ L2 = [PSCustomObject]@{ L3 = [PSCustomObject]@{ L4 = 'deep' } } } } } + $expected = '{"L0":{"L1":{"L2":{"L3":{"L4":"deep"}}}}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 100 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 100 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should throw on Depth 101 exceeding maximum via Pipeline and InputObject' { + { [PSCustomObject]@{ L0 = 1 } | ConvertTo-Json -Depth 101 } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Microsoft.PowerShell.Commands.ConvertToJsonCommand' + { ConvertTo-Json -InputObject ([PSCustomObject]@{ L0 = 1 }) -Depth 101 } | Should -Throw -ErrorId 'ParameterArgumentValidationError,Microsoft.PowerShell.Commands.ConvertToJsonCommand' + } + } + + Context 'Depth truncation with arrays' { + It 'Should truncate nested array at Depth limit via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Arr = ,(,(1, 2, 3)) + } + $expected = '{"Arr":["System.Object[]"]}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 1 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 1 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize nested array fully with sufficient Depth via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Arr = ,(,(1, 2, 3)) + } + $expected = '{"Arr":[[[1,2,3]]]}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 10 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 10 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should truncate array of objects at Depth limit via Pipeline and InputObject' { + $arr = @( + [PSCustomObject]@{ Inner = [PSCustomObject]@{ Value = 1 } } + ) + $expected = '[{"Inner":"@{Value=1}"}]' + $jsonPipeline = ,$arr | ConvertTo-Json -Compress -Depth 1 + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress -Depth 1 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Depth truncation with hashtables' { + It 'Should truncate nested hashtable at Depth limit via Pipeline and InputObject' { + $hash = @{ + L0 = @{ + L1 = @{ + L2 = 'deep' + } + } + } + $expected = '{"L0":{"L1":"System.Collections.Hashtable"}}' + $jsonPipeline = $hash | ConvertTo-Json -Compress -Depth 1 + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress -Depth 1 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize nested hashtable fully with sufficient Depth via Pipeline and InputObject' { + $hash = @{ + L0 = @{ + L1 = @{ + L2 = 'deep' + } + } + } + $expected = '{"L0":{"L1":{"L2":"deep"}}}' + $jsonPipeline = $hash | ConvertTo-Json -Compress -Depth 10 + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress -Depth 10 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Depth truncation string representation' { + It 'Should convert PSCustomObject to @{...} string when truncated via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Child = [PSCustomObject]@{ A = 1; B = 2 } + } + $expected = '{"Child":"@{A=1; B=2}"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 0 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 0 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should convert Hashtable to type name when truncated via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Child = @{ Key = 'Value' } + } + $expected = '{"Child":"System.Collections.Hashtable"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 0 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 0 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should convert Array to space-separated string when truncated via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Child = @(1, 2, 3) + } + $expected = '{"Child":"1 2 3"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 0 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 0 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Multilevel composition: Array containing Dictionary' { + It 'Should serialize array of hashtables correctly via Pipeline and InputObject' { + $arr = @(@{ a = 1 }, @{ b = 2 }, @{ c = 3 }) + $expected = '[{"a":1},{"b":2},{"c":3}]' + $jsonPipeline = $arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize array of ordered dictionaries correctly via Pipeline and InputObject' { + $arr = @( + [ordered]@{ x = 1; y = 2 }, + [ordered]@{ x = 3; y = 4 } + ) + $expected = '[{"x":1,"y":2},{"x":3,"y":4}]' + $jsonPipeline = ,$arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize nested array of hashtables correctly via Pipeline and InputObject' { + $arr = @( + @{ + Items = @( + @{ Value = 1 }, + @{ Value = 2 } + ) + } + ) + $expected = '[{"Items":[{"Value":1},{"Value":2}]}]' + $jsonPipeline = ,$arr | ConvertTo-Json -Compress -Depth 3 + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress -Depth 3 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Multilevel composition: Dictionary containing Array' { + It 'Should serialize dictionary with array values correctly via Pipeline and InputObject' { + $hash = [ordered]@{ + numbers = @(1, 2, 3) + strings = @('a', 'b', 'c') + } + $expected = '{"numbers":[1,2,3],"strings":["a","b","c"]}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize dictionary with nested array values correctly via Pipeline and InputObject' { + $hash = @{ + matrix = @(@(1, 2), @(3, 4)) + } + $expected = '{"matrix":[[1,2],[3,4]]}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize dictionary with empty array value correctly via Pipeline and InputObject' { + $hash = @{ empty = @() } + $expected = '{"empty":[]}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize dictionary with array of dictionaries correctly via Pipeline and InputObject' { + $hash = @{ + Items = @( + @{ X = 1 }, + @{ X = 2 } + ) + } + $expected = '{"Items":[{"X":1},{"X":2}]}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Multilevel composition: PSCustomObject with mixed types' { + It 'Should serialize PSCustomObject with array and hashtable properties via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + List = @(1, 2, 3) + Config = @{ Key = 'Value' } + Name = 'Test' + } + $expected = '{"List":[1,2,3],"Config":{"Key":"Value"},"Name":"Test"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with nested PSCustomObject and array via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Child = [PSCustomObject]@{ + Items = @(1, 2, 3) + } + } + $expected = '{"Child":{"Items":[1,2,3]}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize array of PSCustomObject with mixed properties via Pipeline and InputObject' { + $arr = @( + [PSCustomObject]@{ Type = 'A'; Data = @(1, 2) }, + [PSCustomObject]@{ Type = 'B'; Data = @{ Key = 'Val' } } + ) + $expected = '[{"Type":"A","Data":[1,2]},{"Type":"B","Data":{"Key":"Val"}}]' + $jsonPipeline = $arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Multilevel composition: PowerShell class in complex structures' { + BeforeAll { + class ItemClass { + [int]$Id + [string]$Name + } + + class ContainerClass { + [string]$Type + [ItemClass]$Item + } + } + + It 'Should serialize array of PowerShell class correctly via Pipeline and InputObject' { + $arr = @( + [ItemClass]@{ Id = 1; Name = 'First' }, + [ItemClass]@{ Id = 2; Name = 'Second' } + ) + $expected = '[{"Id":1,"Name":"First"},{"Id":2,"Name":"Second"}]' + $jsonPipeline = $arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize hashtable containing PowerShell class correctly via Pipeline and InputObject' { + $item = [ItemClass]@{ Id = 1; Name = 'Test' } + $hash = @{ Item = $item } + $expected = '{"Item":{"Id":1,"Name":"Test"}}' + $jsonPipeline = $hash | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize nested PowerShell classes correctly via Pipeline and InputObject' { + $item = [ItemClass]@{ Id = 1; Name = 'Inner' } + $container = [ContainerClass]@{ Type = 'Outer'; Item = $item } + $expected = '{"Type":"Outer","Item":{"Id":1,"Name":"Inner"}}' + $jsonPipeline = $container | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $container -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject containing PowerShell class correctly via Pipeline and InputObject' { + $item = [ItemClass]@{ Id = 1; Name = 'Test' } + $obj = [PSCustomObject]@{ + Label = 'Container' + Content = $item + } + $expected = '{"Label":"Container","Content":{"Id":1,"Name":"Test"}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should truncate nested PowerShell class at Depth limit via Pipeline and InputObject' { + $item = [ItemClass]@{ Id = 1; Name = 'Test' } + $container = [ContainerClass]@{ Type = 'Outer'; Item = $item } + $itemString = $item.ToString() + $expected = "{`"Type`":`"Outer`",`"Item`":`"$itemString`"}" + $jsonPipeline = $container | ConvertTo-Json -Compress -Depth 0 + $jsonInputObject = ConvertTo-Json -InputObject $container -Compress -Depth 0 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Complex multilevel compositions' { + It 'Should serialize 3-level mixed composition correctly via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Users = @( + [PSCustomObject]@{ + Name = 'Alice' + Roles = @('Admin', 'User') + }, + [PSCustomObject]@{ + Name = 'Bob' + Roles = @('User') + } + ) + } + $expected = '{"Users":[{"Name":"Alice","Roles":["Admin","User"]},{"Name":"Bob","Roles":["User"]}]}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 3 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 3 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize dictionary with nested mixed types correctly via Pipeline and InputObject' { + $hash = [ordered]@{ + Meta = [PSCustomObject]@{ Version = '1.0' } + Data = @( + ([ordered]@{ Key = 'A'; Values = @(1, 2) }), + ([ordered]@{ Key = 'B'; Values = @(3, 4) }) + ) + } + $expected = '{"Meta":{"Version":"1.0"},"Data":[{"Key":"A","Values":[1,2]},{"Key":"B","Values":[3,4]}]}' + $jsonPipeline = $hash | ConvertTo-Json -Compress -Depth 3 + $jsonInputObject = ConvertTo-Json -InputObject $hash -Compress -Depth 3 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should handle deeply nested mixed types with sufficient Depth via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + L0 = @{ + L1 = [PSCustomObject]@{ + L2 = @( + [PSCustomObject]@{ L3 = 'deep' } + ) + } + } + } + $expected = '{"L0":{"L1":{"L2":[{"L3":"deep"}]}}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 10 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 10 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should truncate deeply nested mixed types at Depth limit via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + L0 = @{ + L1 = [PSCustomObject]@{ + L2 = @( + [PSCustomObject]@{ L3 = 'deep' } + ) + } + } + } + $expected = '{"L0":{"L1":{"L2":""}}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 2 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 2 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + #endregion Comprehensive Depth Truncation and Multilevel Composition Tests (Phase 4) } From 42371b881c8189e871761e11d584e35acb139f64 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:10:50 -0800 Subject: [PATCH 072/112] Add `SubjectAlternativeName` property to the `Signature` object returned from `Get-AuthenticodeSignature` (#26252) --- .../security/MshSignature.cs | 37 +++++++ .../security/SecuritySupport.cs | 1 + .../engine/Security/FileSignature.Tests.ps1 | 101 ++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/src/System.Management.Automation/security/MshSignature.cs b/src/System.Management.Automation/security/MshSignature.cs index fd8dd4f67e..7cbaf98d3a 100644 --- a/src/System.Management.Automation/security/MshSignature.cs +++ b/src/System.Management.Automation/security/MshSignature.cs @@ -185,6 +185,11 @@ namespace System.Management.Automation /// public bool IsOSBinary { get; internal set; } + /// + /// Gets the Subject Alternative Name from the signer certificate. + /// + public string[] SubjectAlternativeName { get; private set; } + /// /// Constructor for class Signature /// @@ -277,6 +282,9 @@ namespace System.Management.Automation _statusMessage = GetSignatureStatusMessage(isc, error, filePath); + + // Extract Subject Alternative Name from the signer certificate + SubjectAlternativeName = GetSubjectAlternativeName(signer); } private static SignatureStatus GetSignatureStatusFromWin32Error(DWORD error) @@ -389,5 +397,34 @@ namespace System.Management.Automation return message; } + + /// + /// Extracts the Subject Alternative Name from the certificate. + /// + /// The certificate to extract SAN from. + /// Array of SAN entries or null if not found. + private static string[] GetSubjectAlternativeName(X509Certificate2 certificate) + { + if (certificate == null) + { + return null; + } + + foreach (X509Extension extension in certificate.Extensions) + { + if (extension.Oid != null && extension.Oid.Value == CertificateFilterInfo.SubjectAlternativeNameOid) + { + string formatted = extension.Format(multiLine: true); + if (string.IsNullOrEmpty(formatted)) + { + return null; + } + + return formatted.Split(new[] { "\r\n", "\n", "\r" }, StringSplitOptions.RemoveEmptyEntries); + } + } + + return null; + } } } diff --git a/src/System.Management.Automation/security/SecuritySupport.cs b/src/System.Management.Automation/security/SecuritySupport.cs index e6a6f10416..12e03d8517 100644 --- a/src/System.Management.Automation/security/SecuritySupport.cs +++ b/src/System.Management.Automation/security/SecuritySupport.cs @@ -815,6 +815,7 @@ namespace System.Management.Automation.Internal // The OID arc 1.3.6.1.4.1.311.80 is assigned to PowerShell. If we need // new OIDs, we can assign them under this branch. internal const string DocumentEncryptionOid = "1.3.6.1.4.1.311.80.1"; + internal const string SubjectAlternativeNameOid = "2.5.29.17"; } } diff --git a/test/powershell/engine/Security/FileSignature.Tests.ps1 b/test/powershell/engine/Security/FileSignature.Tests.ps1 index 9dd26f98ae..f2815fad4f 100644 --- a/test/powershell/engine/Security/FileSignature.Tests.ps1 +++ b/test/powershell/engine/Security/FileSignature.Tests.ps1 @@ -18,6 +18,9 @@ Describe "Windows platform file signatures" -Tags 'Feature' { $signature | Should -Not -BeNullOrEmpty $signature.Status | Should -BeExactly 'Valid' $signature.SignatureType | Should -BeExactly 'Catalog' + + # Verify that SubjectAlternativeName property exists + $signature.PSObject.Properties.Name | Should -Contain 'SubjectAlternativeName' } } @@ -186,4 +189,102 @@ Describe "Windows file content signatures" -Tags @('Feature', 'RequireAdminOnWin $actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint $actual.Status | Should -Be 'Valid' } + + It "Verifies SubjectAlternativeName is populated for certificate with SAN" { + $session = New-PSSession -UseWindowsPowerShell + try { + $sanThumbprint = Invoke-Command -Session $session -ScriptBlock { + $testPrefix = 'SelfSignedTestSAN' + + $enhancedKeyUsage = [Security.Cryptography.OidCollection]::new() + $null = $enhancedKeyUsage.Add('1.3.6.1.5.5.7.3.3') + + $caParams = @{ + Extension = @( + [Security.Cryptography.X509Certificates.X509BasicConstraintsExtension]::new($true, $false, 0, $true), + [Security.Cryptography.X509Certificates.X509KeyUsageExtension]::new('KeyCertSign', $false), + [Security.Cryptography.X509Certificates.X509EnhancedKeyUsageExtension]::new($enhancedKeyUsage, $false) + ) + CertStoreLocation = 'Cert:\CurrentUser\My' + NotAfter = (Get-Date).AddDays(1) + Type = 'Custom' + } + $sanCA = PKI\New-SelfSignedCertificate @caParams -Subject "CN=$testPrefix-CA" + + $rootStore = Get-Item -Path Cert:\LocalMachine\Root + $rootStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + try { + $rootStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($sanCA.RawData)) + } finally { + $rootStore.Close() + } + + $certParams = @{ + CertStoreLocation = 'Cert:\CurrentUser\My' + KeyUsage = 'DigitalSignature' + TextExtension = @( + "2.5.29.37={text}1.3.6.1.5.5.7.3.3", + "2.5.29.19={text}", + "2.5.29.17={text}DNS=test.example.com&DNS=*.example.com" + ) + Type = 'Custom' + } + $sanCert = PKI\New-SelfSignedCertificate @certParams -Subject "CN=$testPrefix-Signed" -Signer $sanCA + + $publisherStore = Get-Item -Path Cert:\LocalMachine\TrustedPublisher + $publisherStore.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + try { + $publisherStore.Add([System.Security.Cryptography.X509Certificates.X509Certificate2]::new($sanCert.RawData)) + } finally { + $publisherStore.Close() + } + + $sanCA | Remove-Item + $sanCA.Thumbprint, $sanCert.Thumbprint + } + } finally { + $session | Remove-PSSession + } + + $sanCARootThumbprint = $sanThumbprint[0] + $sanCertThumbprint = $sanThumbprint[1] + $sanCertificate = Get-Item -Path Cert:\CurrentUser\My\$sanCertThumbprint + + try { + Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Test SAN"' -Encoding UTF8NoBOM + + $scriptPath = Join-Path $TestDrive test.ps1 + $status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $sanCertificate + $status.Status | Should -Be 'Valid' + + $actual = Get-AuthenticodeSignature -FilePath $scriptPath + $actual.SubjectAlternativeName | Should -Not -BeNullOrEmpty + ,$actual.SubjectAlternativeName | Should -BeOfType [string[]] + $actual.SubjectAlternativeName.Count | Should -Be 2 + $actual.SubjectAlternativeName[0] | Should -BeExactly 'DNS Name=test.example.com' + $actual.SubjectAlternativeName[1] | Should -BeExactly 'DNS Name=*.example.com' + } finally { + Remove-Item -Path "Cert:\LocalMachine\Root\$sanCARootThumbprint" -Force -ErrorAction Ignore + Remove-Item -Path "Cert:\LocalMachine\TrustedPublisher\$sanCertThumbprint" -Force -ErrorAction Ignore + Remove-Item -Path "Cert:\CurrentUser\My\$sanCertThumbprint" -Force -ErrorAction Ignore + } + } + + It "Verifies SubjectAlternativeName is null when certificate has no SAN" { + Set-Content -Path testdrive:\test.ps1 -Value 'Write-Output "Test No SAN"' -Encoding UTF8NoBOM + + $scriptPath = Join-Path $TestDrive test.ps1 + $status = Set-AuthenticodeSignature -FilePath $scriptPath -Certificate $certificate + $status.Status | Should -Be 'Valid' + + $actual = Get-AuthenticodeSignature -FilePath $scriptPath + $actual.SignerCertificate.Thumbprint | Should -Be $certificate.Thumbprint + $actual.Status | Should -Be 'Valid' + + # Verify that SubjectAlternativeName property exists + $actual.PSObject.Properties.Name | Should -Contain 'SubjectAlternativeName' + + # Verify the content is null when certificate has no SAN extension + $actual.SubjectAlternativeName | Should -BeNullOrEmpty + } } From 9f9f8b93a39f1051f854561532d9651a2e07d269 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:27:36 +0000 Subject: [PATCH 073/112] Bump github/codeql-action from 4.32.1 to 4.32.2 (#26755) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index b56d2d6635..c5a0b9ff98 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5 + uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5 + uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index d1d974f8ef..a5d0509155 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@6bc82e05fd0ea64601dd4b465378bbcf57de0314 # v3.29.5 + uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3.29.5 with: sarif_file: results.sarif From 439c42f232fd3e6793425be65aabc942f044cdce Mon Sep 17 00:00:00 2001 From: PowerShell Team Bot <69177312+pwshBot@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:02:44 -0800 Subject: [PATCH 074/112] Update outdated package references (#26771) --- .../Microsoft.PowerShell.Commands.Utility.csproj | 2 +- .../Microsoft.PowerShell.SDK.csproj | 6 +++--- .../ResultsComparer/ResultsComparer.csproj | 2 +- tools/cgmanifest.json | 16 ++++++++-------- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 5ee999c2d9..eed0722b4d 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj index 161170355e..0f827aab55 100644 --- a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj +++ b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj @@ -24,9 +24,9 @@ - - - + + + diff --git a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj index c483192484..5cbc66081a 100644 --- a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj +++ b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj @@ -10,6 +10,6 @@ - + diff --git a/tools/cgmanifest.json b/tools/cgmanifest.json index 1001370ade..2334f07085 100644 --- a/tools/cgmanifest.json +++ b/tools/cgmanifest.json @@ -25,7 +25,7 @@ "Type": "nuget", "Nuget": { "Name": "Humanizer.Core", - "Version": "3.0.1" + "Version": "2.14.1" } }, "DevelopmentDependency": false @@ -35,7 +35,7 @@ "Type": "nuget", "Nuget": { "Name": "Json.More.Net", - "Version": "2.2.0" + "Version": "2.1.1" } }, "DevelopmentDependency": false @@ -45,7 +45,7 @@ "Type": "nuget", "Nuget": { "Name": "JsonPointer.Net", - "Version": "6.0.1" + "Version": "5.3.1" } }, "DevelopmentDependency": false @@ -65,7 +65,7 @@ "Type": "nuget", "Nuget": { "Name": "Markdig.Signed", - "Version": "0.44.0" + "Version": "0.45.0" } }, "DevelopmentDependency": false @@ -665,7 +665,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Http", - "Version": "8.1.2" + "Version": "10.0.652802" } }, "DevelopmentDependency": false @@ -675,7 +675,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.NetFramingBase", - "Version": "8.1.2" + "Version": "10.0.652802" } }, "DevelopmentDependency": false @@ -685,7 +685,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.NetTcp", - "Version": "8.1.2" + "Version": "10.0.652802" } }, "DevelopmentDependency": false @@ -695,7 +695,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Primitives", - "Version": "8.1.2" + "Version": "10.0.652802" } }, "DevelopmentDependency": false From 2e26e4b69076a22173ccf8d0ebd6f2c1d590578a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Thu, 12 Feb 2026 08:50:16 -0800 Subject: [PATCH 075/112] Update the `Update-Help` tests to use `-Force` to remove read-only files (#26780) --- test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 b/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 index 593a2ee89d..e871a8d160 100644 --- a/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 +++ b/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 @@ -215,7 +215,7 @@ function RunUpdateHelpTests It ('Validate Update-Help for module ''{0}'' in {1}' -F $moduleName, [PSCustomObject] $updateScope) -Skip:(!(Test-CanWriteToPsHome) -and $userscope -eq $false) { # Delete the whole help directory - Remove-Item ($moduleHelpPath) -Recurse + Remove-Item ($moduleHelpPath) -Recurse -Force -ErrorAction SilentlyContinue [hashtable] $UICultureParam = $(if ((Get-UICulture).Name -ne $myUICulture) { @{ UICulture = $myUICulture } } else { @{} }) [hashtable] $sourcePathParam = $(if ($useSourcePath) { @{ SourcePath = Join-Path $PSScriptRoot assets } } else { @{} }) From 4db414ca3345dd78232096233226584a5fda9895 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 12 Feb 2026 22:45:17 +0000 Subject: [PATCH 076/112] Add GitHub Actions annotations for Pester test failures (#26789) 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 --- .../process-pester-results.ps1 | 56 +++++++++++++++++ build.psm1 | 63 +++++++++++++++++++ 2 files changed, 119 insertions(+) diff --git a/.github/actions/test/process-pester-results/process-pester-results.ps1 b/.github/actions/test/process-pester-results/process-pester-results.ps1 index 523de3beba..5804bec9a9 100644 --- a/.github/actions/test/process-pester-results/process-pester-results.ps1 +++ b/.github/actions/test/process-pester-results/process-pester-results.ps1 @@ -24,6 +24,7 @@ $testIgnoredCount = 0 $testSkippedCount = 0 $testInvalidCount = 0 +# Process test results and generate annotations for failures Get-ChildItem -Path "${TestResultsFolder}/*.xml" -Recurse | ForEach-Object { $results = [xml] (get-content $_.FullName) @@ -35,6 +36,61 @@ Get-ChildItem -Path "${TestResultsFolder}/*.xml" -Recurse | ForEach-Object { $testIgnoredCount += [int]$results.'test-results'.ignored $testSkippedCount += [int]$results.'test-results'.skipped $testInvalidCount += [int]$results.'test-results'.invalid + + # Generate GitHub Actions annotations for test failures + # Select failed test cases + if ("System.Xml.XmlDocumentXPathExtensions" -as [Type]) { + $failures = [System.Xml.XmlDocumentXPathExtensions]::SelectNodes($results.'test-results', './/test-case[@result = "Failure"]') + } + else { + $failures = $results.SelectNodes('.//test-case[@result = "Failure"]') + } + + foreach ($testfail in $failures) { + $description = $testfail.description + $testName = $testfail.name + $message = $testfail.failure.message + $stack_trace = $testfail.failure.'stack-trace' + + # Parse stack trace to get file and line info + $fileInfo = Get-PesterFailureFileInfo -StackTraceString $stack_trace + + if ($fileInfo.File) { + # Convert absolute path to relative path for GitHub Actions + $filePath = $fileInfo.File + + # GitHub Actions expects paths relative to the workspace root + if ($env:GITHUB_WORKSPACE) { + $workspacePath = $env:GITHUB_WORKSPACE + if ($filePath.StartsWith($workspacePath)) { + $filePath = $filePath.Substring($workspacePath.Length).TrimStart('/', '\') + # Normalize to forward slashes for consistency + $filePath = $filePath -replace '\\', '/' + } + } + + # Create annotation title + $annotationTitle = "Test Failure: $description / $testName" + + # Build the annotation message + $annotationMessage = $message -replace "`n", "%0A" -replace "`r" + + # Build and output the workflow command + $workflowCommand = "::error file=$filePath" + if ($fileInfo.Line) { + $workflowCommand += ",line=$($fileInfo.Line)" + } + $workflowCommand += ",title=$annotationTitle::$annotationMessage" + + Write-Host $workflowCommand + + # Output a link to the test run + if ($env:GITHUB_SERVER_URL -and $env:GITHUB_REPOSITORY -and $env:GITHUB_RUN_ID) { + $logUrl = "$($env:GITHUB_SERVER_URL)/$($env:GITHUB_REPOSITORY)/actions/runs/$($env:GITHUB_RUN_ID)" + Write-Host "Test logs: $logUrl" + } + } + } } @" diff --git a/build.psm1 b/build.psm1 index d09b7af925..bf175c5f7c 100644 --- a/build.psm1 +++ b/build.psm1 @@ -1864,6 +1864,69 @@ $stack_trace } +function Get-PesterFailureFileInfo +{ + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string]$StackTraceString + ) + + # Parse stack trace to extract file path and line number + # Common patterns: + # "at line: 123 in C:\path\to\file.ps1" (Pester 4) + # "at C:\path\to\file.ps1:123" + # "at , C:\path\to\file.ps1: line 123" + # "at 1 | Should -Be 2, /path/to/file.ps1:123" (Pester 5) + # "at 1 | Should -Be 2, C:\path\to\file.ps1:123" (Pester 5 Windows) + + $result = @{ + File = $null + Line = $null + } + + if ([string]::IsNullOrWhiteSpace($StackTraceString)) { + return $result + } + + # Try pattern: "at line: 123 in " (Pester 4) + if ($StackTraceString -match 'at line:\s*(\d+)\s+in\s+(.+?)(?:\r|\n|$)') { + $result.Line = $matches[1] + $result.File = $matches[2].Trim() + return $result + } + + # Try pattern: ", :123" (Pester 5 format) + # This handles both Unix paths (/path/file.ps1:123) and Windows paths (C:\path\file.ps1:123) + if ($StackTraceString -match ',\s*((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1):(\d+)') { + $result.File = $matches[1].Trim() + $result.Line = $matches[2] + return $result + } + + # Try pattern: "at :123" (without comma) + # Handle both absolute Unix and Windows paths + if ($StackTraceString -match 'at\s+((?:[A-Za-z]:)?[\/\\][^,]+?\.ps[m]?1):(\d+)(?:\r|\n|$)') { + $result.File = $matches[1].Trim() + $result.Line = $matches[2] + return $result + } + + # Try pattern: ": line 123" + if ($StackTraceString -match '((?:[A-Za-z]:)?[\/\\][^,]+?\.ps[m]?1):\s*line\s+(\d+)(?:\r|\n|$)') { + $result.File = $matches[1].Trim() + $result.Line = $matches[2] + return $result + } + + # Try to extract just the file path if no line number found + if ($StackTraceString -match '(?:at\s+|in\s+)?((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1)') { + $result.File = $matches[1].Trim() + } + + return $result +} + function Test-XUnitTestResults { param( From 6b8d724b030d1ced7bf772c502279d694c72bf47 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 13 Feb 2026 03:57:33 +0000 Subject: [PATCH 077/112] Mark flaky Update-Help web tests as pending to unblock CI (#26796) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: TravisEz13 <10873629+TravisEz13@users.noreply.github.com> --- ...ester-set-itresult-pattern.instructions.md | 198 ++++++++++++++++++ .../engine/Help/UpdatableHelpSystem.Tests.ps1 | 35 +++- 2 files changed, 226 insertions(+), 7 deletions(-) create mode 100644 .github/instructions/pester-set-itresult-pattern.instructions.md diff --git a/.github/instructions/pester-set-itresult-pattern.instructions.md b/.github/instructions/pester-set-itresult-pattern.instructions.md new file mode 100644 index 0000000000..33a73ca081 --- /dev/null +++ b/.github/instructions/pester-set-itresult-pattern.instructions.md @@ -0,0 +1,198 @@ +--- +applyTo: + - "**/*.Tests.ps1" +--- + +# Pester Set-ItResult Pattern for Pending and Skipped Tests + +## Purpose + +This instruction explains when and how to use `Set-ItResult` in Pester tests to mark tests as Pending or Skipped dynamically within test execution. + +## When to Use Set-ItResult + +Use `Set-ItResult` when you need to conditionally mark a test as Pending or Skipped based on runtime conditions that can't be determined at test definition time. + +### Pending vs Skipped + +**Pending**: Use for tests that should be enabled but temporarily can't run due to: +- Intermittent external service failures (network, APIs) +- Known bugs being fixed +- Missing features being implemented +- Environmental issues that are being resolved + +**Skipped**: Use for tests that aren't applicable to the current environment: +- Platform-specific tests running on wrong platform +- Tests requiring specific hardware/configuration not present +- Tests requiring elevated permissions when not available +- Feature-specific tests when feature is disabled + +## Pattern + +### Basic Usage + +```powershell +It "Test description" { + if ($shouldBePending) { + Set-ItResult -Pending -Because "Explanation of why test is pending" + return + } + + if ($shouldBeSkipped) { + Set-ItResult -Skipped -Because "Explanation of why test is skipped" + return + } + + # Test code here +} +``` + +### Important: Always Return After Set-ItResult + +After calling `Set-ItResult`, you **must** return from the test to prevent further execution: + +```powershell +It "Test that checks environment" { + if ($env:SKIP_TESTS -eq 'true') { + Set-ItResult -Skipped -Because "SKIP_TESTS environment variable is set" + return # This is required! + } + + # Test assertions + $result | Should -Be $expected +} +``` + +**Why?** Without `return`, the test continues executing and may fail with errors unrelated to the pending/skipped condition. + +## Examples from the Codebase + +### Example 1: Pending for Intermittent Network Issues + +```powershell +It "Validate Update-Help for module" { + if ($markAsPending) { + Set-ItResult -Pending -Because "Update-Help from the web has intermittent connectivity issues. See issues #2807 and #6541." + return + } + + Update-Help -Module $moduleName -Force + # validation code... +} +``` + +### Example 2: Skipped for Missing Environment + +```powershell +It "Test requires CI environment" { + if (-not $env:CI) { + Set-ItResult -Skipped -Because "Test requires CI environment to safely install Pester" + return + } + + Install-CIPester -ErrorAction Stop +} +``` + +### Example 3: Pending for Platform-Specific Issue + +```powershell +It "Clear-Host works correctly" { + if ($IsARM64) { + Set-ItResult -Pending -Because "ARM64 runs in non-interactively mode and Clear-Host does not work." + return + } + + & { Clear-Host; 'hi' } | Should -BeExactly 'hi' +} +``` + +### Example 4: Skipped for Missing Feature + +```powershell +It "Test ACR authentication" { + if ($env:ACRTESTS -ne 'true') { + Set-ItResult -Skipped -Because "The tests require the ACRTESTS environment variable to be set to 'true' for ACR authentication." + return + } + + $psgetModuleInfo = Find-PSResource -Name $ACRTestModule -Repository $ACRRepositoryName + # test assertions... +} +``` + +## Alternative: Static -Skip and -Pending Parameters + +For conditions that can be determined at test definition time, use the static parameters instead: + +```powershell +# Static skip - condition known at definition time +It "Windows-only test" -Skip:(-not $IsWindows) { + # test code +} + +# Static pending - always pending +It "Test for feature being implemented" -Pending { + # test code that will fail until feature is done +} +``` + +**Use Set-ItResult when**: +- Condition depends on runtime state +- Condition is determined inside a helper function +- Need to check multiple conditions sequentially + +**Use static parameters when**: +- Condition is known at test definition +- Condition doesn't change during test run +- Want Pester to show the condition in test discovery + +## Best Practices + +1. **Always include -Because parameter** with a clear explanation +2. **Always return after Set-ItResult** to prevent further execution +3. **Reference issues or documentation** when relevant (e.g., "See issue #1234") +4. **Be specific in the reason** - explain what's wrong and what's needed +5. **Use Pending sparingly** - it indicates a problem that should be fixed +6. **Prefer Skipped over Pending** when test truly isn't applicable + +## Common Mistakes + +### ❌ Mistake 1: Forgetting to Return + +```powershell +It "Test" { + if ($condition) { + Set-ItResult -Pending -Because "Reason" + # Missing return - test code will still execute! + } + $value | Should -Be $expected # This runs and fails +} +``` + +### ❌ Mistake 2: Vague Reason + +```powershell +Set-ItResult -Pending -Because "Doesn't work" # Too vague +``` + +### ✅ Correct: + +```powershell +It "Test" { + if ($condition) { + Set-ItResult -Pending -Because "Update-Help has intermittent network timeouts. See issue #2807." + return + } + $value | Should -Be $expected +} +``` + +## See Also + +- [Pester Documentation: Set-ItResult](https://pester.dev/docs/commands/Set-ItResult) +- [Pester Documentation: It](https://pester.dev/docs/commands/It) +- Examples in the codebase: + - `test/powershell/Host/ConsoleHost.Tests.ps1` + - `test/infrastructure/ciModule.Tests.ps1` + - `tools/packaging/releaseTests/sbom.tests.ps1` diff --git a/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 b/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 index e871a8d160..6bf82d21af 100644 --- a/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 +++ b/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 @@ -191,7 +191,8 @@ function RunUpdateHelpTests param ( [string]$tag = "CI", [switch]$useSourcePath, - [switch]$userscope + [switch]$userscope, + [switch]$markAsPending ) foreach ($moduleName in $modulesInBox) @@ -214,8 +215,15 @@ function RunUpdateHelpTests It ('Validate Update-Help for module ''{0}'' in {1}' -F $moduleName, [PSCustomObject] $updateScope) -Skip:(!(Test-CanWriteToPsHome) -and $userscope -eq $false) { + if ($markAsPending) { + Set-ItResult -Pending -Because "Update-Help from the web has intermittent connectivity issues. See issues #2807 and #6541." + return + } + # Delete the whole help directory - Remove-Item ($moduleHelpPath) -Recurse -Force -ErrorAction SilentlyContinue + if ($moduleHelpPath) { + Remove-Item ($moduleHelpPath) -Recurse -Force -ErrorAction SilentlyContinue + } [hashtable] $UICultureParam = $(if ((Get-UICulture).Name -ne $myUICulture) { @{ UICulture = $myUICulture } } else { @{} }) [hashtable] $sourcePathParam = $(if ($useSourcePath) { @{ SourcePath = Join-Path $PSScriptRoot assets } } else { @{} }) @@ -246,8 +254,15 @@ function RunSaveHelpTests { try { - $saveHelpFolder = Join-Path $TestDrive (Get-Random).ToString() - New-Item $saveHelpFolder -Force -ItemType Directory > $null + $saveHelpFolder = if ($TestDrive) { + Join-Path $TestDrive (Get-Random).ToString() + } else { + $null + } + + if ($saveHelpFolder) { + New-Item $saveHelpFolder -Force -ItemType Directory > $null + } ## Save help has intermittent connectivity issues for downloading PackageManagement help content. ## Hence the test has been marked as Pending. @@ -283,7 +298,9 @@ function RunSaveHelpTests } finally { - Remove-Item $saveHelpFolder -Force -ErrorAction SilentlyContinue -Recurse + if ($saveHelpFolder) { + Remove-Item $saveHelpFolder -Force -ErrorAction SilentlyContinue -Recurse + } } } } @@ -316,7 +333,9 @@ Describe "Validate Update-Help from the Web for one PowerShell module." -Tags @( $ProgressPreference = $SavedProgressPreference } - RunUpdateHelpTests -Tag "CI" + ## Update-Help from the web has intermittent connectivity issues that cause CI failures. + ## Tests are marked as Pending to unblock work. See issues #2807 and #6541. + RunUpdateHelpTests -Tag "CI" -MarkAsPending } Describe "Validate Update-Help from the Web for one PowerShell module for user scope." -Tags @('CI', 'RequireAdminOnWindows', 'RequireSudoOnUnix') { @@ -328,7 +347,9 @@ Describe "Validate Update-Help from the Web for one PowerShell module for user s $ProgressPreference = $SavedProgressPreference } - RunUpdateHelpTests -Tag "CI" -UserScope + ## Update-Help from the web has intermittent connectivity issues that cause CI failures. + ## Tests are marked as Pending to unblock work. See issues #2807 and #6541. + RunUpdateHelpTests -Tag "CI" -UserScope -MarkAsPending } Describe "Validate Update-Help from the Web for all PowerShell modules." -Tags @('Feature', 'RequireAdminOnWindows', 'RequireSudoOnUnix') { From 08b13f24e6d359877b2cdbe650f7c8fc5ca20135 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Fri, 13 Feb 2026 14:43:16 -0500 Subject: [PATCH 078/112] Add Pester CI Analysis Skill (#26806) --- ...status-and-working-meaning.instructions.md | 299 ++++++++++ .../skills/analyze-pester-failures/SKILL.md | 524 ++++++++++++++++++ .../references/stack-trace-parsing.md | 163 ++++++ .../scripts/analyze-pr-test-failures.ps1 | 456 +++++++++++++++ .gitignore | 3 + 5 files changed, 1445 insertions(+) create mode 100644 .github/instructions/pester-test-status-and-working-meaning.instructions.md create mode 100644 .github/skills/analyze-pester-failures/SKILL.md create mode 100644 .github/skills/analyze-pester-failures/references/stack-trace-parsing.md create mode 100644 .github/skills/analyze-pester-failures/scripts/analyze-pr-test-failures.ps1 diff --git a/.github/instructions/pester-test-status-and-working-meaning.instructions.md b/.github/instructions/pester-test-status-and-working-meaning.instructions.md new file mode 100644 index 0000000000..d2b28a05f1 --- /dev/null +++ b/.github/instructions/pester-test-status-and-working-meaning.instructions.md @@ -0,0 +1,299 @@ +--- +applyTo: "**/*.Tests.ps1" +--- + +# Pester Test Status Meanings and Working Tests + +## Purpose + +This guide clarifies Pester test outcomes and what it means for a test to be "working" - which requires both **passing** AND **actually validating functionality**. + +## Test Statuses in Pester + +### Passed ✓ +**Status Code**: `Passed` +**Exit Result**: Test ran successfully, all assertions passed + +**What it means**: +- Test executed without errors +- All `Should` statements evaluated to true +- Test setup and teardown completed without issues +- Test is **validating** the intended functionality + +**What it does NOT mean**: +- The feature is working (assertions could be wrong) +- The test is meaningful (could be testing wrong thing) +- The test exercises all code paths + +### Failed ✗ +**Status Code**: `Failed` +**Exit Result**: Test ran but assertions failed + +**What it means**: +- Test executed but an assertion returned false +- Expected value did not match actual value +- Test detected a problem with the functionality + +**Examples**: +``` +Expected $true but got $false +Expected 5 items but got 3 +Expected no error but got: Cannot find parameter +``` + +### Error ⚠ +**Status Code**: `Error` +**Exit Result**: Test crashed with an exception + +**What it means**: +- Test failed to complete +- An exception was thrown during test execution +- Could be in test setup, test body, or test cleanup +- Often indicates environmental issue, not code functional issue + +**Examples**: +``` +Cannot bind argument to parameter 'Path' because it is null +File not found: C:\expected\config.json +Access denied writing to registry +``` + +### Pending ⏳ +**Status Code**: `Pending` +**Exit Result**: Test ran but never completed assertions + +**What it means**: +- Test was explicitly marked as not ready to run +- `Set-ItResult -Pending` was called +- Used to indicate: known bugs, missing features, environmental issues + +**When to use Pending**: +- Test for feature in development +- Test disabled due to known bug (issue #1234) +- Test disabled due to intermittent failures being fixed +- Platform-specific issues being resolved + +**⚠️ WARNING**: Pending tests are NOT validating functionality. They hide problems. + +### Skipped ⊘ +**Status Code**: `Skipped` +**Exit Result**: Test did not run (detected at start) + +**What it means**: +- Test was intentionally not executed +- `-Skip` parameter or `It -Skip:$condition` was used +- Environment doesn't support this test + +**When to use Skip**: +- Test not applicable to current platform (Windows-only test on Linux) +- Test requires feature that's not available (admin privileges) +- Test requires specific configuration not present + +**Difference from Pending**: +- **Skip**: "This test shouldn't run here" (known upfront) +- **Pending**: "This test should eventually run but can't now" + +### Ignored ✛ +**Status Code**: `Ignored` +**Exit Result**: Test marked as not applicable + +**What it means**: +- Test has `[Ignore("reason")]` attribute +- Test is permanently disabled in this location +- Not the same as Skipped (which is conditional) + +**When to use Ignore**: +- Test for deprecated feature +- Test for bug that won't be fixed +- Test moved to different test file + +--- + +## What Does "Working" Actually Mean? + +A test is **working** when it meets BOTH criteria: + +### 1. **Test Status is PASSED** ✓ +```powershell +It "Test name" { + # Test executes + # All assertions pass + # Returns Passed status +} +``` + +### 2. **Test Actually Validates Functionality** +```powershell +# ✓ GOOD: Tests actual functionality +It "Get-Item returns files from directory" -Tags @('Unit') { + $testDir = New-Item -ItemType Directory -Force + New-Item -Path $testDir -Name "file.txt" -ItemType File | Out-Null + + $result = Get-Item -Path "$testDir\file.txt" + + $result.Name | Should -Be "file.txt" + $result | Should -Exist + + Remove-Item $testDir -Recurse -Force +} + +# ✗ BAD: Returns Passed but doesn't validate functionality +It "Get-Item returns files from directory" -Tags @('Unit') { + $result = Get-Item -Path somepath # May not exist, may not actually test + $result | Should -Not -BeNullOrEmpty # Too vague +} + +# ✗ BAD: Test marked Pending - validation is hidden +It "Get-Item returns files from directory" -Tags @('Unit') { + Set-ItResult -Pending -Because "File system not working" + return + # No validation happens at all +} +``` + +--- + +## The Problem with Pending Tests + +### Why Pending Tests Hide Problems + +```powershell +# BAD: Test marked Pending - looks like "working" status but validation is skipped +It "Download help from web" { + Set-ItResult -Pending -Because "Web connectivity issues" + return + + # This code never runs: + Update-Help -Module PackageManagement -Force -ErrorAction Stop + Get-Help Get-Package | Should -Not -BeNullOrEmpty +} +``` + +**Result**: +- ✗ Feature is broken (Update-Help fails) +- ✓ Test shows "Pending" (looks acceptable) +- ✗ Problem is hidden and never fixed + +### The Right Approach + +**Option A: Fix the root cause** +```powershell +It "Download help from web" { + # Use local assets that are guaranteed to work + Update-Help -Module PackageManagement -SourcePath ./assets -Force -ErrorAction Stop + + Get-Help Get-Package | Should -Not -BeNullOrEmpty +} +``` + +**Option B: Gracefully skip when unavailable** +```powershell +It "Download help from web" -Skip:$(-not $hasInternet) { + Update-Help -Module PackageManagement -Force -ErrorAction Stop + Get-Help Get-Package | Should -Not -BeNullOrEmpty +} +``` + +**Option C: Add retry logic for intermittent issues** +```powershell +It "Download help from web" { + $maxRetries = 3 + $attempt = 0 + + while ($attempt -lt $maxRetries) { + try { + Update-Help -Module PackageManagement -Force -ErrorAction Stop + break + } + catch { + $attempt++ + if ($attempt -ge $maxRetries) { throw } + Start-Sleep -Seconds 2 + } + } + + Get-Help Get-Package | Should -Not -BeNullOrEmpty +} +``` + +--- + +## Test Status Summary Table + +| Status | Passed? | Validates? | Counts as "Working"? | Use When | +|--------|---------|------------|----------------------|----------| +| **Passed** | ✓ | ✓ | **YES** | Feature is working and test proves it | +| **Failed** | ✗ | ✓ | NO | Feature is broken or test has wrong expectation | +| **Error** | ✗ | ✗ | NO | Test infrastructure broken, can't validate | +| **Pending** | - | ✗ | **NO** ⚠️ | Temporary - test should eventually pass | +| **Skipped** | - | ✗ | NO | Test not applicable to this environment | +| **Ignored** | - | ✗ | NO | Test permanently disabled | + +--- + +## Recommended Patterns + +### Pattern 1: Resilient Test with Fallback +```powershell +It "Feature works with web or local source" { + $useLocal = $false + + try { + Update-Help -Module Package -Force -ErrorAction Stop + } + catch { + $useLocal = $true + Update-Help -Module Package -SourcePath ./assets -Force -ErrorAction Stop + } + + # Validate functionality regardless of source + Get-Help Get-Package | Should -Not -BeNullOrEmpty +} +``` + +### Pattern 2: Conditional Skip with Clear Reason +```powershell +Describe "Update-Help from Web" -Skip $(-not (Test-InternetConnectivity)) { + It "Downloads help successfully" { + Update-Help -Module PackageManagement -Force -ErrorAction Stop + Get-Help Get-Package | Should -Not -BeNullOrEmpty + } +} +``` + +### Pattern 3: Separate Suites by Dependency +```powershell +Describe "Help Content Tests - Web" { + # Tests that require internet - can be skipped if unavailable + It "Downloads from web" { ... } +} + +Describe "Help Content Tests - Local" { + # Tests with local assets - should always pass + It "Loads from local assets" { + Update-Help -Module Package -SourcePath ./assets -Force + Get-Help Get-Package | Should -Not -BeNullOrEmpty + } +} +``` + +--- + +## Checklist: Is Your Test "Working"? + +- [ ] Test status is **Passed** (not Pending, not Skipped, not Failed) +- [ ] Test actually **executes** the feature being tested +- [ ] Test has **specific assertions** (not just `Should -Not -BeNullOrEmpty`) +- [ ] Test includes **cleanup** (removes temp files, restores state) +- [ ] Test can run **multiple times** without side effects +- [ ] Test failure **indicates a real problem** (not flaky assertions) +- [ ] Test success **proves the feature works** (not just "didn't crash") + +If any of these is false, your test may be passing but not "working" properly. + +--- + +## See Also + +- [Pester Documentation](https://pester.dev/) +- [Set-ItResult Documentation](https://pester.dev/docs/commands/Set-ItResult) diff --git a/.github/skills/analyze-pester-failures/SKILL.md b/.github/skills/analyze-pester-failures/SKILL.md new file mode 100644 index 0000000000..ec1b0fe82e --- /dev/null +++ b/.github/skills/analyze-pester-failures/SKILL.md @@ -0,0 +1,524 @@ +--- +name: analyze-pester-failures +description: Troubleshooting guide for analyzing and investigating Pester test failures in PowerShell CI jobs. Help agents understand why tests are failing, interpret test output, navigate test result artifacts, and provide actionable recommendations for fixing test issues. +--- + +# Analyze Pester Test Failures + +Investigate and troubleshoot Pester test failures in GitHub Actions workflows. Understand what tests are failing, why they're failing, and provide recommendations for test fixes. + +| Skill | When to Use | +|-------|-----------| +| analyze-pester-failures | When investigating why Pester tests are failing in a CI job. Use when a test job shows failures and you need to understand what test failed, why it failed, what the error message means, and what might need to be fixed. Also use when asked: "why did this test fail?", "what's the test error?", "test is broken", "test failure analysis", "debug test failure", or given test failure logs and stack traces. | + +## When to Use This Skill + +Use this skill when you need to: + +- Understand why a specific Pester test is failing +- Interpret test failure messages and error output +- Analyze test result data from CI workflow runs (XML, logs, stack traces) +- Identify the root cause of test failures (test logic, assertion failure, exception, timeout, skip/ignore reason) +- Provide recommendations for fixing failing tests +- Compare expected vs. actual test behavior +- Debug test environment issues (missing dependencies, configuration problems) +- Understand test skip/ignored/inconclusive status reasons + +**Do not use this skill for:** +- General PowerShell debugging unrelated to tests +- Test infrastructure/CI setup issues (except as they affect test failure interpretation) +- Performance analysis or benchmarking (that's a different investigation) + +## Quick Start + +### ⚠️ CRITICAL: The Workflow Must Be Followed IN ORDER + +This skill describes a **sequential 6-step analysis workflow**. Skipping steps or jumping around leads to **incomplete analysis and incorrect conclusions**. + +**The Problem**: It's easy to skip to Step 4 or 5 without doing Steps 1-2, resulting in missing data and bad conclusions. + +**The Solution**: Use the automated analysis script to enforce the workflow: + +```powershell +# Automatically runs Steps 1-6 in order, preventing skipping +./.github/skills/analyze-pester-failures/scripts/analyze-pr-test-failures.ps1 -PR + +# Example: +./.github/skills/analyze-pester-failures/scripts/analyze-pr-test-failures.ps1 -PR 26800 +``` + +This script: +1. ✓ Fetches PR status automatically +2. ✓ Downloads artifacts (can't skip, depends on Step 1) +3. ✓ Extracts failures (can't skip, depends on Step 2) +4. ✓ Analyzes error messages +5. ✓ Documents context +6. ✓ Generates recommendations + +**Only use the manual commands below if you fully understand the workflow.** + +### Manual Workflow (for reference) + +```powershell +# Step 1: Identify the failing job +gh pr view --json 'statusCheckRollup' | ConvertFrom-Json | Where-Object { $_.conclusion -eq 'FAILURE' } + +# Step 2: Download artifacts (extract RUN_ID from Step 1) +gh run download --dir ./artifacts +gh run view --log > test-logs.txt + +# Step 3-6: Extract, analyze, and interpret +# (See Analysis Workflow section below) +``` + +## Common Test Failure Analysis Approaches + +### 1. **Interpreting Assertion Failures** +The most common test failure is when an assertion doesn't match expectations. + +**Example:** +``` +Expected $true but got $false at /path/to/test.ps1:42 +Assertion failed: Should -Be "expected" but was "actual" +``` + +**How to analyze:** +- Read the assertion message: what was expected vs. what was actual? +- Check the test logic: is the expectation correct? +- Look for mock/stub issues: are dependencies configured correctly? +- Check parameter values: what inputs were passed to the function under test? + +### 2. **Exception Failures** +Tests fail when PowerShell throws an exception instead of successful completion. + +**Example:** +``` +Command: Write-Host $null +Error: Cannot bind argument to parameter 'Object' because it is null. +``` + +**How to analyze:** +- Read the exception message: what operation failed? +- Check the stack trace: where in the test or tested code did it throw? +- Verify preconditions: does the test setup provide required values/mocks? +- Look for environmental issues: missing modules, permissions, file system state? + +### 3. **Timeout Failures** +A test takes longer than the allowed timeout to complete. + +**Example:** +``` +Test 'Should complete in reasonable time' timed out after 30 seconds +``` + +**How to analyze:** +- Is the timeout appropriate for this test type? (network tests need more time) +- Is there an infinite loop in the test or tested code? +- Are there resource contention issues on the CI runner? +- Does the test hang waiting for something (file lock, network, process)? + +### 4. **Skip/Ignored Reason Analysis** +Tests marked as skipped or ignored provide clues about test environment. + +**Example:** +``` +Test marked [Skip("Only runs on Windows")] - running on Linux +Test marked [Ignore("Known issue #12345")] +``` + +**How to analyze:** +- Read the skip/ignore reason: is it still valid? +- Check if environment has changed: platform, module versions, etc. +- Verify issue status: is the known issue still open? Has it been fixed? +- Determine if skip should be removed or if test needs environment changes + +### 5. **Flaky/Intermittent Failures** +Tests that sometimes pass, sometimes fail indicate race conditions or environment sensitivity. + +**Example:** +- Test passes locally but fails on CI +- Test passes first run of suite, fails on second run +- Test passes on Windows but fails on Linux + +**How to analyze:** +- Look for timeout races: is timing involved in the test? +- Check for test isolation issues: does one test affect another? +- Verify environment differences: CI vs. local paths, permissions, versions +- Look for external dependencies: network calls, file I/O, process interactions + +## Key Artifacts and Locations + +| Item | Purpose | Location | +|------|---------|----------| +| Test result XML | Pester output with test cases, failures, errors | Workflow artifacts: `junit-pester-*.xml` | +| Job logs | Full job output including test execution and errors | GitHub Actions run logs or `gh run download` | +| Stack traces | Error location information from failed assertions | Within job logs and XML failure messages | +| Test files | The actual Pester test code (`.ps1` files) | `test/` directory in repository | + +## Analysis Workflow + +### ⚠️ Important: These Steps MUST Be Followed In Order + +Each step depends on the previous one. Skipping or re-ordering steps causes incomplete analysis: + +- **Step 1** (identify jobs) → You get the RUN_ID needed for Step 2 +- **Step 2** (download) → You get the artifacts needed for Step 3 +- **Step 3** (extract) → You discover what failures exist for Step 4 +- **Step 4** (read messages) → You understand the errors to analyze in Step 5 +- **Step 5** (context) → You gather information to make recommendations in Step 6 +- **Step 6** (interpret) → You use all above to recommend fixes + +**Real Problem We Had**: +- ❌ Jumped to Step 3 without Step 1-2 +- ❌ Used random test data from context instead of downloading PR artifacts +- ❌ Skipped Steps 5-6 entirely +- ❌ Made recommendations without full context + +**Result**: Wrong analysis and recommendations that didn't actually fix the problem. + +### Recommended: Use the Automated Script + +```powershell +./.github/skills/analyze-pester-failures/scripts/analyze-pr-test-failures.ps1 -PR +``` + +This enforces the workflow and prevents skipping. + +### Step 2: Get Test Results + +Fetch the test result artifacts and job logs: + +```powershell +# Download artifacts including test XML results +gh run download --dir ./artifacts + +# Get job logs +gh run view --log > test-logs.txt + +# Inspect test XML +$xml = [xml](Get-Content ./artifacts/junit-pester-*.xml) +$xml.'test-results' | Select-Object total, failures, errors, ignored, inconclusive +``` + +### Step 3: Extract Specific Failures + +Find the failing test cases in the XML: + +```powershell +# Get all failed test cases +$xml = [xml](Get-Content ./artifacts/junit-pester-*.xml) +$failures = $xml.SelectNodes('.//test-case[@result = "Failure"]') + +# For each failure, display key info +$failures | ForEach-Object { + [PSCustomObject]@{ + Name = $_.name + Description = $_.description + Message = $_.failure.message + StackTrace = $_.failure.'stack-trace' + } +} +``` + +### Step 4: Read the Error Message + +The error message tells you what went wrong: + +**Assertion failures:** +``` +Expected $true but got $false +Expected "value1" but got "value2" +Expression should have failed with exception, but didn't +``` + +**Exceptions:** +``` +Cannot find a parameter with name 'Name' +Property 'Property' does not exist on 'Object' +Cannot bind argument to parameter because it is null +``` + +**Timeouts:** +``` +Test timed out after 30 seconds +Test is taking too long to complete +``` + +### Step 5: Understand the Context + +Look at the test file to understand what was being tested: + +```powershell +# Find the test file mentioned in the stack trace +# Example: /path/to/test/Feature.Tests.ps1:42 + +# Read the test code around that line +code : + +# Understand: +# - What assertion is on that line? +# - What is the test trying to verify? +# - What are the setup/mock/before conditions? +# - Are there recent changes to the function being tested? +``` + +### Step 6: Interpret the Failure + +Determine the root cause category: + +**Test issue (needs code fix):** +- Assertion logic is wrong +- Test expectations don't match actual behavior +- Test setup is incomplete +- Mock/stub configuration missing + +**Environmental issue (needs environment change):** +- Test assumes a specific file or registry entry exists +- Test requires Windows/Linux specifically +- Test requires specific PowerShell version +- Test requires specific module version +- Timing-sensitive test affected by CI load + +**Data issue (needs input data change):** +- Test data no longer valid +- External API changed format +- Configuration file has changed structure + +**Flakiness (needs test hardening):** +- Race condition in test +- Timing assumptions too tight +- Resource contention on CI runner +- Non-deterministic behavior in tested code + +## Common Test Failure Patterns + +| Pattern | What It Means | Example | Next Step | +|---------|---------------|---------|-----------| +| `Expected $true but got $false` | Assertion on boolean result failed | Test expects function returns true, but it returns false | Check function logic for bug or test logic for wrong expectation | +| `Cannot find path` | File or directory doesn't exist | Test tries to read config file that's not present | Verify file path, check test setup, ensure CI environment has file | +| `Cannot bind argument to parameter 'X'` | Required parameter value is null or wrong type | Function called with $null where object expected | Check test mock setup, verify parameter types | +| `Test timed out after X seconds` | Test exceeded time limit | Network call or loop takes too long | Increase timeout for slow test, find infinite loop, mock network calls | +| `Expression should have failed but didn't` | Exception wasn't thrown when expected | Test expects error but function succeeds | Check if function behavior changed, update test expectation | +| `Could not find parameter 'X'` | Function doesn't have parameter | Test calls function with parameter that doesn't exist | Check PowerShell version, verify function signature, update test | +| `This platform is not supported` | Test skipped on current OS | Windows-only test running on Linux | Add platform check, update test environment, or mark as platform-specific | +| `Test marked [Ignore]` | Test explicitly disabled | Test has `[Ignore("reason")]` attribute | Check if reason still valid, remove if issue fixed | + +## Interpreting Test Results + +### Test Result Counts + +Pester test outcomes are categorized as: + +| Count | Meaning | Notes | +|-------|---------|-------| +| `total` | Total number of test cases executed | Should match: passed + failed + errors + skipped + ignored | +| `failures` | Test assertions that failed | `Expected X but got Y` type failures | +| `errors` | Tests that threw exceptions | Unhandled PowerShell exceptions during test | +| `skipped` | Tests explicitly skipped (marked with `-Skip`) | Test code recognizes condition and skips | +| `ignored` | Tests marked as ignored (marked with `-Ignore`) | Test disabled intentionally, usually notes reason | +| `inconclusive` | Tests with unclear result | Rare; usually means test framework issue | +| `passed` | Tests with passing assertions | `total - failures - errors - skipped - ignored` | + +### Stack Trace Interpretation + +A stack trace shows where the failure occurred: + +``` +at /home/runner/work/PowerShell/test/Feature.Tests.ps1:42 + +Means: +- File: /home/runner/work/PowerShell/test/Feature.Tests.ps1 +- Line: 42 +- Look at that line to see which assertion failed +``` + +### Understanding Skipped Tests + +When XML shows `result="Ignored"` or `result="Skipped"`: + +```xml + + Only runs on Windows + +``` + +The reason explains why test didn't run. Not a failure, but important for understanding test coverage. + +## Providing Test Failure Analysis + +### Investigation Questions + +After gathering test output, ask yourself: + +1. **Is the test code correct?** + - Does the test assertion match the expected behavior? + - Are test expectations still valid? + - Has the function being tested changed? + +2. **Is the test setup correct?** + - Are mocks/stubs configured properly? + - Does the test environment have required files/configuration? + - Are preconditions (database, files, services) met? + +3. **Is this a code bug or test issue?** + - Does the tested function have a logic error? + - Or does the test have incorrect expectations? + +4. **Is this environment-specific?** + - Only fails on Windows/Linux? + - Only fails on CI but passes locally? + - Timing-dependent or resource-dependent? + +5. **Is this a known/expected failure?** + - Is there already an issue tracking this failure? + - Is the test marked as flaky or expected to fail? + - Does the skip/ignore reason still apply? + +### Recommendation Framework + +Based on your analysis: + +| Finding | Recommendation | +|---------|-----------------| +| Test logic is wrong | "Test assertion on line X is incorrect. Test expects Y but function correctly returns Z. Update test expectation." | +| Tested code has bug | "Function at file.ps1#L42 has logic error. When X happens, returns Y instead of Z. Fix the condition." | +| Missing test setup | "Test setup incomplete. Mock for dependency Y is not configured. Add `Mock Get-Y -MockWith { ... }`" | +| Environment issue | "Test is Windows-specific but running on Linux. Either add platform check or skip on non-Windows." | +| Flaky test | "Test is timing-sensitive (sleep 1 second). Increase timeout or use better synchronization." | +| Test should be skipped | "Test is marked Ignored for good reason. Keep it disabled until issue #12345 is fixed." | + +### Tone and Structure + +Provide analysis as: + +1. **Summary** (1 sentence): What test is failing and general category +2. **Failure Details** (2-3 sentences): What the test output says +3. **Root Cause** (1-2 sentences): Why it's failing (test bug vs. code bug vs. environment) +4. **Recommendation** (actionable): What should be done to fix it +5. **Context** (optional): Link to related code, issues, or recent changes + +## Examples + +### Example 1: Assertion Failure Due to Code Bug + +**Test Output:** +``` +Expected 5 but got 3 at /path/to/Test.ps1:42 +``` + +**Investigation:** +1. Look at line 42: `$result | Should -Be 5` +2. Check the test: It expects function to return 5 items +3. Check the function: It returns `$items | Where-Object {$_.Status -eq "Active"}` but the filter is wrong +4. Root cause: Function has logic error, not test error + +**Recommendation:** +``` +Test failure is due to a code bug: + +The test Set-Configuration should return 5 items but returns 3. + +Looking at the tested function at [module.ps1#L42](module.ps1#L42): + $activeItems = $items | Where-Object {$_.Status -eq "Active"} + +The issue is the filter condition. It's currently filtering by "Active" status, +but should include "Pending" status as well. + +Fix: Change line 42 to: + $activeItems = $items | Where-Object {$_.Status -ne "Disabled"} + +Then re-run the test to verify it now returns 5 items as expected. +``` + +### Example 2: Test Setup Issue + +**Test Output:** +``` +Cannot find path '/expected/config.json' because it does not exist at /path/to/Test.ps1:15 +``` + +**Investigation:** +1. Line 15 tries to read a config file +2. The test setup doesn't create this file +3. Works locally but fails on CI because CI doesn't have the same file + +**Recommendation:** +``` +Test setup is incomplete: + +The test Initialize-Config fails because it expects /expected/config.json but the test doesn't create this file. + +The test needs to ensure the config file exists. Currently line 12-14 doesn't set up the file: + + # Before: + # (no setup of config file) + + # After: + @{ setting1 = "value1"; setting2 = "value2" } | ConvertTo-Json | + Out-File $testConfigPath + +Alternatively, the test function should accept a parameter for the config path and use a temporary file: + param([string]$ConfigPath = (New-TemporaryFile)) + +Re-run the test to verify the config file is properly available. +``` + +### Example 3: Platform-Specific Test Failure + +**Test Output:** +``` +Test 'should read Windows Registry' failed on Linux runner +Cannot find path 'HKEY_LOCAL_MACHINE:\...' +``` + +**Investigation:** +1. Test assumes Windows Registry exists (Windows-only) +2. Running on Linux runner doesn't have Registry +3. Test should skip on non-Windows platforms + +**Recommendation:** +``` +Test is platform-specific but running on wrong platform: + +The test "should read Windows Registry" assumes Windows Registry exists but is running on Linux. + +Add a platform check to skip this test on non-Windows systems: + + It "should read Windows Registry" -Skip:$(-not $IsWindows) { + # test code here + } + +Or group Windows-only tests in a separate Describe block with platform check: + + Describe "Windows Registry Tests" -Skip:$(-not $IsWindows) { + # all Windows-specific tests here + } + +This allows the test to be skipped on Linux/Mac while still running on Windows CI. +``` + +## References + +- [Pester Testing Framework](https://pester.dev/) — Official documentation, best practices for test writing +- [Test Files](../../../test/) — PowerShell test suite in repository +- [GitHub Actions Documentation](https://docs.github.com/en/actions) — Understanding workflow runs and logs +- [PowerShell Documentation](https://learn.microsoft.com/en-us/powershell/) — Language reference for understanding test code + +## Tips + +1. **Read the error message first:** The error message is usually the most direct clue to the problem +2. **Check test vs. code blame:** Is the test wrong or is the code wrong? Look at both sides +3. **Verify test isolation:** Does one test failure affect others? Check for shared state or test ordering dependencies +4. **Test locally first:** Try running the failing test locally to reproduce and understand it better +5. **Check for environmental assumptions:** Windows-specific paths, module versions, file locations may differ on CI +6. **Look for skip/ignore patterns:** If a test is consistently ignored, check if the reason is still valid +7. **Compare passing vs. failing:** If test passes locally but fails on CI, the difference is usually environment-related +8. **Check recent changes:** Did a recent PR change the tested code or test itself? +9. **Understand Pester output format:** Different Pester versions, different `-ErrorAction`, `-WarningAction` produce different test results +10. **Don't assume CI is wrong:** Failures on CI often reveal real issues that local testing missed (network, file permissions, parallelization, etc.) + +## Additional Links + +- [PowerShell Repository](https://github.com/PowerShell/PowerShell) +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Pester Testing Framework](https://github.com/Pester/Pester) diff --git a/.github/skills/analyze-pester-failures/references/stack-trace-parsing.md b/.github/skills/analyze-pester-failures/references/stack-trace-parsing.md new file mode 100644 index 0000000000..707f45560a --- /dev/null +++ b/.github/skills/analyze-pester-failures/references/stack-trace-parsing.md @@ -0,0 +1,163 @@ +# Understanding Pester Test Failures + +This reference explains how to interpret Pester test output and understand failure messages. + +## Supported Formats + +### Pester 4 Format +``` +at line: 123 in C:\path\to\file.ps1 +``` + +**Regex Pattern:** +```powershell +if ($StackTraceString -match 'at line:\s*(\d+)\s+in\s+(.+?)(?:\r|\n|$)') { + $result.Line = $matches[1] + $result.File = $matches[2].Trim() + return $result +} +``` + +### Pester 5 Format (Common) +``` +at 1 | Should -Be 2, C:\path\to\file.ps1:123 +at 1 | Should -Be 2, /home/runner/work/PowerShell/PowerShell/test/file.ps1:123 +``` + +**Regex Pattern:** +```powershell +if ($StackTraceString -match ',\s*((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1):(\d+)') { + $result.File = $matches[1].Trim() + $result.Line = $matches[2] + return $result +} +``` + +### Alternative Format +``` +at C:\path\to\file.ps1:123 +at /path/to/file.ps1:123 +``` + +**Regex Pattern:** +```powershell +if ($StackTraceString -match 'at\s+((?:[A-Za-z]:)?[\/\\][^,]+?\.ps[m]?1):(\d+)(?:\r|\n|$)') { + $result.File = $matches[1].Trim() + $result.Line = $matches[2] + return $result +} +``` + +## Troubleshooting Parsing Failures + +### Issue: Line Number Extracted But File Path Is Null + +**Cause:** Stack trace matches line-with-path pattern but file extraction doesn't work + +**Solution:** +1. Check if file path exists as expected in filesystem +2. Verify regex doesn't have too-greedy bounds (check use of `.+?` vs `.+`) +3. Test regex against actual stack trace string: + ```powershell + $trace = "at line: 42 in C:\path\to\test.ps1" + if ($trace -match 'at line:\s*(\d+)\s+in\s+(.+?)(?:\r|\n|$)') { + Write-Host "File: $($matches[2])" # Should be "C:\path\to\test.ps1" + } + ``` + +### Issue: Special Characters in File Path Break Regex + +**Cause:** Characters like parens `()`, brackets `[]`, pipes `|` have special meaning in regex + +**Solution:** +1. Escape special chars in regex: `[Regex]::Escape($path)` +2. Use character class `[\/\\]` instead of alternation for path separators +3. Test with files containing problematic names: + ```powershell + $traces = @( + "at line: 1 in C:\path\(with)\parens\test.ps1", + "at /home/user/[brackets]/test.ps1:5", + "at C:\path\with spaces\test.ps1:10" + ) + # Test each against all patterns + ``` + +### Issue: Regex Matches But Extracts Wrong Values + +**Symptom:** $matches[1] is file instead of line, or vice versa + +**Debug Steps:** +1. Print all captured groups: `$matches.Values | Format-Table -AutoSize` +2. Verify group order in regex matches expectations +3. Test with sample Pester output: + ```powershell + $sampleTrace = @" + at 1 | Should -Be 2, /home/runner/work/PowerShell/test/file.ps1:42 + "@ + + if ($sampleTrace -match ',\s*((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1):(\d+)') { + Write-Host "Match 1: $($matches[1])" # Should be file path + Write-Host "Match 2: $($matches[2])" # Should be line number + } + ``` + +## Testing the Parser + +Use this PowerShell script to validate `Get-PesterFailureFileInfo`: + +```powershell +# Import the function +. ./build.psm1 + +$testCases = @( + @{ + Input = "at line: 42 in C:\path\to\test.ps1" + Expected = @{ File = "C:\path\to\test.ps1"; Line = "42" } + }, + @{ + Input = "at /home/runner/work/test.ps1:123" + Expected = @{ File = "/home/runner/work/test.ps1"; Line = "123" } + }, + @{ + Input = "at 1 | Should -Be 2, /path/to/file.ps1:99" + Expected = @{ File = "/path/to/file.ps1"; Line = "99" } + } +) + +foreach ($test in $testCases) { + $result = Get-PesterFailureFileInfo -StackTraceString $test.Input + + $fileMatch = $result.File -eq $test.Expected.File + $lineMatch = $result.Line -eq $test.Expected.Line + $status = if ($fileMatch -and $lineMatch) { "✓ PASS" } else { "✗ FAIL" } + + Write-Host "$status : $($test.Input)" + if (-not $fileMatch) { Write-Host " Expected file: $($test.Expected.File), got: $($result.File)" } + if (-not $lineMatch) { Write-Host " Expected line: $($test.Expected.Line), got: $($result.Line)" } +} +``` + +## Adding Support for New Formats + +When Pester changes its output format: + +1. **Capture sample output** from failing tests +2. **Identify the pattern** (e.g., "file path always after comma followed by colon") +3. **Write regex** to match pattern without over-matching +4. **Add to `Get-PesterFailureFileInfo`** before existing patterns (order matters for fallback) +5. **Test with samples** containing special characters, long paths, and edge cases + +Example: Adding a new format at the top of the function: + +```powershell +# Try pattern: "at , :" (Pester 5.1 hypothetical) +if ($StackTraceString -match 'at .+?, ((?:[A-Za-z]:)?[\/\\].+?\.ps[m]?1):(\d+)') { + $result.File = $matches[1].Trim() + $result.Line = $matches[2] + return $result +} + +# Try existing patterns... +``` + +Place new patterns **first** so they take precedence over fallback patterns. diff --git a/.github/skills/analyze-pester-failures/scripts/analyze-pr-test-failures.ps1 b/.github/skills/analyze-pester-failures/scripts/analyze-pr-test-failures.ps1 new file mode 100644 index 0000000000..1248659607 --- /dev/null +++ b/.github/skills/analyze-pester-failures/scripts/analyze-pr-test-failures.ps1 @@ -0,0 +1,456 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Automated Pester test failure analysis workflow for GitHub PRs. + +.DESCRIPTION + This script automates the complete analysis workflow defined in the analyze-pester-failures + skill. It performs all steps in order: + 1. Identify failing test jobs in the PR + 2. Download test artifacts and logs + 3. Extract specific test failures + 4. Parse error messages + 5. Search logs for error markers and generate recommendations + + By automating the workflow, this ensures analysis steps are followed in order + and nothing is skipped. + +.PARAMETER PR + The GitHub PR number to analyze (e.g., 26800) + +.PARAMETER Owner + Repository owner (default: PowerShell) + +.PARAMETER Repo + Repository name (default: PowerShell) + +.PARAMETER OutputDir + Directory to store analysis results (default: ./pester-analysis-PR) + +.PARAMETER Interactive + Prompt for recommendations after analysis (default: non-interactive) + +.PARAMETER ForceDownload + Force re-download of artifacts and logs, even if they already exist + +.EXAMPLE + .\.github\skills\analyze-pester-failures\scripts\analyze-pr-test-failures.ps1 -PR 26800 + Analyzes PR #26800 and saves results to ./pester-analysis-PR26800 + +.EXAMPLE + .\.github\skills\analyze-pester-failures\scripts\analyze-pr-test-failures.ps1 -PR 26800 -Interactive + Interactive mode: shows failures and prompts for next steps + +.EXAMPLE + .\.github\skills\analyze-pester-failures\scripts\analyze-pr-test-failures.ps1 -PR 26800 -ForceDownload + Re-download all logs and artifacts, skipping the cache + +.NOTES + Requires: GitHub CLI (gh) configured and authenticated + This script enforces the workflow defined in .github/skills/analyze-pester-failures/SKILL.md +#> + +param( + [Parameter(Mandatory)] + [int]$PR, + + [string]$Owner = 'PowerShell', + [string]$Repo = 'PowerShell', + [string]$OutputDir, + [switch]$Interactive, + [switch]$ForceDownload +) + +$ErrorActionPreference = 'Stop' + +if (-not $OutputDir) { + $OutputDir = "./pester-analysis-PR$PR" +} + +# Colors for output +$colors = @{ + Step = [ConsoleColor]::Cyan + Success = [ConsoleColor]::Green + Warning = [ConsoleColor]::Yellow + Error = [ConsoleColor]::Red + Info = [ConsoleColor]::Gray +} + +function Write-Step { + param([string]$text, [int]$number) + Write-Host "`n[$number/6] $text" -ForegroundColor $colors.Step -BackgroundColor Black +} + +function Write-Result { + param([string]$text, [ValidateSet('Success','Warning','Error','Info')]$type = 'Info') + Write-Host $text -ForegroundColor $colors[$type] +} + +Write-Host "`n=== Pester Test Failure Analysis ===" -ForegroundColor $colors.Step +Write-Host "PR: $Owner/$Repo#$PR" -ForegroundColor $colors.Info +Write-Host "Output Directory: $OutputDir" -ForegroundColor $colors.Info + +# Ensure output directory exists +if (-not (Test-Path $OutputDir)) { + New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null +} + +# STEP 1: Identify the Failing Test Job +Write-Step "Identify failing test jobs" 1 + +Write-Result "Fetching PR status checks..." Info +$prResponse = gh pr view $PR --repo "$Owner/$Repo" --json 'statusCheckRollup' | ConvertFrom-Json +$allChecks = $prResponse.statusCheckRollup + +$failedJobs = $allChecks | Where-Object { $_.conclusion -eq 'FAILURE' } + +if (-not $failedJobs) { + Write-Result "✓ No failed jobs found" Success + Write-Host " Total checks: $($allChecks.Count)" + $allChecks | Where-Object { $_ } | ForEach-Object { + Write-Host " - $($_.name): $($_.conclusion)" -ForegroundColor $colors.Info + } + exit 0 +} + +Write-Result "✓ Found $($failedJobs.Count) failing job(s)" Warning + +$failedJobs | Where-Object { $_.conclusion -eq 'FAILURE' } | ForEach-Object { + Write-Host " ✗ $($_.name) - $($_.conclusion)" -ForegroundColor $colors.Error + if ($_.detailsUrl) { + Write-Host " URL: $($_.detailsUrl)" -ForegroundColor $colors.Info + } +} + +if ($Interactive) { + Write-Host "`nPress Enter to continue to Step 2..." + Read-Host | Out-Null +} + +# STEP 2: Get Test Results +Write-Step "Download test artifacts and logs" 2 + +# Extract unique run IDs from failing jobs +$uniqueRuns = @() +foreach ($failedJob in $failedJobs) { + if ($failedJob.detailsUrl -match 'runs/(\d+)') { + $runId = $matches[1] + if ($runId -notin $uniqueRuns) { + $uniqueRuns += $runId + } + } +} + +if ($uniqueRuns.Count -eq 0) { + Write-Result "✗ Could not extract run IDs from failing jobs" Error + exit 1 +} + +Write-Result "Found $($uniqueRuns.Count) run(s): $($uniqueRuns -join ', ')" Info + +$artifactDir = Join-Path $OutputDir artifacts + +# Check if artifacts already exist +$existingArtifacts = Get-ChildItem $artifactDir -Recurse -File -ErrorAction SilentlyContinue + +if ($existingArtifacts -and -not $ForceDownload) { + Write-Result "✓ Artifacts already downloaded" Success + $existingArtifacts | ForEach-Object { + Write-Host " - $($_.FullName)" -ForegroundColor $colors.Info + } +} else { + Write-Result "Downloading artifacts from run $($uniqueRuns[0])..." Info + gh run download $uniqueRuns[0] --dir $artifactDir --repo "$Owner/$Repo" 2>&1 | Out-Null + + if (Test-Path $artifactDir) { + Write-Result "✓ Artifacts downloaded" Success + Get-ChildItem $artifactDir -Recurse -File | ForEach-Object { + Write-Host " - $($_.FullName)" -ForegroundColor $colors.Info + } + } else { + Write-Result "✗ Failed to download artifacts" Error + exit 1 + } +} + +# Download individual job logs for failing jobs +Write-Result "Downloading individual job logs..." Info + +$logsDir = Join-Path $OutputDir "logs" +if (-not (Test-Path $logsDir)) { + New-Item -ItemType Directory -Path $logsDir -Force | Out-Null +} + +# Check if logs already exist +$existingLogs = Get-ChildItem $logsDir -Filter "*.txt" -ErrorAction SilentlyContinue + +if ($existingLogs -and -not $ForceDownload) { + Write-Result "✓ Job logs already downloaded" Success + $existingLogs | ForEach-Object { + Write-Host " - $($_.Name)" -ForegroundColor $colors.Info + } +} else { + # Process each run and get its jobs + $failedJobIds = @() + foreach ($runId in $uniqueRuns) { + $runJobs = gh run view $runId --repo "$Owner/$Repo" --json jobs | ConvertFrom-Json + + foreach ($failedJob in $failedJobs) { + # Check if this failed job belongs to this run + if ($failedJob.detailsUrl -match "runs/$runId/") { + $jobMatch = $runJobs.jobs | Where-Object { $_.name -eq $failedJob.name } | Select-Object -First 1 + if ($jobMatch) { + $failedJobIds += @{ + name = $failedJob.name + id = $jobMatch.databaseId + runId = $runId + } + } + } + } + } + + # Download logs for all failed jobs + foreach ($jobInfo in $failedJobIds) { + $logFile = Join-Path $logsDir ("log-{0}.txt" -f ($jobInfo.name -replace '[^a-zA-Z0-9-]', '_')) + Write-Result " Downloading: $($jobInfo.name) (Run $($jobInfo.runId))" Info + gh run view $jobInfo.runId --log --job $jobInfo.id --repo "$Owner/$Repo" > $logFile 2>&1 + } + + Write-Result "✓ Job logs downloaded" Success + Get-ChildItem $logsDir -Filter "*.txt" | ForEach-Object { + Write-Host " - $($_.Name)" -ForegroundColor $colors.Info + } +} + +if ($Interactive) { + Write-Host "`nPress Enter to continue to Step 3..." + Read-Host | Out-Null +} + +# STEP 3: Extract Specific Failures +Write-Step "Extract test failures from XML" 3 + +$xmlFiles = Get-ChildItem $artifactDir -Filter "*.xml" -Recurse +if (-not $xmlFiles) { + Write-Result "✗ No test result XML files found" Error + exit 1 +} + +Write-Result "✓ Found $($xmlFiles.Count) test result file(s)" Success + +$allFailures = @() + +foreach ($xmlFile in $xmlFiles) { + Write-Result "`nParsing: $($xmlFile.Name)" Info + + try { + [xml]$xml = Get-Content $xmlFile + $testResults = $xml.'test-results' + + Write-Host " Total: $($testResults.total)" -ForegroundColor $colors.Info + Write-Host " Passed: $($testResults.passed)" -ForegroundColor $colors.Success + if ($testResults.failures -gt 0) { + Write-Host " Failed: $($testResults.failures)" -ForegroundColor $colors.Error + } + if ($testResults.errors -gt 0) { + Write-Host " Errors: $($testResults.errors)" -ForegroundColor $colors.Error + } + if ($testResults.skipped -gt 0) { + Write-Host " Skipped: $($testResults.skipped)" -ForegroundColor $colors.Warning + } + if ($testResults.ignored -gt 0) { + Write-Host " Ignored: $($testResults.ignored)" -ForegroundColor $colors.Warning + } + + # Extract failures + $failures = $xml.SelectNodes('.//test-case[@result = "Failure"]') + + foreach ($failure in $failures) { + $allFailures += @{ + Name = $failure.name + File = $xmlFile.Name + Message = $failure.failure.message + StackTrace = $failure.failure.'stack-trace' + } + } + } catch { + Write-Result "✗ Error parsing XML: $_" Error + } +} + +Write-Result "`n✓ Extracted $($allFailures.Count) failures total" Success + +# Save failures to JSON for later analysis +$allFailures | ConvertTo-Json -Depth 10 | Out-File (Join-Path $OutputDir "failures.json") + +if ($Interactive) { + Write-Host "`nPress Enter to continue to Step 4..." + Read-Host | Out-Null +} + +# STEP 4: Read Error Messages +Write-Step "Analyze error messages" 4 + +$failuresByType = @{} + +foreach ($failure in $allFailures) { + $message = $failure.Message -split "`n" | Select-Object -First 1 + + # Categorize failure + $type = 'Other' + if ($message -match 'Expected .* but got') { $type = 'Assertion' } + elseif ($message -match 'Cannot (find|bind)') { $type = 'Exception' } + elseif ($message -match 'timed out') { $type = 'Timeout' } + + if (-not $failuresByType[$type]) { + $failuresByType[$type] = @() + } + $failuresByType[$type] += $failure +} + +Write-Result "Failure breakdown:" Info +$failuresByType.GetEnumerator() | ForEach-Object { + Write-Host " $($_.Key): $($_.Value.Count)" -ForegroundColor $colors.Warning +} + +Write-Result "`nTop failure messages:" Info +$allFailures | Group-Object Message | Sort-Object Count -Descending | Select-Object -First 3 | ForEach-Object { + Write-Host " [$($_.Count)x] $($_.Name -split "`n" | Select-Object -First 1)" -ForegroundColor $colors.Info +} + +# Save analysis +$analysis = @{ + FailuresByType = @{} + TopMessages = @() +} + +$failuresByType.GetEnumerator() | ForEach-Object { + $analysis.FailuresByType[$_.Key] = $_.Value.Count +} + +$allFailures | Group-Object Message | Sort-Object Count -Descending | Select-Object -First 5 | ForEach-Object { + $analysis.TopMessages += @{ + Count = $_.Count + Message = ($_.Name -split "`n" | Select-Object -First 1) + } +} + +$analysis | ConvertTo-Json | Out-File (Join-Path $OutputDir "analysis.json") + +if ($Interactive) { + Write-Host "`nPress Enter to continue to Step 5..." + Read-Host | Out-Null +} + +# STEP 5: Search Logs for Error Markers +Write-Step "Search logs for error markers" 5 + +$logsDir = Join-Path $OutputDir "logs" +if (-not (Test-Path $logsDir)) { + Write-Result "⚠ Logs directory not found" Warning +} else { + $logFiles = Get-ChildItem $logsDir -Filter "*.txt" -ErrorAction SilentlyContinue + if (-not $logFiles) { + Write-Result "⚠ No log files found in logs directory" Warning + } else { + Write-Result "Searching $($logFiles.Count) job log(s) for error markers ([-])" Info + Write-Result "Format: [JobName] [LineNumber] Content" Info + Write-Host "" + + $allErrorLines = @() + + foreach ($logFile in $logFiles) { + $jobName = $logFile.BaseName -replace '^log-', '' + $logLines = @(Get-Content $logFile) + + for ($i = 0; $i -lt $logLines.Count; $i++) { + $line = $logLines[$i] + if ($line -match '\s\[-\]\s') { + $allErrorLines += @{ + JobName = $jobName + LineNumber = $i + 1 + Content = $line + } + } + } + } + + if ($allErrorLines.Count -gt 0) { + Write-Result "✓ Found $($allErrorLines.Count) error marker line(s)" Warning + + $allErrorLines | ForEach-Object { + Write-Host " [$($_.JobName)] [$($_.LineNumber)] $($_.Content)" -ForegroundColor $colors.Error + } + + # Save to file + $allErrorLines | ConvertTo-Json | Out-File (Join-Path $OutputDir "error-markers.json") + Write-Result "✓ Error markers saved to error-markers.json" Success + } else { + Write-Result "✓ No error markers found in logs" Success + } + } +} + +if ($Interactive) { + Write-Host "`nPress Enter to continue to Step 6..." + Read-Host | Out-Null +} + +# STEP 6: Generate Recommendations +Write-Step "Generate recommendations" 6 + +$recommendations = @() + +# Analyze patterns +if ($failuresByType['Assertion']) { + $recommendations += "Multiple assertion failures detected. These indicate test expectations don't match actual behavior." +} + +if ($failuresByType['Exception']) { + $recommendations += "Exception errors found. Check test setup and prerequisites - may indicate missing files, modules, or permissions." +} + +if ($failuresByType['Timeout']) { + $recommendations += "Timeout failures suggest slow or hanging operations. Consider network issues or resource constraints on CI." +} + +# Check for patterns in failure messages +$failureMessages = $allFailures.Message -join "`n" +if ($failureMessages -match 'PackageManagement') { + $recommendations += "PackageManagement module issues detected. Verify module availability and help repository access." +} + +if ($failureMessages -match 'Update-Help') { + $recommendations += "Update-Help failures detected. Check network connectivity to help repository and help installation paths." +} + +Write-Result "`n📋 Recommendations:" Info +if ($recommendations) { + $recommendations | ForEach-Object { Write-Host " • $_" -ForegroundColor $colors.Info } +} else { + Write-Host " • Review failures in detail" -ForegroundColor $colors.Info + Write-Host " • Check if test changes are needed" -ForegroundColor $colors.Info + Write-Host " • Consider environment-specific issues" -ForegroundColor $colors.Info +} + +$recommendations | Out-File (Join-Path $OutputDir "recommendations.txt") + +# Summary +Write-Host "`n=== Analysis Complete ===" -ForegroundColor $colors.Step +Write-Host "Results saved to: $OutputDir" -ForegroundColor $colors.Info +Write-Host " - failures.json (detailed failure data)" -ForegroundColor $colors.Info +Write-Host " - analysis.json (summary analysis)" -ForegroundColor $colors.Info +Write-Host " - recommendations.txt (suggested fixes)" -ForegroundColor $colors.Info +Write-Host " - error-markers.json (error markers from logs)" -ForegroundColor $colors.Info +Write-Host " - logs/ (individual job log files)" -ForegroundColor $colors.Info +Write-Host " - artifacts/ (downloaded test artifacts)" -ForegroundColor $colors.Info + +Write-Host "`nNext steps:" -ForegroundColor $colors.Step +Write-Host "1. Review recommendations.txt for analysis" -ForegroundColor $colors.Info +Write-Host "2. Examine failures.json for detailed error messages" -ForegroundColor $colors.Info +Write-Host "3. Check error-markers.json for specific test failures in logs" -ForegroundColor $colors.Info +Write-Host "4. Review individual job logs in logs/ directory for contextual details" -ForegroundColor $colors.Info +Write-Host "`n" diff --git a/.gitignore b/.gitignore index f115e61e22..48556cf1b8 100644 --- a/.gitignore +++ b/.gitignore @@ -119,5 +119,8 @@ assets/manpage/*.gz tmp/* .env.local +# Pester test failure analysis results (generated by analyze-pr-test-failures.ps1) +**/pester-analysis-*/ + # Ignore CTRF report files crtf/* From 8f457fc7140a758b1434940fc71320eef54e7874 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 17 Feb 2026 13:40:04 -0800 Subject: [PATCH 079/112] Skip the flaky `Update-Help` test for the `PackageManagement` module (#26845) --- .../engine/Help/UpdatableHelpSystem.Tests.ps1 | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 b/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 index 6bf82d21af..155654b3b8 100644 --- a/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 +++ b/test/powershell/engine/Help/UpdatableHelpSystem.Tests.ps1 @@ -215,7 +215,7 @@ function RunUpdateHelpTests It ('Validate Update-Help for module ''{0}'' in {1}' -F $moduleName, [PSCustomObject] $updateScope) -Skip:(!(Test-CanWriteToPsHome) -and $userscope -eq $false) { - if ($markAsPending) { + if ($markAsPending -or ($IsLinux -and $moduleName -eq "PackageManagement")) { Set-ItResult -Pending -Because "Update-Help from the web has intermittent connectivity issues. See issues #2807 and #6541." return } @@ -333,9 +333,7 @@ Describe "Validate Update-Help from the Web for one PowerShell module." -Tags @( $ProgressPreference = $SavedProgressPreference } - ## Update-Help from the web has intermittent connectivity issues that cause CI failures. - ## Tests are marked as Pending to unblock work. See issues #2807 and #6541. - RunUpdateHelpTests -Tag "CI" -MarkAsPending + RunUpdateHelpTests -Tag "CI" } Describe "Validate Update-Help from the Web for one PowerShell module for user scope." -Tags @('CI', 'RequireAdminOnWindows', 'RequireSudoOnUnix') { @@ -347,9 +345,7 @@ Describe "Validate Update-Help from the Web for one PowerShell module for user s $ProgressPreference = $SavedProgressPreference } - ## Update-Help from the web has intermittent connectivity issues that cause CI failures. - ## Tests are marked as Pending to unblock work. See issues #2807 and #6541. - RunUpdateHelpTests -Tag "CI" -UserScope -MarkAsPending + RunUpdateHelpTests -Tag "CI" -UserScope } Describe "Validate Update-Help from the Web for all PowerShell modules." -Tags @('Feature', 'RequireAdminOnWindows', 'RequireSudoOnUnix') { From f3b5603b5dfcb0cc209e9e6c2a8e9a0fdaf16236 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:16:54 -0800 Subject: [PATCH 080/112] Bump github/codeql-action from 4.32.2 to 4.32.3 (#26839) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index c5a0b9ff98..96fb89287d 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3.29.5 + uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3.29.5 + uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index a5d0509155..422381cf50 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v3.29.5 + uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5 with: sarif_file: results.sarif From 9d68a05ac3f68f07ea6daed31cb95bf3039a4fb8 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Thu, 19 Feb 2026 04:04:45 +0900 Subject: [PATCH 081/112] Add comprehensive `PSCustomObject` tests for `ConvertTo-Json` (#26743) --- .../ConvertTo-Json.Tests.ps1 | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 4fe4b0d59b..8be8631ee6 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -1043,6 +1043,250 @@ Describe 'ConvertTo-Json' -tags "CI" { #endregion Comprehensive Array and Dictionary Tests (Phase 2) + + #region Comprehensive PSCustomObject Tests (Phase 3) + # Test coverage for ConvertTo-Json PSCustomObject serialization + # Covers: Pipeline vs InputObject, ETS vs no ETS, nested structures + + Context 'PSCustomObject basic serialization' { + It 'Should serialize PSCustomObject with single property via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Name = 'Test' } + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly '{"Name":"Test"}' + $jsonInputObject | Should -BeExactly '{"Name":"Test"}' + } + + It 'Should serialize PSCustomObject with multiple properties via Pipeline and InputObject' { + $obj = [PSCustomObject][ordered]@{ + Name = 'Test' + Value = 42 + Active = $true + } + $expected = '{"Name":"Test","Value":42,"Active":true}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should preserve property order in PSCustomObject via Pipeline and InputObject' { + $obj = [PSCustomObject][ordered]@{ + Zebra = 1 + Alpha = 2 + Middle = 3 + } + $expected = '{"Zebra":1,"Alpha":2,"Middle":3}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with null property via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ NullProp = $null } + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly '{"NullProp":null}' + $jsonInputObject | Should -BeExactly '{"NullProp":null}' + } + } + + Context 'PSCustomObject with various property types' { + It 'Should serialize PSCustomObject with scalar properties via Pipeline and InputObject' { + $obj = [PSCustomObject][ordered]@{ + IntVal = 42 + DoubleVal = 3.14 + StringVal = 'hello' + BoolVal = $true + } + $expected = '{"IntVal":42,"DoubleVal":3.14,"StringVal":"hello","BoolVal":true}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with DateTime property via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Date = [DateTime]::new(2024, 6, 15, 10, 30, 0, [DateTimeKind]::Utc) + } + $expected = '{"Date":"2024-06-15T10:30:00Z"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with Guid property via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Id = [Guid]'12345678-1234-1234-1234-123456789abc' + } + $expected = '{"Id":"12345678-1234-1234-1234-123456789abc"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with enum property via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Day = [DayOfWeek]::Monday } + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly '{"Day":1}' + $jsonInputObject | Should -BeExactly '{"Day":1}' + } + + It 'Should serialize PSCustomObject with enum as string via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Day = [DayOfWeek]::Monday } + $jsonPipeline = $obj | ConvertTo-Json -Compress -EnumsAsStrings + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -EnumsAsStrings + $jsonPipeline | Should -BeExactly '{"Day":"Monday"}' + $jsonInputObject | Should -BeExactly '{"Day":"Monday"}' + } + + It 'Should serialize PSCustomObject with array property via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Numbers = @(1, 2, 3) } + $expected = '{"Numbers":[1,2,3]}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with hashtable property via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Config = @{ Key = 'Value' } } + $expected = '{"Config":{"Key":"Value"}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Nested PSCustomObject' { + It 'Should serialize nested PSCustomObject via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Outer = [PSCustomObject]@{ + Inner = 'value' + } + } + $expected = '{"Outer":{"Inner":"value"}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize deeply nested PSCustomObject via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Level1 = [PSCustomObject]@{ + Level2 = [PSCustomObject]@{ + Level3 = 'deep' + } + } + } + $expected = '{"Level1":{"Level2":{"Level3":"deep"}}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize nested PSCustomObject with Depth limit via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ + Level1 = [PSCustomObject]@{ + Level2 = [PSCustomObject]@{ + Level3 = 'deep' + } + } + } + $expected = '{"Level1":{"Level2":"@{Level3=deep}"}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 1 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 1 + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize PSCustomObject with mixed nested types via Pipeline and InputObject' { + $obj = [PSCustomObject][ordered]@{ + Child = [PSCustomObject]@{ Name = 'child' } + Items = @(1, 2, 3) + Config = @{ Key = 'Value' } + } + $expected = '{"Child":{"Name":"child"},"Items":[1,2,3],"Config":{"Key":"Value"}}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'PSCustomObject ETS properties' { + It 'Should include NoteProperty on PSCustomObject via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Original = 'value' } + $obj | Add-Member -MemberType NoteProperty -Name Added -Value 'added' + $expected = '{"Original":"value","Added":"added"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should include ScriptProperty on PSCustomObject via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Value = 10 } + $obj | Add-Member -MemberType ScriptProperty -Name Doubled -Value { $this.Value * 2 } + $expected = '{"Value":10,"Doubled":20}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should include multiple ETS properties on PSCustomObject via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Base = 'base' } + $obj | Add-Member -MemberType NoteProperty -Name Note1 -Value 'note1' + $obj | Add-Member -MemberType NoteProperty -Name Note2 -Value 'note2' + $expected = '{"Base":"base","Note1":"note1","Note2":"note2"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Array of PSCustomObject' { + It 'Should serialize array of PSCustomObject via Pipeline and InputObject' { + $arr = @( + [PSCustomObject][ordered]@{ Id = 1; Name = 'First' } + [PSCustomObject][ordered]@{ Id = 2; Name = 'Second' } + ) + $expected = '[{"Id":1,"Name":"First"},{"Id":2,"Name":"Second"}]' + $jsonPipeline = $arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize single PSCustomObject without array wrapper via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Id = 1 } + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly '{"Id":1}' + $jsonInputObject | Should -BeExactly '{"Id":1}' + } + + It 'Should serialize single PSCustomObject with -AsArray via Pipeline and InputObject' { + $obj = [PSCustomObject]@{ Id = 1 } + $jsonPipeline = $obj | ConvertTo-Json -Compress -AsArray + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -AsArray + $jsonPipeline | Should -BeExactly '[{"Id":1}]' + $jsonInputObject | Should -BeExactly '[{"Id":1}]' + } + } + + #endregion Comprehensive PSCustomObject Tests (Phase 3) + #region Comprehensive Depth Truncation and Multilevel Composition Tests (Phase 4) # Test coverage for ConvertTo-Json depth truncation and complex nested structures # Covers: -Depth parameter behavior, multilevel type compositions From 1965e059bc836046ac0759640f64f9311402c545 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Thu, 19 Feb 2026 05:39:53 +1000 Subject: [PATCH 082/112] Fix up default value for parameters with the `in` modifier (#26785) --- .../engine/runtime/Binding/Binders.cs | 7 +++++++ .../engine/Basic/CLRBinding.Tests.ps1 | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs index 5913c98829..027c488638 100644 --- a/src/System.Management.Automation/engine/runtime/Binding/Binders.cs +++ b/src/System.Management.Automation/engine/runtime/Binding/Binders.cs @@ -7201,6 +7201,13 @@ namespace System.Management.Automation.Language var argValue = parameters[i].DefaultValue; if (argValue == null) { + if (parameterType.IsByRef) + { + // When the default value is null for a ByRef parameter (e.g. an optional `in` parameter + // using `default`), expression trees cannot create Expression.Default for the T& type. + // In that case we switch to the element type and use Default(TElement) instead. + parameterType = parameterType.GetElementType(); + } argExprs[i] = Expression.Default(parameterType); } else if (!parameters[i].HasDefaultValue && parameterType != typeof(object) && argValue == Type.Missing) diff --git a/test/powershell/engine/Basic/CLRBinding.Tests.ps1 b/test/powershell/engine/Basic/CLRBinding.Tests.ps1 index 98a05d8518..cb23fa168d 100644 --- a/test/powershell/engine/Basic/CLRBinding.Tests.ps1 +++ b/test/powershell/engine/Basic/CLRBinding.Tests.ps1 @@ -27,6 +27,12 @@ public class TestClass public static string StaticWithOptionalExpected() => StaticWithOptional(); public static string StaticWithOptional([Optional] string value) => value; + public static int PrimitiveTypeWithInDefault(in int value = default) => value; + + public static Guid ValueTypeWithInDefault(in Guid value = default) => value; + + public static string RefTypeWithInDefault(in string value = default) => value; + public object InstanceWithDefaultExpected() => InstanceWithDefault(); public object InstanceWithDefault(object value = null) => value; @@ -101,6 +107,21 @@ public class TestClassCstorWithOptional $actual | Should -Be $expected } + It "Binds to static method with primitive type with in modifier and default argument" { + $actual = [CLRBindingTests.TestClass]::PrimitiveTypeWithInDefault() + $actual | Should -Be 0 + } + + It "Binds to static method with value type with in modifier and default argument" { + $actual = [CLRBindingTests.TestClass]::ValueTypeWithInDefault() + $actual | Should -Be ([Guid]::Empty) + } + + It "Binds to static method with ref type with in modifier and default argument" { + $actual = [CLRBindingTests.TestClass]::RefTypeWithInDefault() + $null -eq $actual | Should -BeTrue + } + It "Binds to instance method with default argument" { $c = [CLRBindingTests.TestClass]::new() From 212c5f97e3bdb0ec175d407b5d471b4af3a3c45b Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 18 Feb 2026 16:35:10 -0800 Subject: [PATCH 083/112] Update to .NET 11 SDK and update dependencies (#26783) --- .pipelines/templates/nupkg.yml | 12 +++---- .pipelines/templates/windows-hosted-build.yml | 8 ++--- PowerShell.Common.props | 2 +- build.psm1 | 6 ++-- docs/building/linux.md | 2 +- global.json | 2 +- .../PowerShell.Windows.x64.csproj | 2 +- ...oft.PowerShell.Commands.Diagnostics.csproj | 2 +- ...soft.PowerShell.Commands.Management.csproj | 2 +- ...crosoft.PowerShell.Commands.Utility.csproj | 2 +- ...crosoft.PowerShell.CoreCLR.Eventing.csproj | 2 +- .../Microsoft.PowerShell.SDK.csproj | 10 +++--- .../Microsoft.WSMan.Management.csproj | 2 +- src/Modules/PSGalleryModules.csproj | 4 +-- src/ResGen/ResGen.csproj | 2 +- .../System.Management.Automation.csproj | 12 +++---- .../remoting/common/RemoteSessionNamedPipe.cs | 1 - src/TypeCatalogGen/TypeCatalogGen.csproj | 2 +- src/powershell/Program.cs | 2 +- test/Test.Common.props | 2 +- test/tools/NamedPipeConnection/build.ps1 | 4 +-- ...soft.PowerShell.NamedPipeConnection.csproj | 2 +- test/tools/OpenCover/OpenCover.psm1 | 2 +- test/tools/TestService/TestService.csproj | 2 +- test/tools/WebListener/WebListener.csproj | 2 +- tools/findMissingNotices.ps1 | 4 +-- tools/packaging/boms/windows.json | 35 +++++++++++-------- tools/packaging/packaging.psm1 | 10 +++--- tools/packaging/packaging.strings.psd1 | 2 +- ...crosoft.PowerShell.Commands.Utility.csproj | 2 +- .../Microsoft.PowerShell.ConsoleHost.csproj | 2 +- .../System.Management.Automation.csproj | 2 +- 32 files changed, 76 insertions(+), 72 deletions(-) diff --git a/.pipelines/templates/nupkg.yml b/.pipelines/templates/nupkg.yml index 3558c94940..c296aadc24 100644 --- a/.pipelines/templates/nupkg.yml +++ b/.pipelines/templates/nupkg.yml @@ -117,13 +117,13 @@ jobs: Start-PSBuild -Clean -Runtime linux-x64 -Configuration Release -ReleaseTag $(ReleaseTagVar) $sharedModules | Foreach-Object { - $refFile = Get-ChildItem -Path "$(PowerShellRoot)\src\$_\obj\Release\net10.0\refint\$_.dll" + $refFile = Get-ChildItem -Path "$(PowerShellRoot)\src\$_\obj\Release\net11.0\refint\$_.dll" Write-Verbose -Verbose "RefAssembly: $refFile" Copy-Item -Path $refFile -Destination "$refAssemblyFolder\$_.dll" -Verbose - $refDoc = "$(PowerShellRoot)\src\$_\bin\Release\net10.0\$_.xml" + $refDoc = "$(PowerShellRoot)\src\$_\bin\Release\net11.0\$_.xml" if (-not (Test-Path $refDoc)) { Write-Warning "$refDoc not found" - Get-ChildItem -Path "$(PowerShellRoot)\src\$_\bin\Release\net10.0\" | Out-String | Write-Verbose -Verbose + Get-ChildItem -Path "$(PowerShellRoot)\src\$_\bin\Release\net11.0\" | Out-String | Write-Verbose -Verbose } else { Copy-Item -Path $refDoc -Destination "$refAssemblyFolder\$_.xml" -Verbose @@ -133,13 +133,13 @@ jobs: Start-PSBuild -Clean -Runtime win7-x64 -Configuration Release -ReleaseTag $(ReleaseTagVar) $winOnlyModules | Foreach-Object { - $refFile = Get-ChildItem -Path "$(PowerShellRoot)\src\$_\obj\Release\net10.0\refint\*.dll" + $refFile = Get-ChildItem -Path "$(PowerShellRoot)\src\$_\obj\Release\net11.0\refint\*.dll" Write-Verbose -Verbose 'RefAssembly: $refFile' Copy-Item -Path $refFile -Destination "$refAssemblyFolder\$_.dll" -Verbose - $refDoc = "$(PowerShellRoot)\src\$_\bin\Release\net10.0\$_.xml" + $refDoc = "$(PowerShellRoot)\src\$_\bin\Release\net11.0\$_.xml" if (-not (Test-Path $refDoc)) { Write-Warning "$refDoc not found" - Get-ChildItem -Path "$(PowerShellRoot)\src\$_\bin\Release\net10.0" | Out-String | Write-Verbose -Verbose + Get-ChildItem -Path "$(PowerShellRoot)\src\$_\bin\Release\net11.0" | Out-String | Write-Verbose -Verbose } else { Copy-Item -Path $refDoc -Destination "$refAssemblyFolder\$_.xml" -Verbose diff --git a/.pipelines/templates/windows-hosted-build.yml b/.pipelines/templates/windows-hosted-build.yml index 83c6b32cdf..a2933e9081 100644 --- a/.pipelines/templates/windows-hosted-build.yml +++ b/.pipelines/templates/windows-hosted-build.yml @@ -274,7 +274,7 @@ jobs: ) $sourceModulePath = Join-Path '$(GlobalToolArtifactPath)' 'publish' 'PowerShell.Windows.x64' 'release' 'Modules' - $destModulesPath = Join-Path "$outputPath" 'temp' 'tools' 'net10.0' 'any' 'modules' + $destModulesPath = Join-Path "$outputPath" 'temp' 'tools' 'net11.0' 'any' 'modules' $modulesToCopy | ForEach-Object { $modulePath = Join-Path $sourceModulePath $_ @@ -282,7 +282,7 @@ jobs: } # Copy ref assemblies - Copy-Item '$(Pipeline.Workspace)/Symbols_$(Architecture)/ref' "$outputPath\temp\tools\net10.0\any\ref" -Recurse -Force + Copy-Item '$(Pipeline.Workspace)/Symbols_$(Architecture)/ref' "$outputPath\temp\tools\net11.0\any\ref" -Recurse -Force $contentPath = Join-Path "$outputPath\temp" 'content' $contentFilesPath = Join-Path "$outputPath\temp" 'contentFiles' @@ -290,14 +290,14 @@ jobs: Remove-Item -Path $contentPath,$contentFilesPath -Recurse -Force # remove PDBs to reduce the size of the nupkg - Remove-Item -Path "$outputPath\temp\tools\net10.0\any\*.pdb" -Recurse -Force + Remove-Item -Path "$outputPath\temp\tools\net11.0\any\*.pdb" -Recurse -Force # create powershell.config.json $config = [ordered]@{} $config.Add("Microsoft.PowerShell:ExecutionPolicy", "RemoteSigned") $config.Add("WindowsPowerShellCompatibilityModuleDenyList", @("PSScheduledJob", "BestPractices", "UpdateServices")) - $configPublishPath = Join-Path "$outputPath" 'temp' 'tools' 'net10.0' 'any' "powershell.config.json" + $configPublishPath = Join-Path "$outputPath" 'temp' 'tools' 'net11.0' 'any' "powershell.config.json" Set-Content -Path $configPublishPath -Value ($config | ConvertTo-Json) -Force -ErrorAction Stop Compress-Archive -Path "$outputPath\temp\*" -DestinationPath "$outputPath\$nupkgName" -Force diff --git a/PowerShell.Common.props b/PowerShell.Common.props index dfc16f830d..df13f12536 100644 --- a/PowerShell.Common.props +++ b/PowerShell.Common.props @@ -144,7 +144,7 @@ (c) Microsoft Corporation. PowerShell 7 - net10.0 + net11.0 13.0 true diff --git a/build.psm1 b/build.psm1 index bf175c5f7c..ee50c9cde5 100644 --- a/build.psm1 +++ b/build.psm1 @@ -1010,8 +1010,8 @@ function New-PSOptions { [ValidateSet('Debug', 'Release', 'CodeCoverage', 'StaticAnalysis', '')] [string]$Configuration, - [ValidateSet("net10.0")] - [string]$Framework = "net10.0", + [ValidateSet("net11.0")] + [string]$Framework = "net11.0", # These are duplicated from Start-PSBuild # We do not use ValidateScript since we want tab completion @@ -4021,7 +4021,7 @@ function Clear-NativeDependencies $filesToDeleteWinDesktop = @() $deps = Get-Content "$PublishFolder/pwsh.deps.json" -Raw | ConvertFrom-Json -Depth 20 - $targetRuntime = ".NETCoreApp,Version=v10.0/$($script:Options.Runtime)" + $targetRuntime = ".NETCoreApp,Version=v11.0/$($script:Options.Runtime)" $runtimePackNetCore = $deps.targets.${targetRuntime}.PSObject.Properties.Name -like 'runtimepack.Microsoft.NETCore.App.Runtime*' $runtimePackWinDesktop = $deps.targets.${targetRuntime}.PSObject.Properties.Name -like 'runtimepack.Microsoft.WindowsDesktop.App.Runtime*' diff --git a/docs/building/linux.md b/docs/building/linux.md index 6ccf12073e..55d96e4c21 100644 --- a/docs/building/linux.md +++ b/docs/building/linux.md @@ -69,7 +69,7 @@ Start-PSBuild -UseNuGetOrg Congratulations! If everything went right, PowerShell is now built. The `Start-PSBuild` script will output the location of the executable: -`./src/powershell-unix/bin/Debug/net10.0/linux-x64/publish/pwsh`. +`./src/powershell-unix/bin/Debug/net11.0/linux-x64/publish/pwsh`. You should now be running the PowerShell Core that you just built, if you run the above executable. You can run our cross-platform Pester tests with `Start-PSPester -UseNuGetOrg`, and our xUnit tests with `Start-PSxUnit`. diff --git a/global.json b/global.json index c2af57a3fe..2fe6c88b1f 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "10.0.102" + "version": "11.0.100-preview.1.26104.118" } } diff --git a/src/GlobalTools/PowerShell.Windows.x64/PowerShell.Windows.x64.csproj b/src/GlobalTools/PowerShell.Windows.x64/PowerShell.Windows.x64.csproj index 49d607ebfe..8449c58ebb 100644 --- a/src/GlobalTools/PowerShell.Windows.x64/PowerShell.Windows.x64.csproj +++ b/src/GlobalTools/PowerShell.Windows.x64/PowerShell.Windows.x64.csproj @@ -2,7 +2,7 @@ Exe - net10.0 + net11.0 enable enable true diff --git a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj index 32e0d32988..ab9bd21035 100644 --- a/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj +++ b/src/Microsoft.PowerShell.Commands.Diagnostics/Microsoft.PowerShell.Commands.Diagnostics.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj index 7d9d61ede9..a8c2c34aca 100644 --- a/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj +++ b/src/Microsoft.PowerShell.Commands.Management/Microsoft.PowerShell.Commands.Management.csproj @@ -47,7 +47,7 @@ - + diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index eed0722b4d..56fa91bcf6 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj index 78232a2f1a..6df470621f 100644 --- a/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj +++ b/src/Microsoft.PowerShell.CoreCLR.Eventing/Microsoft.PowerShell.CoreCLR.Eventing.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj index 0f827aab55..daa29bd01f 100644 --- a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj +++ b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj @@ -16,19 +16,19 @@ - - + + - - + + - + diff --git a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj index da5ea7ddd0..e3bb66c87a 100644 --- a/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj +++ b/src/Microsoft.WSMan.Management/Microsoft.WSMan.Management.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Modules/PSGalleryModules.csproj b/src/Modules/PSGalleryModules.csproj index 7008fe6a08..9136df5c7b 100644 --- a/src/Modules/PSGalleryModules.csproj +++ b/src/Modules/PSGalleryModules.csproj @@ -5,7 +5,7 @@ Microsoft Corporation (c) Microsoft Corporation. - net10.0 + net11.0 true @@ -16,7 +16,7 @@ - + diff --git a/src/ResGen/ResGen.csproj b/src/ResGen/ResGen.csproj index 7fcf1ff3f3..954038cfa5 100644 --- a/src/ResGen/ResGen.csproj +++ b/src/ResGen/ResGen.csproj @@ -2,7 +2,7 @@ Generates C# typed bindings for .resx files - net10.0 + net11.0 resgen Exe true diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index 8c77c770a8..d8a66b2ab7 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -32,12 +32,12 @@ - - - - - - + + + + + + diff --git a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs index e21608a378..fc5226a007 100644 --- a/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs +++ b/src/System.Management.Automation/engine/remoting/common/RemoteSessionNamedPipe.cs @@ -1302,7 +1302,6 @@ namespace System.Management.Automation.Remoting return new NamedPipeClientStream( PipeDirection.InOut, isAsync: true, - isConnected: true, pipeHandle); } catch (Exception) diff --git a/src/TypeCatalogGen/TypeCatalogGen.csproj b/src/TypeCatalogGen/TypeCatalogGen.csproj index ffc3ff9998..83b21e178f 100644 --- a/src/TypeCatalogGen/TypeCatalogGen.csproj +++ b/src/TypeCatalogGen/TypeCatalogGen.csproj @@ -2,7 +2,7 @@ Generates CorePsTypeCatalog.cs given powershell.inc - net10.0 + net11.0 true TypeCatalogGen Exe diff --git a/src/powershell/Program.cs b/src/powershell/Program.cs index aa79bb8cf0..1de961674d 100644 --- a/src/powershell/Program.cs +++ b/src/powershell/Program.cs @@ -152,7 +152,7 @@ namespace Microsoft.PowerShell IntPtr executablePathPtr = IntPtr.Zero; try { - mib = [MACOS_CTL_KERN, MACOS_KERN_PROCARGS2, pid]; + mib = new int[] { MACOS_CTL_KERN, MACOS_KERN_PROCARGS2, pid }; unsafe { diff --git a/test/Test.Common.props b/test/Test.Common.props index 3fafbbf8f8..be70bda4bc 100644 --- a/test/Test.Common.props +++ b/test/Test.Common.props @@ -6,7 +6,7 @@ Microsoft Corporation (c) Microsoft Corporation. - net10.0 + net11.0 13.0 true diff --git a/test/tools/NamedPipeConnection/build.ps1 b/test/tools/NamedPipeConnection/build.ps1 index a0978c4cb3..3d92df01fc 100644 --- a/test/tools/NamedPipeConnection/build.ps1 +++ b/test/tools/NamedPipeConnection/build.ps1 @@ -36,8 +36,8 @@ param ( [ValidateSet("Debug", "Release")] [string] $BuildConfiguration = "Debug", - [ValidateSet("net10.0")] - [string] $BuildFramework = "net10.0" + [ValidateSet("net11.0")] + [string] $BuildFramework = "net11.0" ) $script:ModuleName = 'Microsoft.PowerShell.NamedPipeConnection' diff --git a/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj b/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj index 89147481bc..e004a57367 100644 --- a/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj +++ b/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj @@ -8,7 +8,7 @@ 1.0.0.0 1.0.0 1.0.0 - net10.0 + net11.0 true 13.0 diff --git a/test/tools/OpenCover/OpenCover.psm1 b/test/tools/OpenCover/OpenCover.psm1 index 9e7adb640c..e8848a1508 100644 --- a/test/tools/OpenCover/OpenCover.psm1 +++ b/test/tools/OpenCover/OpenCover.psm1 @@ -624,7 +624,7 @@ function Invoke-OpenCover [parameter()]$OutputLog = "$HOME/Documents/OpenCover.xml", [parameter()]$TestPath = "${script:psRepoPath}/test/powershell", [parameter()]$OpenCoverPath = "$HOME/OpenCover", - [parameter()]$PowerShellExeDirectory = "${script:psRepoPath}/src/powershell-win-core/bin/CodeCoverage/net10.0/win7-x64/publish", + [parameter()]$PowerShellExeDirectory = "${script:psRepoPath}/src/powershell-win-core/bin/CodeCoverage/net11.0/win7-x64/publish", [parameter()]$PesterLogElevated = "$HOME/Documents/TestResultsElevated.xml", [parameter()]$PesterLogUnelevated = "$HOME/Documents/TestResultsUnelevated.xml", [parameter()]$PesterLogFormat = "NUnitXml", diff --git a/test/tools/TestService/TestService.csproj b/test/tools/TestService/TestService.csproj index af62ecaaca..5d5f1a9e4f 100644 --- a/test/tools/TestService/TestService.csproj +++ b/test/tools/TestService/TestService.csproj @@ -15,7 +15,7 @@ - + diff --git a/test/tools/WebListener/WebListener.csproj b/test/tools/WebListener/WebListener.csproj index cbb01a67da..00cd6825df 100644 --- a/test/tools/WebListener/WebListener.csproj +++ b/test/tools/WebListener/WebListener.csproj @@ -7,6 +7,6 @@ - + diff --git a/tools/findMissingNotices.ps1 b/tools/findMissingNotices.ps1 index 42722701d9..1346148bae 100644 --- a/tools/findMissingNotices.ps1 +++ b/tools/findMissingNotices.ps1 @@ -193,8 +193,8 @@ function Get-CGRegistrations { $registrationChanged = $false - $dotnetTargetName = 'net10.0' - $dotnetTargetNameWin7 = 'net10.0-windows8.0' + $dotnetTargetName = 'net11.0' + $dotnetTargetNameWin7 = 'net11.0-windows8.0' $unixProjectName = 'powershell-unix' $windowsProjectName = 'powershell-win-core' $actualRuntime = $Runtime diff --git a/tools/packaging/boms/windows.json b/tools/packaging/boms/windows.json index f811109f81..8248b3d03a 100644 --- a/tools/packaging/boms/windows.json +++ b/tools/packaging/boms/windows.json @@ -1276,11 +1276,6 @@ "FileType": "NonProduct", "Architecture": null }, - { - "Pattern": "mscorrc.dll", - "FileType": "NonProduct", - "Architecture": null - }, { "Pattern": "msquic.dll", "FileType": "NonProduct", @@ -2496,6 +2491,11 @@ "FileType": "NonProduct", "Architecture": null }, + { + "Pattern": "ref\\System.IO.Compression.Zstandard.dll", + "FileType": "NonProduct", + "Architecture": null + }, { "Pattern": "ref\\System.IO.Pipelines.dll", "FileType": "NonProduct", @@ -3206,6 +3206,11 @@ "FileType": "NonProduct", "Architecture": null }, + { + "Pattern": "System.IO.Compression.Zstandard.dll", + "FileType": "NonProduct", + "Architecture": null + }, { "Pattern": "System.IO.dll", "FileType": "NonProduct", @@ -4591,6 +4596,16 @@ "FileType": "Product", "Architecture": null }, + { + "Pattern": "pwsh.profile.dsc.resource.json", + "FileType": "Product", + "Architecture": null + }, + { + "Pattern": "pwsh.profile.resource.ps1", + "FileType": "Product", + "Architecture": null + }, { "Pattern": "RegisterManifest.ps1", "FileType": "Product", @@ -4605,15 +4620,5 @@ "Pattern": "System.Management.Automation.dll", "FileType": "Product", "Architecture": null - }, - { - "Pattern": "pwsh.profile.dsc.resource.json", - "FileType": "Product", - "Architecture": null - }, - { - "Pattern": "pwsh.profile.resource.ps1", - "FileType": "Product", - "Architecture": null } ] diff --git a/tools/packaging/packaging.psm1 b/tools/packaging/packaging.psm1 index cddde65aa9..ca4d5d4671 100644 --- a/tools/packaging/packaging.psm1 +++ b/tools/packaging/packaging.psm1 @@ -18,7 +18,7 @@ $AllDistributions = @() $AllDistributions += $DebianDistributions $AllDistributions += $RedhatDistributions $AllDistributions += 'macOs' -$script:netCoreRuntime = 'net10.0' +$script:netCoreRuntime = 'net11.0' $script:iconFileName = "Powershell_black_64.png" $script:iconPath = Join-Path -path $PSScriptRoot -ChildPath "../../assets/$iconFileName" -Resolve @@ -2323,12 +2323,12 @@ function Get-MacOSPackageIdentifierInfo param( [Parameter(Mandatory)] [string]$Version, - + [switch]$LTS ) - + $IsPreview = Test-IsPreview -Version $Version -IsLTS:$LTS - + # Determine package identifier based on preview status if ($IsPreview) { $PackageIdentifier = 'com.microsoft.powershell-preview' @@ -2336,7 +2336,7 @@ function Get-MacOSPackageIdentifierInfo else { $PackageIdentifier = 'com.microsoft.powershell' } - + return @{ IsPreview = $IsPreview PackageIdentifier = $PackageIdentifier diff --git a/tools/packaging/packaging.strings.psd1 b/tools/packaging/packaging.strings.psd1 index eeb9a86ec1..0bf14ff0db 100644 --- a/tools/packaging/packaging.strings.psd1 +++ b/tools/packaging/packaging.strings.psd1 @@ -166,7 +166,7 @@ open {0} - + diff --git a/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index f358b454ba..6a96c8b117 100644 --- a/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 $(RefAsmVersion) true $(SnkFile) diff --git a/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj b/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj index c0794a6a70..07161449da 100644 --- a/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj +++ b/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 $(RefAsmVersion) true $(SnkFile) diff --git a/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj b/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj index f4626e110f..d633173b9c 100644 --- a/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj +++ b/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj @@ -1,6 +1,6 @@ - net10.0 + net11.0 $(RefAsmVersion) true $(SnkFile) From 2af5ad14a3d6910600bb8e4905a95fcf8aa86c44 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Thu, 19 Feb 2026 15:39:42 +0000 Subject: [PATCH 084/112] Update `LangVersion` to `preview` (#26214) --- PowerShell.Common.props | 2 +- .../WebCmdlet/Common/BasicHtmlWebResponseObject.Common.cs | 4 ++-- .../PSVersionInfoGenerator/PSVersionInfoGenerator.csproj | 2 +- test/Test.Common.props | 2 +- test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj | 2 +- .../src/code/Microsoft.PowerShell.NamedPipeConnection.csproj | 2 +- .../Microsoft.PowerShell.Commands.Utility.csproj | 2 +- .../Microsoft.PowerShell.ConsoleHost.csproj | 2 +- .../System.Management.Automation.csproj | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/PowerShell.Common.props b/PowerShell.Common.props index df13f12536..fa2a5305e3 100644 --- a/PowerShell.Common.props +++ b/PowerShell.Common.props @@ -145,7 +145,7 @@ PowerShell 7 net11.0 - 13.0 + preview true true diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/BasicHtmlWebResponseObject.Common.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/BasicHtmlWebResponseObject.Common.cs index 9bd76f9941..ea650e80e6 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/BasicHtmlWebResponseObject.Common.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/Common/BasicHtmlWebResponseObject.Common.cs @@ -81,9 +81,9 @@ namespace Microsoft.PowerShell.Commands { List parsedFields = new(); MatchCollection fieldMatch = HtmlParser.InputFieldRegex.Matches(Content); - foreach (Match field in fieldMatch) + foreach (Match match in fieldMatch) { - parsedFields.Add(CreateHtmlObject(field.Value, "INPUT")); + parsedFields.Add(CreateHtmlObject(match.Value, "INPUT")); } _inputFields = new WebCmdletElementCollection(parsedFields); diff --git a/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj b/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj index 1099b2a3c1..44d93a5e31 100644 --- a/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj +++ b/src/System.Management.Automation/SourceGenerators/PSVersionInfoGenerator/PSVersionInfoGenerator.csproj @@ -7,7 +7,7 @@ netstandard2.0 - 13.0 + preview true true enable diff --git a/test/Test.Common.props b/test/Test.Common.props index be70bda4bc..8a5522e9ea 100644 --- a/test/Test.Common.props +++ b/test/Test.Common.props @@ -7,7 +7,7 @@ (c) Microsoft Corporation. net11.0 - 13.0 + preview true true diff --git a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj index 5cbc66081a..90143a9f48 100644 --- a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj +++ b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj @@ -3,7 +3,7 @@ Exe $(PERFLAB_TARGET_FRAMEWORKS) net5.0 - 13.0 + preview diff --git a/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj b/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj index e004a57367..ba216dee23 100644 --- a/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj +++ b/test/tools/NamedPipeConnection/src/code/Microsoft.PowerShell.NamedPipeConnection.csproj @@ -10,7 +10,7 @@ 1.0.0 net11.0 true - 13.0 + preview diff --git a/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 6a96c8b117..34ead3a2dc 100644 --- a/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/tools/packaging/projects/reference/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -5,7 +5,7 @@ true $(SnkFile) true - 13.0 + preview diff --git a/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj b/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj index 07161449da..378ca59ff9 100644 --- a/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj +++ b/tools/packaging/projects/reference/Microsoft.PowerShell.ConsoleHost/Microsoft.PowerShell.ConsoleHost.csproj @@ -5,7 +5,7 @@ true $(SnkFile) true - 13.0 + preview diff --git a/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj b/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj index d633173b9c..8f80aa50a3 100644 --- a/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj +++ b/tools/packaging/projects/reference/System.Management.Automation/System.Management.Automation.csproj @@ -5,7 +5,7 @@ true $(SnkFile) true - 13.0 + preview From 2f08eb44e69f0cb5d995dfccb541fd07df215d12 Mon Sep 17 00:00:00 2001 From: Yoshifumi Date: Fri, 20 Feb 2026 04:04:48 +0900 Subject: [PATCH 085/112] Add comprehensive PowerShell class tests for `ConvertTo-Json` (#26769) --- .../ConvertTo-Json.Tests.ps1 | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 index 8be8631ee6..f1ddc43a22 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Json.Tests.ps1 @@ -1738,4 +1738,156 @@ Describe 'ConvertTo-Json' -tags "CI" { } #endregion Comprehensive Depth Truncation and Multilevel Composition Tests (Phase 4) + + #region Comprehensive PowerShell Class Tests (Phase 5) + # Test coverage for ConvertTo-Json PowerShell class serialization + # Covers: Pipeline vs InputObject, ETS vs no ETS, nested structures, inheritance + + Context 'PowerShell class serialization' { + BeforeAll { + class SimpleClass { + [string]$StringVal + [int]$IntVal + [bool]$BoolVal + [double]$DoubleVal + [bigint]$BigIntVal + [guid]$GuidVal + [ipaddress]$IPVal + [object[]]$ArrayVal + [System.Collections.Specialized.OrderedDictionary]$DictVal + hidden [string]$HiddenVal + } + } + + It 'Should serialize PowerShell class with various property types including ETS via Pipeline and InputObject' { + $obj = [SimpleClass]::new() + $obj.StringVal = 'hello' + $obj.IntVal = 42 + $obj.BoolVal = $true + $obj.DoubleVal = 3.14 + $obj.BigIntVal = [bigint]::Parse('99999999999999999999') + $obj.GuidVal = [guid]'12345678-1234-1234-1234-123456789abc' + $obj.IPVal = [ipaddress]::Parse('192.168.1.1') + $obj.ArrayVal = @(1, 'two', $true) + $obj.DictVal = [ordered]@{ Key = 'Value'; Nested = [ordered]@{ Inner = 1 } } + $obj.HiddenVal = 'secret' + $obj | Add-Member -MemberType NoteProperty -Name ETSNote -Value 'note' + $obj | Add-Member -MemberType ScriptProperty -Name ETSScript -Value { $this.StringVal.Length } + $obj.IPVal | Add-Member -MemberType NoteProperty -Name Label -Value 'primary' + $expectedPipeline = '{"StringVal":"hello","IntVal":42,"BoolVal":true,"DoubleVal":3.14,"BigIntVal":99999999999999999999,"GuidVal":"12345678-1234-1234-1234-123456789abc","IPVal":{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952},"ArrayVal":[1,"two",true],"DictVal":{"Key":"Value","Nested":{"Inner":1}},"HiddenVal":"secret","ETSNote":"note","ETSScript":5}' + $expectedInputObject = '{"StringVal":"hello","IntVal":42,"BoolVal":true,"DoubleVal":3.14,"BigIntVal":99999999999999999999,"GuidVal":"12345678-1234-1234-1234-123456789abc","IPVal":{"AddressFamily":2,"ScopeId":null,"IsIPv6Multicast":false,"IsIPv6LinkLocal":false,"IsIPv6SiteLocal":false,"IsIPv6Teredo":false,"IsIPv6UniqueLocal":false,"IsIPv4MappedToIPv6":false,"Address":16885952},"ArrayVal":[1,"two",true],"DictVal":{"Key":"Value","Nested":{"Inner":1}},"HiddenVal":"secret"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress -Depth 3 + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress -Depth 3 + $jsonPipeline | Should -BeExactly $expectedPipeline + $jsonInputObject | Should -BeExactly $expectedInputObject + } + + It 'Should serialize PowerShell class with default values via Pipeline and InputObject' { + $obj = [SimpleClass]::new() + $expected = '{"StringVal":null,"IntVal":0,"BoolVal":false,"DoubleVal":0.0,"BigIntVal":0,"GuidVal":"00000000-0000-0000-0000-000000000000","IPVal":null,"ArrayVal":null,"DictVal":null,"HiddenVal":null}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'Nested PowerShell class' { + BeforeAll { + class InnerClass { + [string]$Inner + } + + class OuterClass { + [string]$Outer + [InnerClass]$Child + } + + class DeepClass { + [string]$Name + [OuterClass]$Nested + } + } + + It 'Should serialize deeply nested PowerShell class via Pipeline and InputObject' { + $inner = [InnerClass]@{ Inner = 'deep' } + $outer = [OuterClass]@{ Outer = 'middle'; Child = $inner } + $deep = [DeepClass]@{ Name = 'top'; Nested = $outer } + $expected = '{"Name":"top","Nested":{"Outer":"middle","Child":{"Inner":"deep"}}}' + $jsonPipeline = $deep | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $deep -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize nested PowerShell class with null child via Pipeline and InputObject' { + $outer = [OuterClass]@{ Outer = 'outer value'; Child = $null } + $expected = '{"Outer":"outer value","Child":null}' + $jsonPipeline = $outer | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $outer -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + Context 'PowerShell class inheritance' { + BeforeAll { + class BaseClass { + [string]$BaseProp + } + + class ChildClass : BaseClass { + [string]$ChildProp + } + + class GrandChildClass : ChildClass { + [string]$GrandChildProp + } + } + + It 'Should serialize derived class with base properties via Pipeline and InputObject' { + $obj = [ChildClass]@{ BaseProp = 'base'; ChildProp = 'child' } + $expected = '{"ChildProp":"child","BaseProp":"base"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + It 'Should serialize multi-level inherited class via Pipeline and InputObject' { + $obj = [GrandChildClass]@{ + BaseProp = 'base' + ChildProp = 'child' + GrandChildProp = 'grandchild' + } + $expected = '{"GrandChildProp":"grandchild","ChildProp":"child","BaseProp":"base"}' + $jsonPipeline = $obj | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $obj -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + + } + + Context 'Mixed PSCustomObject and PowerShell class' { + BeforeAll { + class MixedClass { + [string]$ClassName + } + } + + It 'Should serialize array with mixed types via Pipeline and InputObject' { + $classObj = [MixedClass]@{ ClassName = 'class' } + $customObj = [PSCustomObject]@{ CustomName = 'custom' } + $arr = @($classObj, $customObj) + $expected = '[{"ClassName":"class"},{"CustomName":"custom"}]' + $jsonPipeline = $arr | ConvertTo-Json -Compress + $jsonInputObject = ConvertTo-Json -InputObject $arr -Compress + $jsonPipeline | Should -BeExactly $expected + $jsonInputObject | Should -BeExactly $expected + } + } + + #endregion Comprehensive PowerShell Class Tests (Phase 5) + } From cf40400cb9b250a65a65fc18064884cb73be10d1 Mon Sep 17 00:00:00 2001 From: kasperk81 <83082615+kasperk81@users.noreply.github.com> Date: Thu, 19 Feb 2026 21:28:04 +0200 Subject: [PATCH 086/112] Fetch latest ICU release version dynamically (#26827) --- tools/packaging/packaging.psm1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tools/packaging/packaging.psm1 b/tools/packaging/packaging.psm1 index ca4d5d4671..f400e060b2 100644 --- a/tools/packaging/packaging.psm1 +++ b/tools/packaging/packaging.psm1 @@ -2148,7 +2148,7 @@ function Get-PackageDependencies # than the build version and we know that older versions just works. # $MinICUVersion = 60 # runtime minimum supported - $BuildICUVersion = 76 # current build version + $BuildICUVersion = Get-IcuLatestRelease $MaxICUVersion = $BuildICUVersion + 30 # headroom if ($Distribution -eq 'deb') { @@ -5808,3 +5808,15 @@ function Test-IsProductFile { return $false } + +# Get major version from latest ICU release (latest: stable version) +function Get-IcuLatestRelease { + $response = Invoke-WebRequest -Uri "https://github.com/unicode-org/icu/releases/latest" + $tagUrl = ($response.Links | Where-Object href -like "*releases/tag/release-*")[0].href + + if ($tagUrl -match 'release-(\d+)\.') { + return [int]$Matches[1] + } + + throw "Unable to determine the latest ICU release version." +} From 6a91b1f4c7c2c56c555930145f7e72091c3bcc87 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Thu, 19 Feb 2026 20:56:46 -0500 Subject: [PATCH 087/112] Bring the `v7.6.0-rc.1` changelog to master (#26857) --- CHANGELOG/preview.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/CHANGELOG/preview.md b/CHANGELOG/preview.md index f1c5f56628..851640f8cc 100644 --- a/CHANGELOG/preview.md +++ b/CHANGELOG/preview.md @@ -1,5 +1,39 @@ # Preview Changelog +## [7.6.0-rc.1] - 2026-02-19 + +### Tests + +- Fix `$PSDefaultParameterValues` leak causing tests to skip unexpectedly (#26705) + +### Build and Packaging Improvements + +
+ + + +

Expand to see details.

+ +
+ +
    +
  • Update branch for release (#26779)
  • +
  • Update Microsoft.PowerShell.PSResourceGet version to 1.2.0-rc3 (#26767)
  • +
  • Update Microsoft.PowerShell.Native package version (#26748)
  • +
  • Move PowerShell build to depend on .NET SDK 10.0.102 (#26717)
  • +
  • Fix buildinfo.json uploading for preview, LTS, and stable releases (#26715)
  • +
  • Fix macOS preview package identifier detection to use version string (#26709)
  • +
  • Update metadata.json to update the Latest attribute with a better name (#26708)
  • +
  • Remove unused runCodesignValidationInjection variable from pipeline templates (#26707)
  • +
  • Update Get-ChangeLog to handle backport PRs correctly (#26706)
  • +
  • Bring release changes from the v7.6.0-preview.6 release (#26626)
  • +
  • Fix the DSC test by skipping AfterAll cleanup if the initial setup in BeforeAll failed (#26622)
  • +
+ +
+ +[7.6.0-rc.1]: https://github.com/PowerShell/PowerShell/compare/v7.6.0-preview.6...v7.6.0-rc.1 + ## [7.6.0-preview.6] - 2025-12-11 ### Engine Updates and Fixes From f2c9775d7084192b69f2d5155a33035a44d37596 Mon Sep 17 00:00:00 2001 From: Patrick Meinecke Date: Fri, 20 Feb 2026 12:34:59 -0500 Subject: [PATCH 088/112] Update metadata.json for v7.6.0-rc.1 (#26856) --- tools/metadata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/metadata.json b/tools/metadata.json index e0f4952c58..1bf3e96e39 100644 --- a/tools/metadata.json +++ b/tools/metadata.json @@ -1,10 +1,10 @@ { "StableReleaseTag": "v7.5.4", - "PreviewReleaseTag": "v7.6.0-preview.5", + "PreviewReleaseTag": "v7.6.0-rc.1", "ServicingReleaseTag": "v7.0.13", "ReleaseTag": "v7.5.4", "LTSReleaseTag" : ["v7.4.13"], - "NextReleaseTag": "v7.6.0-preview.6", + "NextReleaseTag": "v7.7.0-preview.1", "LTSRelease": { "PublishToChannels": false, "Package": false }, "StableRelease": { "PublishToChannels": false, "Package": false } } From b58ec3609342ab8a9bdffe65924e57f77dbcf9c7 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 20 Feb 2026 10:29:13 -0800 Subject: [PATCH 089/112] Exclude .exe packages from publishing to GitHub (#26859) --- .pipelines/templates/release-githubNuget.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.pipelines/templates/release-githubNuget.yml b/.pipelines/templates/release-githubNuget.yml index 5f67ce6a9e..206079c555 100644 --- a/.pipelines/templates/release-githubNuget.yml +++ b/.pipelines/templates/release-githubNuget.yml @@ -35,6 +35,14 @@ jobs: targetType: inline script: | $Path = "$(Pipeline.Workspace)/GitHubPackages" + + # The .exe packages are for Windows Update only and should not be uploaded to GitHub release. + $exefiles = Get-ChildItem -Path $Path -Filter *.exe + if ($exefiles) { + Write-Verbose -Verbose "Remove .exe packages:" + $exefiles | Remove-Item -Force -Verbose + } + $OutputPath = Join-Path $Path 'hashes.sha256' $packages = Get-ChildItem -Path $Path -Include * -Recurse -File $checksums = $packages | From c19a440244a5b4b4f63b987fb1a816dec77ed2a5 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 20 Feb 2026 14:05:55 -0800 Subject: [PATCH 090/112] Fix `Import-Module.Tests.ps1` to handle Arm32 platform (#26862) --- test/powershell/Language/Scripting/Requires.Tests.ps1 | 2 +- .../Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/test/powershell/Language/Scripting/Requires.Tests.ps1 b/test/powershell/Language/Scripting/Requires.Tests.ps1 index d4cad910e1..b5cbb39732 100644 --- a/test/powershell/Language/Scripting/Requires.Tests.ps1 +++ b/test/powershell/Language/Scripting/Requires.Tests.ps1 @@ -41,7 +41,7 @@ Describe "Requires tests" -Tags "CI" { BeforeAll { $currentVersion = $PSVersionTable.PSVersion - $powerShellVersions = "1.0", "2.0", "3.0", "4.0", "5.0", "5.1", "6.0", "6.1", "6.2", "7.0", "7.1", "7.2", "7.3", "7.4", "7.5", "7.6" + $powerShellVersions = "1.0", "2.0", "3.0", "4.0", "5.0", "5.1", "6.0", "6.1", "6.2", "7.0", "7.1", "7.2", "7.3", "7.4", "7.5", "7.6", "7.7" $latestVersion = [version]($powerShellVersions | Sort-Object -Descending -Top 1) $nonExistingMinor = "$($latestVersion.Major).$($latestVersion.Minor + 1)" $nonExistingMajor = "$($latestVersion.Major + 1).0" diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 index 6885abe847..19dc80caf2 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 @@ -62,6 +62,7 @@ Describe "Import-Module" -Tags "CI" { 'X86' { 'x86' } 'X64' { 'amd64' } 'Arm64' { 'arm' } + 'Arm' { 'arm' } default { throw "Unknown processor architecture" } } New-ModuleManifest -Path "$TestDrive\TestModule.psd1" -ProcessorArchitecture $currentProcessorArchitecture From 19fa6a9f79eefebaa8c401b759dcdacc3d4cc703 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:40:05 +0000 Subject: [PATCH 091/112] Remove obsolete `CA2006` rule suppression (#25939) --- .../security/CertificateProvider.cs | 1 - .../engine/remoting/fanin/WSManNativeAPI.cs | 9 --------- .../remoting/fanin/WSManPluginFacade.cs | 10 ---------- .../remoting/fanin/WSManTransportManager.cs | 19 +++---------------- .../security/SecuritySupport.cs | 2 -- 5 files changed, 3 insertions(+), 38 deletions(-) diff --git a/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs b/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs index 08ee5a70f5..37c687a777 100644 --- a/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs +++ b/src/Microsoft.PowerShell.Security/security/CertificateProvider.cs @@ -3424,7 +3424,6 @@ namespace Microsoft.PowerShell.Commands return IntPtr.Zero; } #else - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private static IntPtr hWnd = IntPtr.Zero; private static bool firstRun = true; diff --git a/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs b/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs index 289f88c576..d7bce63462 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/WSManNativeAPI.cs @@ -304,7 +304,6 @@ namespace System.Management.Automation.Remoting.Client /// /// Making password secure. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr password; } @@ -626,7 +625,6 @@ namespace System.Management.Automation.Remoting.Client { internal int bufferLength; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr data; } @@ -637,10 +635,8 @@ namespace System.Management.Automation.Remoting.Client { private readonly WSManDataStruct _internalData; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _marshalledObject = IntPtr.Zero; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _marshalledBuffer = IntPtr.Zero; /// @@ -933,7 +929,6 @@ namespace System.Management.Automation.Remoting.Client { internal int streamIDsCount; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr streamIDs; } @@ -1085,7 +1080,6 @@ namespace System.Management.Automation.Remoting.Client /// /// Pointer to an array of WSManOption objects. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr options; internal bool optionsMustUnderstand; @@ -1223,13 +1217,11 @@ namespace System.Management.Automation.Remoting.Client { internal int argsCount; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr args; } private WSManCommandArgSetInternal _internalData; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private MarshalledObject _data; #region Managed to Unmanaged @@ -1733,7 +1725,6 @@ namespace System.Management.Automation.Remoting.Client // GC handle which prevents garbage collector from collecting this delegate. private GCHandle _gcHandle; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private readonly IntPtr _asyncCallback; internal WSManShellAsyncCallback(WSManShellCompletionFunction callback) diff --git a/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs b/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs index 7fbc236a4d..2d99a42cfb 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/WSManPluginFacade.cs @@ -343,61 +343,51 @@ namespace System.Management.Automation.Remoting /// /// WsManPluginShutdownPluginCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginShutdownPluginCallbackNative; /// /// WSManPluginShellCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginShellCallbackNative; /// /// WSManPluginReleaseShellContextCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginReleaseShellContextCallbackNative; /// /// WSManPluginCommandCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginCommandCallbackNative; /// /// WSManPluginReleaseCommandContextCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginReleaseCommandContextCallbackNative; /// /// WSManPluginSendCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginSendCallbackNative; /// /// WSManPluginReceiveCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginReceiveCallbackNative; /// /// WSManPluginSignalCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginSignalCallbackNative; /// /// WSManPluginConnectCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginConnectCallbackNative; /// /// WSManPluginCommandCallbackNative. /// - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] internal IntPtr wsManPluginShutdownCallbackNative; } } diff --git a/src/System.Management.Automation/engine/remoting/fanin/WSManTransportManager.cs b/src/System.Management.Automation/engine/remoting/fanin/WSManTransportManager.cs index 58b9cfe098..d4ee779b5a 100644 --- a/src/System.Management.Automation/engine/remoting/fanin/WSManTransportManager.cs +++ b/src/System.Management.Automation/engine/remoting/fanin/WSManTransportManager.cs @@ -317,18 +317,13 @@ namespace System.Management.Automation.Remoting.Client #endregion #region Private Data + // operation handles are owned by WSMan - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _wsManSessionHandle; - - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _wsManShellOperationHandle; - - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _wsManReceiveOperationHandle; - - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _wsManSendOperationHandle; + // this is used with WSMan callbacks to represent a session transport manager. private long _sessionContextID; @@ -2643,7 +2638,6 @@ namespace System.Management.Automation.Remoting.Client /// internal class WSManAPIDataCommon : IDisposable { - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _handle; // if any private WSManNativeApi.WSManStreamIDSet_ManToUn _inputStreamSet; @@ -2791,18 +2785,11 @@ namespace System.Management.Automation.Remoting.Client // operation handles private readonly IntPtr _wsManShellOperationHandle; - - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _wsManCmdOperationHandle; - - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _cmdSignalOperationHandle; - - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _wsManReceiveOperationHandle; - - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private IntPtr _wsManSendOperationHandle; + // this is used with WSMan callbacks to represent a command transport manager. private long _cmdContextId; diff --git a/src/System.Management.Automation/security/SecuritySupport.cs b/src/System.Management.Automation/security/SecuritySupport.cs index 12e03d8517..dc6d048c5b 100644 --- a/src/System.Management.Automation/security/SecuritySupport.cs +++ b/src/System.Management.Automation/security/SecuritySupport.cs @@ -1607,10 +1607,8 @@ namespace System.Management.Automation } } - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private static IntPtr s_amsiContext = IntPtr.Zero; - [SuppressMessage("Microsoft.Reliability", "CA2006:UseSafeHandleToEncapsulateNativeResources")] private static IntPtr s_amsiSession = IntPtr.Zero; private static readonly bool s_amsiInitFailed = false; From cfd59665308d440795c6430d9ae0f5432211e609 Mon Sep 17 00:00:00 2001 From: xtqqczze <45661989+xtqqczze@users.noreply.github.com> Date: Mon, 23 Feb 2026 19:11:12 +0000 Subject: [PATCH 092/112] Enable CA1852: Seal internal types (#25890) --- .globalconfig | 4 ++++ test/tools/TestAlc/init/Init.cs | 2 +- test/tools/TestExe/TestExe.cs | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.globalconfig b/.globalconfig index 21ecbc766a..e0dd4ccb9e 100644 --- a/.globalconfig +++ b/.globalconfig @@ -510,6 +510,10 @@ dotnet_diagnostic.CA1846.severity = warning # https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1847 dotnet_diagnostic.CA1847.severity = warning +# CA1852: Seal internal types +# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852 +dotnet_diagnostic.CA1852.severity = warning + # CA1853: Unnecessary call to 'Dictionary.ContainsKey(key)' # https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1853 dotnet_diagnostic.CA1853.severity = warning diff --git a/test/tools/TestAlc/init/Init.cs b/test/tools/TestAlc/init/Init.cs index 4241e56fa4..33f6635f71 100644 --- a/test/tools/TestAlc/init/Init.cs +++ b/test/tools/TestAlc/init/Init.cs @@ -10,7 +10,7 @@ using System.Runtime.Loader; namespace Test.Isolated.Init { - internal class CustomLoadContext : AssemblyLoadContext + internal sealed class CustomLoadContext : AssemblyLoadContext { private readonly string _dependencyDirPath; diff --git a/test/tools/TestExe/TestExe.cs b/test/tools/TestExe/TestExe.cs index a9b3d83426..9230f9e6bf 100644 --- a/test/tools/TestExe/TestExe.cs +++ b/test/tools/TestExe/TestExe.cs @@ -20,7 +20,7 @@ namespace TestExe System = 2, } - internal class TestExe + internal sealed class TestExe { private static int Main(string[] args) { From 7ca2b72af765aa52ebde99aa1214f6ec29f51868 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 23 Feb 2026 12:33:12 -0800 Subject: [PATCH 093/112] Fix a preview detection test for the packaging script (#26882) --- test/packaging/packaging.tests.ps1 | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/test/packaging/packaging.tests.ps1 b/test/packaging/packaging.tests.ps1 index cc4bbe0d72..a7d322205b 100644 --- a/test/packaging/packaging.tests.ps1 +++ b/test/packaging/packaging.tests.ps1 @@ -46,18 +46,15 @@ Describe "Packaging Module Functions" { $result.PackageIdentifier | Should -Be "com.microsoft.powershell" } - It "Should NOT use package name for preview detection (bug fix verification)" { + It "Should NOT use package name for preview detection (bug fix verification) - " -TestCases @( + @{ Version = "7.6.0-preview.6"; Name = "Preview" } + @{ Version = "7.6.0-rc.1"; Name = "RC" } + ) { # This test verifies the fix for issue #26673 # The bug was using ($Name -like '*-preview') which always returned false # because preview builds use Name="powershell" not "powershell-preview" - - $Version = "7.6.0-preview.6" - $Name = "powershell" # Preview builds use "powershell" not "powershell-preview" - - # The INCORRECT logic (the bug): $Name -like '*-preview' - $incorrectCheck = $Name -like '*-preview' - $incorrectCheck | Should -Be $false -Because "Package name is 'powershell' not 'powershell-preview'" - + param($Version) + # The CORRECT logic (the fix): uses version string $result = Get-MacOSPackageIdentifierInfo -Version $Version -LTS:$false $result.IsPreview | Should -Be $true -Because "Version string correctly identifies preview" From f3eb51b1f40c9b731dfcf46284f51a646973c6c8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 23 Feb 2026 12:35:13 -0800 Subject: [PATCH 094/112] Add GitOps policy to auto-label backport candidates when CL-BuildPackaging is added (#26881) --- ...d.clBuildPackaging.addBackportConsider.yml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/policies/labelAdded.clBuildPackaging.addBackportConsider.yml diff --git a/.github/policies/labelAdded.clBuildPackaging.addBackportConsider.yml b/.github/policies/labelAdded.clBuildPackaging.addBackportConsider.yml new file mode 100644 index 0000000000..78edc18cb1 --- /dev/null +++ b/.github/policies/labelAdded.clBuildPackaging.addBackportConsider.yml @@ -0,0 +1,56 @@ +id: labelAdded.clBuildPackaging.addBackportConsider +name: GitOps.PullRequestIssueManagement +description: Add backport consideration labels when CL-BuildPackaging is added to an open PR targeting master +owner: +resource: repository +disabled: false +where: +configuration: + resourceManagementConfiguration: + eventResponderTasks: + - description: Add BackPort-7.4.x-Consider when CL-BuildPackaging is added to open PR targeting master + if: + - payloadType: Pull_Request + - isOpen + - labelAdded: + label: CL-BuildPackaging + - targetsBranch: + branch: master + - not: + hasLabel: + label: BackPort-7.4.x-Consider + then: + - addLabel: + label: BackPort-7.4.x-Consider + + - description: Add BackPort-7.5.x-Consider when CL-BuildPackaging is added to open PR targeting master + if: + - payloadType: Pull_Request + - isOpen + - labelAdded: + label: CL-BuildPackaging + - targetsBranch: + branch: master + - not: + hasLabel: + label: BackPort-7.5.x-Consider + then: + - addLabel: + label: BackPort-7.5.x-Consider + + - description: Add BackPort-7.6.x-Consider when CL-BuildPackaging is added to open PR targeting master + if: + - payloadType: Pull_Request + - isOpen + - labelAdded: + label: CL-BuildPackaging + - targetsBranch: + branch: master + - not: + hasLabel: + label: BackPort-7.6.x-Consider + then: + - addLabel: + label: BackPort-7.6.x-Consider +onFailure: +onSuccess: From 7cdcd5893f784ddee1e6db702013190e83c0bb7a Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 23 Feb 2026 13:45:32 -0800 Subject: [PATCH 095/112] Correct the package name for .deb and .rpm packages (#26877) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Travis Plunk --- .github/workflows/macos-ci.yml | 3 ++- tools/packaging/packaging.psm1 | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 8f15a2f3a6..c7717a2673 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -54,6 +54,7 @@ jobs: outputs: source: ${{ steps.filter.outputs.source }} buildModuleChanged: ${{ steps.filter.outputs.buildModuleChanged }} + packagingChanged: ${{ steps.filter.outputs.packagingChanged }} steps: - name: checkout uses: actions/checkout@v6 @@ -161,7 +162,7 @@ jobs: name: macOS packaging and testing needs: - changes - if: ${{ needs.changes.outputs.source == 'true' || needs.changes.outputs.buildModuleChanged == 'true' }} + if: ${{ needs.changes.outputs.packagingChanged == 'true' }} runs-on: - macos-15-large steps: diff --git a/tools/packaging/packaging.psm1 b/tools/packaging/packaging.psm1 index f400e060b2..16ae6b21fc 100644 --- a/tools/packaging/packaging.psm1 +++ b/tools/packaging/packaging.psm1 @@ -1159,12 +1159,16 @@ function New-UnixPackage { } # Determine if the version is a preview version - # Only LTS packages get a prefix in the name - # Preview versions are identified by the version string itself (e.g., 7.6.0-preview.6) - # Rebuild versions are also identified by the version string (e.g., 7.4.13-rebuild.5) + $IsPreview = Test-IsPreview -Version $Version -IsLTS:$LTS + + # For deb/rpm packages, use the '-lts' and '-preview' channel suffix variants to match existing names on packages.microsoft.com. + # For osxpkg package, only LTS packages get a channel suffix in the name. $Name = if($LTS) { "powershell-lts" } + elseif ($IsPreview -and $Type -ne "osxpkg") { + "powershell-preview" + } else { "powershell" } From 43d4388d038a663bbd500481d3fd52817c00b93e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:27:47 -0800 Subject: [PATCH 096/112] Bump github/codeql-action from 4.32.3 to 4.32.4 (#26879) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index 96fb89287d..fbbb5c31ef 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5 + uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5 + uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index 422381cf50..c51e85b944 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v3.29.5 + uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5 with: sarif_file: results.sarif From a6e0d94c7c002618c10f14b9c70addfc2f7cf8f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 15:57:39 -0800 Subject: [PATCH 097/112] Bump actions/dependency-review-action from 4.8.2 to 4.8.3 (#26861) --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index dbef2e1c8b..6bace64885 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -19,4 +19,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 + uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 From 7311378cf9bdb8567becaecdcad49315da00175b Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:59:25 -0700 Subject: [PATCH 098/112] Add version in description and pass store task on failure (#26885) --- .pipelines/templates/release-MSIX-Publish.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.pipelines/templates/release-MSIX-Publish.yml b/.pipelines/templates/release-MSIX-Publish.yml index 2bf1e13010..a92c71f826 100644 --- a/.pipelines/templates/release-MSIX-Publish.yml +++ b/.pipelines/templates/release-MSIX-Publish.yml @@ -59,8 +59,15 @@ jobs: $json.listings.'en-us'.baseListing.releaseNotes = $message + # Add PowerShell version to the top of the description + $description = $json.listings.'en-us'.baseListing.description + $version = "$(ReleaseTag)" + $updatedDescription = "Version: $version`n`n$description" + $json.listings.'en-us'.baseListing.description = $updatedDescription + Write-Verbose -Verbose "Updated description: $updatedDescription" + $json | ConvertTo-Json -Depth 100 | Set-Content $jsonPath -Encoding UTF8 - displayName: 'Update Release Notes in JSON' + displayName: 'Add Changelog Link and Version Number to SBJSON' - task: PowerShell@2 inputs: @@ -101,6 +108,7 @@ jobs: - task: MS-RDX-MRO.windows-store-publish.publish-task.store-publish@3 displayName: 'Publish StoreBroker Package (Stable/LTS)' condition: and(ne('${{ parameters.skipMSIXPublish }}', 'true'), or(eq(variables['STABLE'], 'true'), eq(variables['LTS'], 'true'))) + continueOnError: true inputs: serviceEndpoint: 'StoreAppPublish-Stable' appId: '$(AppID)' @@ -114,6 +122,7 @@ jobs: - task: MS-RDX-MRO.windows-store-publish.publish-task.store-publish@3 displayName: 'Publish StoreBroker Package (Preview)' condition: and(ne('${{ parameters.skipMSIXPublish }}', 'true'), eq(variables['PREVIEW'], 'true')) + continueOnError: true inputs: serviceEndpoint: 'StoreAppPublish-Preview' appId: '$(AppID)' From 6e2b4c784a8b2e832e5442c9b2c72f3433339873 Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Wed, 25 Feb 2026 14:51:56 -0500 Subject: [PATCH 099/112] Split TPN manifest and Component Governance manifest (#26891) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../templates/compliance/generateNotice.yml | 2 +- .vsts-ci/linux-internal.yml | 2 +- .vsts-ci/mac.yml | 2 +- .vsts-ci/psresourceget-acr.yml | 2 +- .vsts-ci/windows-arm64.yml | 2 +- tools/{ => cgmanifest/main}/cgmanifest.json | 96 +-- tools/cgmanifest/tpn/cgmanifest.json | 755 ++++++++++++++++++ tools/clearlyDefined/ClearlyDefined.ps1 | 2 +- .../Find-LastHarvestedVersion.ps1 | 156 ++++ .../src/ClearlyDefined/ClearlyDefined.psm1 | 275 ++++++- tools/findMissingNotices.ps1 | 234 +++++- tools/packaging/packaging.psm1 | 2 +- 12 files changed, 1459 insertions(+), 71 deletions(-) rename tools/{ => cgmanifest/main}/cgmanifest.json (91%) create mode 100644 tools/cgmanifest/tpn/cgmanifest.json create mode 100644 tools/clearlyDefined/Find-LastHarvestedVersion.ps1 diff --git a/.pipelines/templates/compliance/generateNotice.yml b/.pipelines/templates/compliance/generateNotice.yml index 0a38ed8f8e..90fd08dd8d 100644 --- a/.pipelines/templates/compliance/generateNotice.yml +++ b/.pipelines/templates/compliance/generateNotice.yml @@ -55,7 +55,7 @@ jobs: - task: ms.vss-governance-buildtask.governance-build-task-component-detection.ComponentGovernanceComponentDetection@0 displayName: 'Component Detection' inputs: - sourceScanPath: '$(repoRoot)\tools' + sourceScanPath: '$(repoRoot)\tools\cgmanifest\tpn' - pwsh: | $(repoRoot)/tools/clearlyDefined/ClearlyDefined.ps1 -TestAndHarvest diff --git a/.vsts-ci/linux-internal.yml b/.vsts-ci/linux-internal.yml index c1c8bcef62..b90ab0d9eb 100644 --- a/.vsts-ci/linux-internal.yml +++ b/.vsts-ci/linux-internal.yml @@ -34,7 +34,7 @@ pr: - .vsts-ci/misc-analysis.yml - .vsts-ci/windows.yml - .vsts-ci/windows/* - - tools/cgmanifest.json + - tools/cgmanifest/* - LICENSE.txt - test/common/markdown/* - test/perf/* diff --git a/.vsts-ci/mac.yml b/.vsts-ci/mac.yml index 4d3681edca..678ded6525 100644 --- a/.vsts-ci/mac.yml +++ b/.vsts-ci/mac.yml @@ -34,7 +34,7 @@ pr: - .vsts-ci/misc-analysis.yml - .vsts-ci/windows.yml - .vsts-ci/windows/* - - tools/cgmanifest.json + - tools/cgmanifest/* - LICENSE.txt - test/common/markdown/* - test/perf/* diff --git a/.vsts-ci/psresourceget-acr.yml b/.vsts-ci/psresourceget-acr.yml index 194c7ba9f5..225e269953 100644 --- a/.vsts-ci/psresourceget-acr.yml +++ b/.vsts-ci/psresourceget-acr.yml @@ -34,7 +34,7 @@ pr: - .github/ISSUE_TEMPLATE/* - .github/workflows/* - .vsts-ci/misc-analysis.yml - - tools/cgmanifest.json + - tools/cgmanifest/* - LICENSE.txt - test/common/markdown/* - test/perf/* diff --git a/.vsts-ci/windows-arm64.yml b/.vsts-ci/windows-arm64.yml index 4c75c1d31e..1c4bc2ee8a 100644 --- a/.vsts-ci/windows-arm64.yml +++ b/.vsts-ci/windows-arm64.yml @@ -28,7 +28,7 @@ pr: - .dependabot/config.yml - .github/ISSUE_TEMPLATE/* - .vsts-ci/misc-analysis.yml - - tools/cgmanifest.json + - tools/cgmanifest/* - LICENSE.txt - test/common/markdown/* - test/perf/* diff --git a/tools/cgmanifest.json b/tools/cgmanifest/main/cgmanifest.json similarity index 91% rename from tools/cgmanifest.json rename to tools/cgmanifest/main/cgmanifest.json index 2334f07085..a0746028a5 100644 --- a/tools/cgmanifest.json +++ b/tools/cgmanifest/main/cgmanifest.json @@ -85,7 +85,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Bcl.AsyncInterfaces", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -125,7 +125,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Extensions.ObjectPool", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -165,7 +165,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.Registry.AccessControl", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -175,7 +175,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Win32.SystemEvents", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -185,7 +185,7 @@ "Type": "nuget", "Nuget": { "Name": "Microsoft.Windows.Compatibility", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -205,7 +205,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -215,7 +215,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -225,7 +225,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -235,7 +235,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.android-x86.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -245,7 +245,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -255,7 +255,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -265,7 +265,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -275,7 +275,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-bionic-x64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -285,7 +285,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -295,7 +295,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -305,7 +305,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-musl-x64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -315,7 +315,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.linux-x64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -325,7 +325,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -335,7 +335,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.maccatalyst-x64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -355,7 +355,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -365,7 +365,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-arm64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -375,7 +375,7 @@ "Type": "nuget", "Nuget": { "Name": "runtime.osx-x64.runtime.native.System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -435,7 +435,7 @@ "Type": "nuget", "Nuget": { "Name": "System.CodeDom", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -445,7 +445,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition.Registration", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -455,7 +455,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ComponentModel.Composition", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -465,7 +465,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Configuration.ConfigurationManager", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -475,7 +475,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.Odbc", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -485,7 +485,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.OleDb", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -505,7 +505,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.EventLog", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -515,7 +515,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Diagnostics.PerformanceCounter", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -525,7 +525,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.AccountManagement", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -535,7 +535,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices.Protocols", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -545,7 +545,7 @@ "Type": "nuget", "Nuget": { "Name": "System.DirectoryServices", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -555,7 +555,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Drawing.Common", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -565,7 +565,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Packaging", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -575,7 +575,7 @@ "Type": "nuget", "Nuget": { "Name": "System.IO.Ports", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -585,7 +585,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Management", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -595,7 +595,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Net.Http.WinHttpHandler", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -605,7 +605,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Reflection.Context", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -615,7 +615,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Runtime.Caching", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -625,7 +625,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Pkcs", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -635,7 +635,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.ProtectedData", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -645,7 +645,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Cryptography.Xml", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -655,7 +655,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Security.Permissions", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -705,7 +705,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceModel.Syndication", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -715,7 +715,7 @@ "Type": "nuget", "Nuget": { "Name": "System.ServiceProcess.ServiceController", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -725,7 +725,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Speech", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false @@ -745,7 +745,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Windows.Extensions", - "Version": "10.0.2" + "Version": "10.0.3" } }, "DevelopmentDependency": false diff --git a/tools/cgmanifest/tpn/cgmanifest.json b/tools/cgmanifest/tpn/cgmanifest.json new file mode 100644 index 0000000000..a0746028a5 --- /dev/null +++ b/tools/cgmanifest/tpn/cgmanifest.json @@ -0,0 +1,755 @@ +{ + "Registrations": [ + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "DotNetAnalyzers.DocumentationAnalyzers.Unstable", + "Version": "1.0.0.59" + } + }, + "DevelopmentDependency": true + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "DotNetAnalyzers.DocumentationAnalyzers", + "Version": "1.0.0-beta.59" + } + }, + "DevelopmentDependency": true + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Humanizer.Core", + "Version": "2.14.1" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Json.More.Net", + "Version": "2.1.1" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "JsonPointer.Net", + "Version": "5.3.1" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "JsonSchema.Net", + "Version": "7.4.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Markdig.Signed", + "Version": "0.45.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.ApplicationInsights", + "Version": "2.23.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.Bcl.AsyncInterfaces", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.CodeAnalysis.Analyzers", + "Version": "3.11.0" + } + }, + "DevelopmentDependency": true + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.CodeAnalysis.Common", + "Version": "5.0.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.CodeAnalysis.CSharp", + "Version": "5.0.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.Extensions.ObjectPool", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.Management.Infrastructure.Runtime.Win", + "Version": "3.0.0" + } + }, + "DevelopmentDependency": true + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.PowerShell.MarkdownRender", + "Version": "7.2.1" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.Security.Extensions", + "Version": "1.4.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.Win32.Registry.AccessControl", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.Win32.SystemEvents", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Microsoft.Windows.Compatibility", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "Newtonsoft.Json", + "Version": "13.0.4" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.android-arm.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.android-arm64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.android-x64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.android-x86.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-arm.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-arm64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-bionic-arm64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-bionic-x64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-musl-arm.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-musl-arm64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-musl-x64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.linux-x64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.maccatalyst-arm64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.maccatalyst-x64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.native.System.Data.SqlClient.sni", + "Version": "4.4.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.osx-arm64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.osx-x64.runtime.native.System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.win-arm64.runtime.native.System.Data.SqlClient.sni", + "Version": "4.4.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.win-x64.runtime.native.System.Data.SqlClient.sni", + "Version": "4.4.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "runtime.win-x86.runtime.native.System.Data.SqlClient.sni", + "Version": "4.4.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "StyleCop.Analyzers.Unstable", + "Version": "1.2.0.556" + } + }, + "DevelopmentDependency": true + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "StyleCop.Analyzers", + "Version": "1.1.118" + } + }, + "DevelopmentDependency": true + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.CodeDom", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ComponentModel.Composition.Registration", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ComponentModel.Composition", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Configuration.ConfigurationManager", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Data.Odbc", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Data.OleDb", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Data.SqlClient", + "Version": "4.9.0" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Diagnostics.EventLog", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Diagnostics.PerformanceCounter", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.DirectoryServices.AccountManagement", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.DirectoryServices.Protocols", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.DirectoryServices", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Drawing.Common", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.IO.Packaging", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.IO.Ports", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Management", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Net.Http.WinHttpHandler", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Reflection.Context", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Runtime.Caching", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Security.Cryptography.Pkcs", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Security.Cryptography.ProtectedData", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Security.Cryptography.Xml", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Security.Permissions", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ServiceModel.Http", + "Version": "10.0.652802" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ServiceModel.NetFramingBase", + "Version": "10.0.652802" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ServiceModel.NetTcp", + "Version": "10.0.652802" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ServiceModel.Primitives", + "Version": "10.0.652802" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ServiceModel.Syndication", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.ServiceProcess.ServiceController", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Speech", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Web.Services.Description", + "Version": "8.1.2" + } + }, + "DevelopmentDependency": false + }, + { + "Component": { + "Type": "nuget", + "Nuget": { + "Name": "System.Windows.Extensions", + "Version": "10.0.3" + } + }, + "DevelopmentDependency": false + } + ], + "$schema": "https://json.schemastore.org/component-detection-manifest.json" +} diff --git a/tools/clearlyDefined/ClearlyDefined.ps1 b/tools/clearlyDefined/ClearlyDefined.ps1 index 1830c2969e..c5303b8622 100644 --- a/tools/clearlyDefined/ClearlyDefined.ps1 +++ b/tools/clearlyDefined/ClearlyDefined.ps1 @@ -21,7 +21,7 @@ if ($ForceModuleReload) { Import-Module -Name "$PSScriptRoot/src/ClearlyDefined" @extraParams -$cgManfest = Get-Content "$PSScriptRoot/../cgmanifest.json" | ConvertFrom-Json +$cgManfest = Get-Content "$PSScriptRoot/../cgmanifest/main/cgmanifest.json" | ConvertFrom-Json $fullCgList = $cgManfest.Registrations.Component | ForEach-Object { [Pscustomobject]@{ diff --git a/tools/clearlyDefined/Find-LastHarvestedVersion.ps1 b/tools/clearlyDefined/Find-LastHarvestedVersion.ps1 new file mode 100644 index 0000000000..a989a3e1fc --- /dev/null +++ b/tools/clearlyDefined/Find-LastHarvestedVersion.ps1 @@ -0,0 +1,156 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +<# +.SYNOPSIS + Find the last harvested version of a NuGet package from ClearlyDefined. + +.DESCRIPTION + Searches for the last harvested version of a package by checking versions + backwards from the specified current version. This is useful for reverting + to a known-good harvested version when a newer version hasn't been harvested yet. + +.PARAMETER Name + The NuGet package name to search for. + +.PARAMETER CurrentVersion + The version to start searching backwards from. Version comparison uses semantic versioning. + +.PARAMETER PackageSourceName + The NuGet package source name to use when searching for available versions. + Default is 'findMissingNoticesNugetOrg' if not specified. + +.EXAMPLE + Find-LastHarvestedVersion -Name "Microsoft.Windows.Compatibility" -CurrentVersion "8.0.24" + + # This will return "8.0.22" if that's the last harvested version + +.NOTES + Requires the ClearlyDefined module to be imported: + Import-Module ".\clearlyDefined\src\ClearlyDefined" -Force +#> + +function Find-LastHarvestedVersion { + [CmdletBinding()] + param( + [parameter(Mandatory)] + [string]$Name, + + [parameter(Mandatory)] + [string]$CurrentVersion, + + [string]$PackageSourceName = 'findMissingNoticesNugetOrg' + ) + + try { + Write-Verbose "Finding last harvested version for $Name starting from v$CurrentVersion..." + + # Parse the current version + try { + [System.Management.Automation.SemanticVersion]$currentSemVer = $CurrentVersion + } catch { + [Version]$currentSemVer = $CurrentVersion + } + + # First try the ClearlyDefined search API (more efficient) + try { + Write-Verbose "Searching ClearlyDefined API for versions of $Name (sorted by release date)..." + # Get versions sorted by release date descending (newest first) for efficiency + $versions = Get-ClearlyDefinedPackageVersions -PackageName $Name + + if ($versions -and $versions.Count -gt 0) { + # Results are already sorted by release date newest first + # Filter to versions <= current version + foreach ($versionInfo in $versions) { + try { + $versionObj = [System.Management.Automation.SemanticVersion]$versionInfo.Version + if ($versionObj -le $currentSemVer) { + # Check harvest status + if ($versionInfo.Harvested) { + Write-Verbose "Found harvested version: v$($versionInfo.Version)" + return $versionInfo.Version + } else { + Write-Verbose "v$($versionInfo.Version) - Not harvested, continuing..." + } + } + } catch { + # Skip versions that can't be parsed + } + } + + Write-Verbose "No harvested version found in ClearlyDefined results" + return $null + } + } catch { + Write-Verbose "ClearlyDefined search API failed ($_), falling back to NuGet search..." + } + + # Fallback: Get all available versions from NuGet and check individually + Write-Verbose "Falling back to NuGet source search..." + + # Ensure package source exists + if (!(Get-PackageSource -Name $PackageSourceName -ErrorAction SilentlyContinue)) { + Write-Verbose "Registering package source: $PackageSourceName" + $null = Register-PackageSource -Name $PackageSourceName -Location https://www.nuget.org/api/v2 -ProviderName NuGet + } + + # Get all available versions from NuGet + try { + $allVersions = Find-Package -Name $Name -AllowPrereleaseVersions -source $PackageSourceName -AllVersions -ErrorAction SilentlyContinue | ForEach-Object { + try { + $packageVersion = [System.Management.Automation.SemanticVersion]$_.Version + } catch { + $packageVersion = [Version]$_.Version + } + $_ | Add-Member -Name SemVer -MemberType NoteProperty -Value $packageVersion -PassThru + } | Where-Object { $_.SemVer -le $currentSemVer } | Sort-Object -Property SemVer -Descending | ForEach-Object { $_.Version } + } catch { + Write-Warning "Failed to get versions for $Name : $_" + return $null + } + + if (!$allVersions) { + Write-Verbose "No versions found for $Name" + return $null + } + + # Check each version backwards until we find one that's harvested + foreach ($version in $allVersions) { + $pkg = [PSCustomObject]@{ + type = "nuget" + Name = $Name + PackageVersion = $version + } + + try { + $result = $pkg | Get-ClearlyDefinedData + if ($result -and $result.harvested) { + Write-Verbose "Found harvested version: v$version" + return $version + } else { + Write-Verbose "v$version - Not harvested, continuing..." + } + } catch { + Write-Verbose "Error checking v$version : $_" -Verbose + } + } + + Write-Verbose "No harvested version found for $Name" + return $null + } finally { + Save-ClearlyDefinedCache + } +} + +# If this script is called directly (not sourced), run a test +if ($MyInvocation.InvocationName -eq '.' -or $MyInvocation.Line -like '. "*Find-LastHarvestedVersion*') { + # Script was sourced, just load the function +} else { + # Script was called directly + Write-Host "Testing Find-LastHarvestedVersion function..." + Write-Host "Ensure ClearlyDefined module is loaded first:" + Write-Host ' Import-Module ".\clearlydefined\src\ClearlyDefined" -Force' + Write-Host "" + Write-Host "Example usage:" + Write-Host ' Find-LastHarvestedVersion -Name "Microsoft.Windows.Compatibility" -CurrentVersion "8.0.24"' +} diff --git a/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 b/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 index 4d87440297..4e3d375dc5 100644 --- a/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 +++ b/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 @@ -2,6 +2,9 @@ # Licensed under the MIT License. # Start the collection (known as harvest) of ClearlyDefined data for a package + +$retryIntervalSec = 90 +$maxRetryCount = 5 function Start-ClearlyDefinedHarvest { [CmdletBinding()] param( @@ -27,7 +30,9 @@ function Start-ClearlyDefinedHarvest { $coordinates = Get-ClearlyDefinedCoordinates @PSBoundParameters $body = @{tool='package';coordinates=$coordinates} | convertto-json Write-Verbose $body -Verbose - (Invoke-WebRequest -Method Post -Uri 'https://api.clearlydefined.io/harvest' -Body $body -ContentType 'application/json' -MaximumRetryCount 5 -RetryIntervalSec 60 -Verbose).Content + Start-job -ScriptBlock { + Invoke-WebRequest -Method Post -Uri 'https://api.clearlydefined.io/harvest' -Body $using:body -ContentType 'application/json' -MaximumRetryCount $using:maxRetryCount -RetryIntervalSec $using:retryIntervalSec + } } } @@ -74,6 +79,207 @@ Function Get-ClearlyDefinedCoordinates { # Cache of ClearlyDefined data $cdCache = @{} +function Test-ClearlyDefinedCachePersistenceAllowed { + [CmdletBinding()] + param() + + if ($env:TF_BUILD -or $env:ADO_BUILD_ID -or $env:BUILD_BUILDID) { + return $false + } + + if ($env:GITHUB_ACTIONS -or $env:GITHUB_RUN_ID) { + return $false + } + + return $true +} + +function Get-ClearlyDefinedCachePath { + [CmdletBinding()] + param() + + $tempPath = [System.IO.Path]::GetTempPath() + return (Join-Path -Path $tempPath -ChildPath 'clearlydefined-cache.json') +} + +function Save-ClearlyDefinedCache { + [CmdletBinding()] + param() + + if (-not (Test-ClearlyDefinedCachePersistenceAllowed)) { + Write-Verbose 'Skipping cache persistence for CI environment.' + return + } + + if ($cdCache.Count -eq 0) { + Write-Verbose 'No cache entries to persist.' + return + } + + $cachePath = Get-ClearlyDefinedCachePath + $entries = foreach ($key in $cdCache.Keys) { + [PSCustomObject]@{ + coordinates = $key + data = $cdCache[$key] + } + } + + $cachePayload = @{ + savedAtUtc = (Get-Date).ToUniversalTime() + entries = $entries + } | ConvertTo-Json -Depth 20 + + $cachePayload | Set-Content -Path $cachePath -Encoding UTF8 + Write-Verbose "Persisted cache to $cachePath" +} + +function Import-ClearlyDefinedCache { + [CmdletBinding()] + param() + + if (-not (Test-ClearlyDefinedCachePersistenceAllowed)) { + Write-Verbose 'Skipping cache import for CI environment.' + return + } + + $cachePath = Get-ClearlyDefinedCachePath + if (-not (Test-Path -Path $cachePath)) { + Write-Verbose 'No persisted cache found.' + return + } + + try { + $payload = Get-Content -Path $cachePath -Raw | ConvertFrom-Json + } catch { + Write-Verbose "Failed to read cache file: $cachePath" + return + } + + if (-not $payload.entries) { + Write-Verbose 'Cache file did not contain entries.' + return + } + + foreach ($entry in $payload.entries) { + if (-not $entry.coordinates -or -not $entry.data) { + continue + } + + try { + $entry.data.cachedTime = [datetime]$entry.data.cachedTime + } catch { + continue + } + + $cdCache[$entry.coordinates] = $entry.data + } + + Write-Verbose "Imported $($cdCache.Count) cache entries from $cachePath" +} + +# Search for packages in ClearlyDefined +Function Search-ClearlyDefined { + [CmdletBinding()] + param( + [string]$Type = 'nuget', + [string]$Provider = 'nuget', + [string]$Namespace, + [string]$Name, + [string]$Pattern, + [datetime]$ReleasedAfter, + [datetime]$ReleasedBefore, + [ValidateSet('releaseDate', 'name')] + [string]$Sort, + [switch]$SortDesc + ) + + $queryParams = @() + if ($Type) { $queryParams += "type=$([System.Uri]::EscapeDataString($Type))" } + if ($Provider) { $queryParams += "provider=$([System.Uri]::EscapeDataString($Provider))" } + if ($Namespace) { $queryParams += "namespace=$([System.Uri]::EscapeDataString($Namespace))" } + if ($Name) { $queryParams += "name=$([System.Uri]::EscapeDataString($Name))" } + if ($Pattern) { $queryParams += "pattern=$([System.Uri]::EscapeDataString($Pattern))" } + if ($ReleasedAfter) { $queryParams += "releasedAfter=$($ReleasedAfter.ToString('o'))" } + if ($ReleasedBefore) { $queryParams += "releasedBefore=$($ReleasedBefore.ToString('o'))" } + if ($Sort) { $queryParams += "sort=$([System.Uri]::EscapeDataString($Sort))" } + if ($SortDesc) { $queryParams += "sortDesc=true" } + + $searchUri = "https://api.clearlydefined.io/definitions?" + ($queryParams -join '&') + Write-Verbose "Searching ClearlyDefined: $searchUri" + + try { + $results = Invoke-RestMethod -Uri $searchUri -MaximumRetryCount $maxRetryCount -RetryIntervalSec $retryIntervalSec + return $results + } catch { + if ($retryIntervalSec -lt 300) { + $retryIntervalSec++ + } + + Write-Warning "Failed to search ClearlyDefined: $_" + return $null + } +} + +# Get available versions for a NuGet package with harvest status +Function Get-ClearlyDefinedPackageVersions { + [CmdletBinding()] + param( + [parameter(mandatory = $true)] + [string] + $PackageName, + + [validateset('nuget')] + [string] + $PackageType = 'nuget' + ) + + # Search for all definitions of this package, sorted by release date (newest first) + Write-Verbose "Fetching versions of $PackageName from ClearlyDefined..." + + $results = Search-ClearlyDefined -Type $PackageType -Provider nuget -Name $PackageName -Sort releaseDate -SortDesc + + if (!$results) { + Write-Verbose "No results found for $PackageName" + return @() + } + + # Convert results to version info objects + $versions = @() + + # API returns results in different formats depending on the query + $dataArray = $null + if ($results.data) { + $dataArray = $results.data + } elseif ($results -is [array]) { + $dataArray = $results + } elseif ($results.PSObject.Properties.Count -gt 0) { + # If it's an object with properties, try to extract the actual results + foreach ($prop in $results.PSObject.Properties) { + if ($prop.Value -is [object] -and $prop.Value.revision) { + $dataArray += $prop.Value + } + } + } + + if ($dataArray) { + foreach ($item in $dataArray) { + if ($item.revision) { + $harvested = if ($item.licensed -and $item.licensed.declared) { $true } else { $false } + + $versions += [PSCustomObject]@{ + Name = $item.name + Version = $item.revision + Harvested = $harvested + Licensed = $item.licensed.declared + } + } + } + } + + # Results are already sorted by API, no need to re-sort + return $versions +} + # Get the ClearlyDefined data for a package Function Get-ClearlyDefinedData { [CmdletBinding()] @@ -96,8 +302,9 @@ Function Get-ClearlyDefinedData { ) Begin { - $cacheMinutes = 60 - $cacheCutoff = (get-date).AddMinutes(-$cacheMinutes) + # Different TTLs for different cache types + $harvestedCacheMinutes = 60 # Cache positive results for 60 minutes + $nonHarvestedCacheMinutes = 30 # Cache negative results for 30 minutes (less aggressive) $coordinateList = @() } @@ -111,19 +318,55 @@ Function Get-ClearlyDefinedData { foreach($coordinates in $coordinateList) { Write-Progress -Activity "Getting ClearlyDefined data" -Status "Getting data for $coordinates" -PercentComplete (($completed / $total) * 100) $containsKey = $cdCache.ContainsKey($coordinates) - if ($containsKey -and $cdCache[$coordinates].cachedTime -gt $cacheCutoff) { - Write-Verbose "Returning cached data for $coordinates" - Write-Output $cdCache[$coordinates] - continue + + if ($containsKey) { + $cached = $cdCache[$coordinates] + # Check if cache entry is still valid based on its type + $cacheCutoff = if ($cached.harvestedResult) { + (get-date).AddMinutes(-$harvestedCacheMinutes) + } else { + (get-date).AddMinutes(-$nonHarvestedCacheMinutes) + } + + if ($cached.cachedTime -gt $cacheCutoff) { + Write-Progress -Activity "Getting ClearlyDefined data" -Status "Getting data for $coordinates - cache hit" -PercentComplete (($completed / $total) * 100) + Write-Verbose "Returning cached data for $coordinates (harvested: $($cached.harvestedResult))" + Write-Output $cached + $completed++ + continue + } } - Invoke-RestMethod -Uri "https://api.clearlydefined.io/definitions/$coordinates" -MaximumRetryCount 5 -RetryIntervalSec 60 | ForEach-Object { - [bool] $harvested = if ($_.licensed.declared) { $true } else { $false } - Add-Member -NotePropertyName cachedTime -NotePropertyValue (get-date) -InputObject $_ -PassThru | Add-Member -NotePropertyName harvested -NotePropertyValue $harvested -PassThru - if ($_.harvested) { - Write-Verbose "Caching data for $coordinates" - $cdCache[$coordinates] = $_ + Write-Progress -Activity "Getting ClearlyDefined data" -Status "Getting data for $coordinates - cache miss" -PercentComplete (($completed / $total) * 100) + + try { + Invoke-RestMethod -Uri "https://api.clearlydefined.io/definitions/$coordinates" -MaximumRetryCount $maxRetryCount -RetryIntervalSec $retryIntervalSec | ForEach-Object { + [bool] $harvested = if ($_.licensed.declared) { $true } else { $false } + # Always cache, with harvestedResult property to distinguish for TTL purposes + Add-Member -NotePropertyName cachedTime -NotePropertyValue (get-date) -InputObject $_ -PassThru | + Add-Member -NotePropertyName harvested -NotePropertyValue $harvested -PassThru | + Add-Member -NotePropertyName harvestedResult -NotePropertyValue $harvested -PassThru | + ForEach-Object { + Write-Verbose "Caching data for $coordinates (harvested: $($_.harvested))" + $cdCache[$coordinates] = $_ + Write-Output $_ + } } + } catch { + if ($retryIntervalSec -lt 300) { + $retryIntervalSec++ + } + + Write-Warning "Failed to get ClearlyDefined data for $coordinates : $_" + # Return a minimal object indicating failure/not harvested so the pipeline continues + $failedResult = [PSCustomObject]@{ + coordinates = $coordinates + harvested = $false + harvestedResult = $false + cachedTime = (get-date) + licensed = @{ declared = $null } + } + Write-Output $failedResult } $completed++ } @@ -134,4 +377,10 @@ Export-ModuleMember -Function @( 'Start-ClearlyDefinedHarvest' 'Get-ClearlyDefinedData' 'ConvertFrom-ClearlyDefinedCoordinates' + 'Search-ClearlyDefined' + 'Get-ClearlyDefinedPackageVersions' + 'Save-ClearlyDefinedCache' + 'Import-ClearlyDefinedCache' + 'Test-ClearlyDefinedCachePersistenceAllowed' + 'Get-ClearlyDefinedCachePath' ) diff --git a/tools/findMissingNotices.ps1 b/tools/findMissingNotices.ps1 index 1346148bae..884eff5066 100644 --- a/tools/findMissingNotices.ps1 +++ b/tools/findMissingNotices.ps1 @@ -7,12 +7,14 @@ param( [switch] $Fix, - [switch] $IsStable + [switch] $IsStable, + [switch] $ForceHarvestedOnly ) Import-Module dotnet.project.assets Import-Module "$PSScriptRoot\..\.github\workflows\GHWorkflowHelper" -Force . "$PSScriptRoot\..\tools\buildCommon\startNativeExecution.ps1" +. "$PSScriptRoot\clearlyDefined\Find-LastHarvestedVersion.ps1" $packageSourceName = 'findMissingNoticesNugetOrg' if (!(Get-PackageSource -Name $packageSourceName -ErrorAction SilentlyContinue)) { @@ -20,7 +22,7 @@ if (!(Get-PackageSource -Name $packageSourceName -ErrorAction SilentlyContinue)) } $existingRegistrationTable = @{} -$cgManifestPath = (Resolve-Path -Path $PSScriptRoot\..\tools\cgmanifest.json).ProviderPath +$cgManifestPath = (Resolve-Path -Path $PSScriptRoot\cgmanifest\main\cgmanifest.json).ProviderPath $existingRegistrationsJson = Get-Content $cgManifestPath | ConvertFrom-Json -AsHashtable $existingRegistrationsJson.Registrations | ForEach-Object { $registration = [Registration]$_ @@ -335,8 +337,91 @@ if ($IsStable) { } $count = $newRegistrations.Count +$registrationsToSave = $newRegistrations +$tpnRegistrationsToSave = $null + +# If -ForceHarvestedOnly is specified with -Fix, only include harvested packages +# and revert non-harvested packages to their previous versions +if ($Fix -and $ForceHarvestedOnly) { + Write-Verbose "Checking harvest status and filtering to harvested packages with reversion..." -Verbose + + # Import ClearlyDefined module to check harvest status + Import-Module -Name "$PSScriptRoot/clearlyDefined/src/ClearlyDefined" -Force + + # Import cache from previous runs to speed up lookups + Import-ClearlyDefinedCache + + # Get harvest data for all registrations + $fullCgList = $newRegistrations | + ForEach-Object { + [PSCustomObject]@{ + type = $_.Component.Type + Name = $_.Component.Nuget.Name + PackageVersion = $_.Component.Nuget.Version + } + } + + $fullList = $fullCgList | Get-ClearlyDefinedData + + # Build a lookup table of harvest status by package name + version + $harvestStatus = @{} + foreach ($item in $fullList) { + $key = "$($item.Name)|$($item.PackageVersion)" + $harvestStatus[$key] = $item.harvested + } + + # Build a lookup table of old versions from existing manifest + $oldVersions = @{} + foreach ($registration in $existingRegistrationsJson.Registrations) { + $name = $registration.Component.Nuget.Name + if (!$oldVersions.ContainsKey($name)) { + $oldVersions[$name] = $registration + } + } + + # Process each new registration: keep harvested, revert non-harvested + $tpnRegistrationsToSave = @() + $harvestedCount = 0 + $revertedCount = 0 + + foreach ($reg in $newRegistrations) { + $name = $reg.Component.Nuget.Name + $version = $reg.Component.Nuget.Version + $key = "$name|$version" + + if ($harvestStatus.ContainsKey($key) -and $harvestStatus[$key]) { + # Package is harvested, include it + $tpnRegistrationsToSave += $reg + $harvestedCount++ + } else { + # Package not harvested, find last harvested version + $lastHarvestedVersion = Find-LastHarvestedVersion -Name $name -CurrentVersion $version + + # Use last harvested version if found, otherwise use old version as fallback + if ($lastHarvestedVersion) { + if ($lastHarvestedVersion -ne $version) { + $revertedReg = New-NugetComponent -Name $name -Version $lastHarvestedVersion -DevelopmentDependency:$reg.DevelopmentDependency + $tpnRegistrationsToSave += $revertedReg + $revertedCount++ + Write-Verbose "Reverted $name from v$version to last harvested v$lastHarvestedVersion" -Verbose + } else { + $tpnRegistrationsToSave += $reg + } + } elseif ($oldVersions.ContainsKey($name)) { + $tpnRegistrationsToSave += $oldVersions[$name] + $revertedCount++ + Write-Verbose "Reverted $name to previous version (no harvested version found)" -Verbose + } else { + Write-Warning "$name v$version not harvested and no previous version found. Excluding from manifest." + } + } + } + + Write-Verbose "Completed filtering for TPN: $harvestedCount harvested + $revertedCount reverted = $($tpnRegistrationsToSave.Count) total" -Verbose +} + $newJson = @{ - Registrations = $newRegistrations + Registrations = $registrationsToSave '$schema' = "https://json.schemastore.org/component-detection-manifest.json" } | ConvertTo-Json -depth 99 @@ -345,6 +430,149 @@ if ($Fix -and $registrationChanged) { Set-GWVariable -Name CGMANIFEST_PATH -Value $cgManifestPath } +# If -ForceHarvestedOnly was used, write the TPN manifest with filtered registrations +if ($Fix -and $ForceHarvestedOnly -and $tpnRegistrationsToSave.Count -gt 0) { + $tpnManifestDir = Join-Path -Path $PSScriptRoot -ChildPath "cgmanifest\tpn" + New-Item -ItemType Directory -Path $tpnManifestDir -Force | Out-Null + $tpnManifestPath = Join-Path -Path $tpnManifestDir -ChildPath "cgmanifest.json" + + $tpnManifest = @{ + Registrations = @($tpnRegistrationsToSave) + '$schema' = "https://json.schemastore.org/component-detection-manifest.json" + } + + $tpnJson = $tpnManifest | ConvertTo-Json -depth 99 + $tpnJson | Set-Content $tpnManifestPath -Encoding utf8NoBOM + Write-Verbose "TPN manifest created/updated with $($tpnRegistrationsToSave.Count) registrations (filtered for harvested packages)" -Verbose +} + +# Skip legacy TPN update when -ForceHarvestedOnly already produced a filtered manifest +if ($Fix -and $registrationChanged -and -not $ForceHarvestedOnly) { + # Import ClearlyDefined module to check harvest status + Write-Verbose "Checking harvest status for newly added packages..." -Verbose + Import-Module -Name "$PSScriptRoot/clearlyDefined/src/ClearlyDefined" -Force + + # Get harvest data for all registrations + $fullCgList = $newRegistrations | + ForEach-Object { + [PSCustomObject]@{ + type = $_.Component.Type + Name = $_.Component.Nuget.Name + PackageVersion = $_.Component.Nuget.Version + } + } + + $fullList = $fullCgList | Get-ClearlyDefinedData + $needHarvest = $fullList | Where-Object { !$_.harvested } + + if ($needHarvest.Count -gt 0) { + Write-Verbose "Found $($needHarvest.Count) packages that need harvesting. Starting harvest..." -Verbose + $needHarvest | Select-Object -ExpandProperty coordinates | ConvertFrom-ClearlyDefinedCoordinates | Start-ClearlyDefinedHarvest + } else { + Write-Verbose "All packages are already harvested." -Verbose + } + + # After manifest update and harvest, update TPN manifest with individual package status + Write-Verbose "Updating TPN manifest with individual package harvest status..." -Verbose + $tpnManifestDir = Join-Path -Path $PSScriptRoot -ChildPath "cgmanifest\tpn" + $tpnManifestPath = Join-Path -Path $tpnManifestDir -ChildPath "cgmanifest.json" + + # Load current TPN manifest to get previous versions + $currentTpnManifest = @() + if (Test-Path $tpnManifestPath) { + $currentTpnJson = Get-Content $tpnManifestPath | ConvertFrom-Json -AsHashtable + $currentTpnManifest = $currentTpnJson.Registrations + } + + # Build a lookup table of old versions + $oldVersions = @{} + foreach ($registration in $currentTpnManifest) { + $name = $registration.Component.Nuget.Name + if (!$oldVersions.ContainsKey($name)) { + $oldVersions[$name] = $registration + } + } + + # Note: Do not recheck harvest status here. Harvesting is an async process that takes a significant amount of time. + # Use the harvest data from the initial check. Newly triggered harvests will be captured + # on the next run of this script after harvesting completes. + $finalHarvestData = $fullList + + # Update packages individually based on harvest status + $tpnRegistrations = @() + $harvestedCount = 0 + $restoredCount = 0 + + foreach ($item in $finalHarvestData) { + $matchingNewRegistration = $newRegistrations | Where-Object { + $_.Component.Nuget.Name -eq $item.Name -and + $_.Component.Nuget.Version -eq $item.PackageVersion + } + + if ($matchingNewRegistration) { + if ($item.harvested) { + # Use new harvested version + $tpnRegistrations += $matchingNewRegistration + $harvestedCount++ + } else { + # Package not harvested - find the last harvested version from ClearlyDefined API + Write-Verbose "Finding last harvested version for $($item.Name)..." -Verbose + + $lastHarvestedVersion = $null + try { + # Search through all versions of this package to find the last harvested one + # Create a list of versions we know about from all runtimes + $packageVersionsToCheck = $newRegistrations | Where-Object { + $_.Component.Nuget.Name -eq $item.Name + } | ForEach-Object { $_.Component.Nuget.Version } | Sort-Object -Unique -Descending + + foreach ($versionToCheck in $packageVersionsToCheck) { + $versionCheckList = [PSCustomObject]@{ + type = "nuget" + Name = $item.Name + PackageVersion = $versionToCheck + } + + $versionStatus = $versionCheckList | Get-ClearlyDefinedData + if ($versionStatus -and $versionStatus.harvested) { + $lastHarvestedVersion = $versionToCheck + break # Found the most recent harvested version + } + } + } catch { + Write-Verbose "Error checking harvested versions for $($item.Name): $_" -Verbose + } + + # Use last harvested version if found, otherwise use old version as fallback + if ($lastHarvestedVersion) { + $revertedReg = New-NugetComponent -Name $item.Name -Version $lastHarvestedVersion -DevelopmentDependency:$matchingNewRegistration.DevelopmentDependency + $tpnRegistrations += $revertedReg + $restoredCount++ + Write-Verbose "Reverted $($item.Name) from v$($item.PackageVersion) to last harvested v$lastHarvestedVersion" -Verbose + } elseif ($oldVersions.ContainsKey($item.Name)) { + $tpnRegistrations += $oldVersions[$item.Name] + $restoredCount++ + Write-Verbose "Reverted $($item.Name) to previous version in TPN (no harvested version found)" -Verbose + } else { + Write-Warning "$($item.Name) v$($item.PackageVersion) not harvested and no harvested version found. Excluding from TPN manifest." + } + } + } + } + + # Save updated TPN manifest + if ($tpnRegistrations.Count -gt 0) { + $tpnManifest = @{ + Registrations = @($tpnRegistrations) + '$schema' = "https://json.schemastore.org/component-detection-manifest.json" + } + + $tpnJson = $tpnManifest | ConvertTo-Json -depth 99 + $tpnJson | Set-Content $tpnManifestPath -Encoding utf8NoBOM + Write-Verbose "TPN manifest updated: $harvestedCount new harvested + $restoredCount reverted to last harvested versions" -Verbose + } +} + if (!$Fix -and $registrationChanged) { $temp = Get-GWTempPath diff --git a/tools/packaging/packaging.psm1 b/tools/packaging/packaging.psm1 index 16ae6b21fc..53d895bc49 100644 --- a/tools/packaging/packaging.psm1 +++ b/tools/packaging/packaging.psm1 @@ -4920,7 +4920,7 @@ function New-GlobalToolNupkgSource } # Set VSTS environment variable for CGManifest file path. - $globalToolCGManifestPFilePath = Join-Path -Path "$env:REPOROOT" -ChildPath "tools\cgmanifest.json" + $globalToolCGManifestPFilePath = Join-Path -Path "$env:REPOROOT" -ChildPath "tools/cgmanifest/main/cgmanifest.json" $globalToolCGManifestFilePath = Resolve-Path -Path $globalToolCGManifestPFilePath -ErrorAction SilentlyContinue if (($null -eq $globalToolCGManifestFilePath) -or (! (Test-Path -Path $globalToolCGManifestFilePath))) { From 95e38946b187512721018da5b265df77728adb77 Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Thu, 26 Feb 2026 17:50:37 -0500 Subject: [PATCH 100/112] Add PMC packages for debian13 and rhel10 (#26912) --- tools/packages.microsoft.com/mapping.json | 42 +++++++++++++++-------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/tools/packages.microsoft.com/mapping.json b/tools/packages.microsoft.com/mapping.json index 682c96d911..334f6dfdd5 100644 --- a/tools/packages.microsoft.com/mapping.json +++ b/tools/packages.microsoft.com/mapping.json @@ -21,6 +21,13 @@ ], "PackageFormat": "PACKAGE_NAME-POWERSHELL_RELEASE-1.rh.x86_64.rpm" }, + { + "url": "microsoft-rhel10.0-prod", + "distribution": [ + "stable" + ], + "PackageFormat": "PACKAGE_NAME-POWERSHELL_RELEASE-1.rh.x86_64.rpm" + }, { "url": "cbl-mariner-2.0-prod-Microsoft-aarch64", "distribution": [ @@ -99,6 +106,27 @@ ], "PackageFormat": "PACKAGE_NAME_POWERSHELL_RELEASE-1.deb_amd64.deb" }, + { + "url": "microsoft-debian-bullseye-prod", + "distribution": [ + "bullseye" + ], + "PackageFormat": "PACKAGE_NAME_POWERSHELL_RELEASE-1.deb_amd64.deb" + }, + { + "url": "microsoft-debian-bookworm-prod", + "distribution": [ + "bookworm" + ], + "PackageFormat": "PACKAGE_NAME_POWERSHELL_RELEASE-1.deb_amd64.deb" + }, + { + "url": "microsoft-debian-trixie-prod", + "distribution": [ + "trixie" + ], + "PackageFormat": "PACKAGE_NAME_POWERSHELL_RELEASE-1.deb_amd64.deb" + }, { "url": "microsoft-ubuntu-bionic-prod", "distribution": [ @@ -133,20 +161,6 @@ "xenial" ], "PackageFormat": "PACKAGE_NAME_POWERSHELL_RELEASE-1.deb_amd64.deb" - }, - { - "url": "microsoft-debian-bullseye-prod", - "distribution": [ - "bullseye" - ], - "PackageFormat": "PACKAGE_NAME_POWERSHELL_RELEASE-1.deb_amd64.deb" - }, - { - "url": "microsoft-debian-bookworm-prod", - "distribution": [ - "bookworm" - ], - "PackageFormat": "PACKAGE_NAME_POWERSHELL_RELEASE-1.deb_amd64.deb" } ] } From 3544ce5dbbd8539342f6b4b45b8d8623941eb62c Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 2 Mar 2026 12:40:22 -0800 Subject: [PATCH 101/112] Update PowerShell Profile DSC resource manifests to allow null for content (#26929) --- dsc/pwsh.profile.dsc.resource.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json index cd18e94eec..aa5f5c29ee 100644 --- a/dsc/pwsh.profile.dsc.resource.json +++ b/dsc/pwsh.profile.dsc.resource.json @@ -90,7 +90,7 @@ "content": { "title": "Content", "description": "Defines the content of the profile. If you don't specify this property, the resource doesn't manage the file contents. If you specify this property as an empty string, the resource removes all content from the file. If you specify this property as a non-empty string, the resource sets the file contents to the specified string. The resources retains newlines from this property without any modification.", - "type": "string" + "type": [ "string", "null" ] }, "_exist": { "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json" From eb1915e76b511e1d7d1094c3dc2511065ed0c850 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:42:27 -0500 Subject: [PATCH 102/112] Hardcode Official templates (#26928) --- .../PowerShell-Coordinated_Packages-Official.yml | 11 +++-------- .pipelines/PowerShell-Packages-Official.yml | 11 +++-------- .pipelines/PowerShell-Release-Official-Azure.yml | 9 ++------- .pipelines/PowerShell-Release-Official.yml | 11 +++-------- .pipelines/PowerShell-vPack-Official.yml | 9 ++------- 5 files changed, 13 insertions(+), 38 deletions(-) diff --git a/.pipelines/PowerShell-Coordinated_Packages-Official.yml b/.pipelines/PowerShell-Coordinated_Packages-Official.yml index e4de1fe5c2..380c9c5516 100644 --- a/.pipelines/PowerShell-Coordinated_Packages-Official.yml +++ b/.pipelines/PowerShell-Coordinated_Packages-Official.yml @@ -29,11 +29,8 @@ parameters: displayName: Debugging - Enable CodeQL and set cadence to 1 hour type: boolean default: false - - name: OfficialBuild - type: boolean - default: false -name: bins-$(BUILD.SOURCEBRANCHNAME)-prod.${{ parameters.OfficialBuild }}-$(Build.BuildId) +name: bins-$(BUILD.SOURCEBRANCHNAME)-prod.true-$(Build.BuildId) resources: repositories: @@ -91,8 +88,6 @@ variables: value: true ${{ else }}: value: false - - name: templateFile - value: ${{ iif ( parameters.OfficialBuild, 'v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates', 'v2/OneBranch.NonOfficial.CrossPlat.yml@onebranchTemplates' ) }} # Fix for BinSkim ICU package error in Linux containers - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT value: true @@ -100,10 +95,10 @@ variables: - name: ob_sdl_binskim_enabled value: false - name: ps_official_build - value: ${{ parameters.OfficialBuild }} + value: true extends: - template: ${{ variables.templateFile }} + template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates parameters: customTags: 'ES365AIMigrationTooling' featureFlags: diff --git a/.pipelines/PowerShell-Packages-Official.yml b/.pipelines/PowerShell-Packages-Official.yml index 18ef7b2d14..a13ef12378 100644 --- a/.pipelines/PowerShell-Packages-Official.yml +++ b/.pipelines/PowerShell-Packages-Official.yml @@ -24,14 +24,11 @@ parameters: # parameters are shown up in ADO UI in a build queue time displayName: Skip Signing type: string default: 'NO' - - name: OfficialBuild - type: boolean - default: false - name: disableNetworkIsolation type: boolean default: false -name: pkgs-$(BUILD.SOURCEBRANCHNAME)-prod.${{ parameters.OfficialBuild }}-$(Build.BuildId) +name: pkgs-$(BUILD.SOURCEBRANCHNAME)-prod.true-$(Build.BuildId) variables: - name: CDP_DEFINITION_BUILD_COUNT @@ -67,8 +64,6 @@ variables: - name: branchCounter value: $[counter(variables['branchCounterKey'], 1)] - group: MSIXSigningProfile - - name: templateFile - value: ${{ iif ( parameters.OfficialBuild, 'v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates', 'v2/OneBranch.NonOfficial.CrossPlat.yml@onebranchTemplates' ) }} - name: disableNetworkIsolation value: ${{ parameters.disableNetworkIsolation }} @@ -89,7 +84,7 @@ resources: ref: refs/heads/main extends: - template: ${{ variables.templateFile }} + template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates parameters: cloudvault: enabled: false @@ -294,7 +289,7 @@ extends: jobs: - template: /.pipelines/templates/package-create-msix.yml@self parameters: - OfficialBuild: ${{ parameters.OfficialBuild }} + OfficialBuild: true - stage: upload displayName: 'Upload' diff --git a/.pipelines/PowerShell-Release-Official-Azure.yml b/.pipelines/PowerShell-Release-Official-Azure.yml index f4c41143b5..8154342046 100644 --- a/.pipelines/PowerShell-Release-Official-Azure.yml +++ b/.pipelines/PowerShell-Release-Official-Azure.yml @@ -13,11 +13,8 @@ parameters: # parameters are shown up in ADO UI in a build queue time displayName: Skip Signing type: string default: 'NO' - - name: OfficialBuild - type: boolean - default: false -name: ev2-$(BUILD.SOURCEBRANCHNAME)-prod.${{ parameters.OfficialBuild }}-$(Build.BuildId) +name: ev2-$(BUILD.SOURCEBRANCHNAME)-prod.true-$(Build.BuildId) variables: - name: CDP_DEFINITION_BUILD_COUNT @@ -49,8 +46,6 @@ variables: - name: LinuxContainerImage value: mcr.microsoft.com/onebranch/azurelinux/build:3.0 - group: PoolNames - - name: templateFile - value: ${{ iif ( parameters.OfficialBuild, 'v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates', 'v2/OneBranch.NonOfficial.CrossPlat.yml@onebranchTemplates' ) }} resources: repositories: @@ -72,7 +67,7 @@ resources: - releases/* extends: - template: ${{ variables.templateFile }} + template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates parameters: featureFlags: WindowsHostVersion: diff --git a/.pipelines/PowerShell-Release-Official.yml b/.pipelines/PowerShell-Release-Official.yml index 868d61ebfd..fa14b9b0ac 100644 --- a/.pipelines/PowerShell-Release-Official.yml +++ b/.pipelines/PowerShell-Release-Official.yml @@ -29,11 +29,8 @@ parameters: # parameters are shown up in ADO UI in a build queue time displayName: Skip MSIX Publish type: boolean default: false - - name: OfficialBuild - type: boolean - default: false -name: release-$(BUILD.SOURCEBRANCHNAME)-prod.${{ parameters.OfficialBuild }}-$(Build.BuildId) +name: release-$(BUILD.SOURCEBRANCHNAME)-prod.true-$(Build.BuildId) variables: - name: CDP_DEFINITION_BUILD_COUNT @@ -65,10 +62,8 @@ variables: - name: ReleaseTagVar value: ${{ parameters.ReleaseTagVar }} - group: PoolNames - - name: templateFile - value: ${{ iif ( parameters.OfficialBuild, 'v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates', 'v2/OneBranch.NonOfficial.CrossPlat.yml@onebranchTemplates' ) }} - name: releaseEnvironment - value: ${{ iif ( parameters.OfficialBuild, 'Production', 'Test' ) }} + value: 'Production' # Fix for BinSkim ICU package error in Linux containers - name: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT value: true @@ -97,7 +92,7 @@ resources: - releases/* extends: - template: ${{ variables.templateFile }} + template: v2/OneBranch.Official.CrossPlat.yml@onebranchTemplates parameters: release: category: NonAzure diff --git a/.pipelines/PowerShell-vPack-Official.yml b/.pipelines/PowerShell-vPack-Official.yml index 096dfb574a..fbbf3683db 100644 --- a/.pipelines/PowerShell-vPack-Official.yml +++ b/.pipelines/PowerShell-vPack-Official.yml @@ -1,9 +1,6 @@ trigger: none parameters: # parameters are shown up in ADO UI in a build queue time -- name: OfficialBuild - type: boolean - default: true - name: 'createVPack' displayName: 'Create and Submit VPack' type: boolean @@ -33,7 +30,7 @@ parameters: # parameters are shown up in ADO UI in a build queue time - Netlock default: "R1" -name: vPack_$(Build.SourceBranchName)_Prod.${{ parameters.OfficialBuild }}_Create.${{ parameters.createVPack }}_Name.${{ parameters.vPackName}}_$(date:yyyyMMdd).$(rev:rr) +name: vPack_$(Build.SourceBranchName)_Prod.true_Create.${{ parameters.createVPack }}_Name.${{ parameters.vPackName}}_$(date:yyyyMMdd).$(rev:rr) variables: - name: CDP_DEFINITION_BUILD_COUNT @@ -58,8 +55,6 @@ variables: value: ${{ parameters.ReleaseTagVar }} - group: Azure Blob variable group - group: certificate_logical_to_actual # used within signing task - - name: templateFile - value: ${{ iif ( parameters.OfficialBuild, 'v2/Microsoft.Official.yml@onebranchTemplates', 'v2/Microsoft.NonOfficial.yml@onebranchTemplates' ) }} - group: DotNetPrivateBuildAccess - group: certificate_logical_to_actual - name: netiso @@ -75,7 +70,7 @@ resources: ref: refs/heads/main extends: - template: ${{ variables.templateFile }} + template: v2/Microsoft.Official.yml@onebranchTemplates parameters: platform: name: 'windows_undocked' # windows undocked From 2c14e1147014a2be863e7b48d1d155b2778997d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:41:33 -0700 Subject: [PATCH 103/112] Bump github/codeql-action from 4.32.4 to 4.32.6 (#26942) --- .github/workflows/analyze-reusable.yml | 4 ++-- .github/workflows/scorecards.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/analyze-reusable.yml b/.github/workflows/analyze-reusable.yml index fbbb5c31ef..21966cb855 100644 --- a/.github/workflows/analyze-reusable.yml +++ b/.github/workflows/analyze-reusable.yml @@ -47,7 +47,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5 + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v3.29.5 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -74,4 +74,4 @@ jobs: shell: pwsh - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5 + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v3.29.5 diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index c51e85b944..ee70756298 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -67,6 +67,6 @@ jobs: # Upload the results to GitHub's code scanning dashboard. - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v3.29.5 + uses: github/codeql-action/upload-sarif@0d579ffd059c29b07949a3cce3983f0780820c98 # v3.29.5 with: sarif_file: results.sarif From 1ee3d7116d20143cbc06cbb71c56390fb432c7d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:41:56 -0700 Subject: [PATCH 104/112] Bump actions/dependency-review-action from 4.8.3 to 4.9.0 (#26938) --- .github/workflows/dependency-review.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 6bace64885..79f45e9eb4 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -19,4 +19,4 @@ jobs: - name: 'Checkout Repository' uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - name: 'Dependency Review' - uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 + uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0 From e170b7210a375b2ed75f3d2e5e41772858b97679 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Mar 2026 14:13:30 -0700 Subject: [PATCH 105/112] Bump actions/upload-artifact from 6 to 7 (#26914) --- .github/workflows/macos-ci.yml | 2 +- .github/workflows/scorecards.yml | 2 +- .github/workflows/windows-packaging-reusable.yml | 2 +- .github/workflows/xunit-tests.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index c7717a2673..5f363e0c26 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -229,7 +229,7 @@ jobs: testResultsFolder: "${{ runner.workspace }}/testResults" - name: Upload package artifact if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: macos-package path: "*.pkg" diff --git a/.github/workflows/scorecards.yml b/.github/workflows/scorecards.yml index ee70756298..7e868e10db 100644 --- a/.github/workflows/scorecards.yml +++ b/.github/workflows/scorecards.yml @@ -59,7 +59,7 @@ jobs: # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: SARIF file path: results.sarif diff --git a/.github/workflows/windows-packaging-reusable.yml b/.github/workflows/windows-packaging-reusable.yml index bb4873adeb..55715c42a4 100644 --- a/.github/workflows/windows-packaging-reusable.yml +++ b/.github/workflows/windows-packaging-reusable.yml @@ -81,7 +81,7 @@ jobs: - name: Upload Build Artifacts if: always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: windows-packaging-${{ matrix.architecture }}-${{ matrix.channel }} path: | diff --git a/.github/workflows/xunit-tests.yml b/.github/workflows/xunit-tests.yml index 5d225446cb..a1c86bea70 100644 --- a/.github/workflows/xunit-tests.yml +++ b/.github/workflows/xunit-tests.yml @@ -46,7 +46,7 @@ jobs: Write-Host "Completed xUnit test run." - name: Upload xUnit results - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 if: always() with: name: ${{ inputs.test_results_artifact_name }} From 312291762e0bfc2467481aac4f22b48df02107ed Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:16:18 -0400 Subject: [PATCH 106/112] Fix ConvertFrom-ClearlyDefinedCoordinates to handle API object coordinates (#26893) 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 Co-authored-by: PowerShell Team Bot <69177312+pwshBot@users.noreply.github.com> Co-authored-by: Dongbo Wang --- DotnetRuntimeMetadata.json | 2 +- ...crosoft.PowerShell.Commands.Utility.csproj | 2 +- .../Microsoft.PowerShell.SDK.csproj | 2 +- .../ResultsComparer/ResultsComparer.csproj | 2 +- test/tools/TestService/TestService.csproj | 2 +- test/xUnit/xUnit.tests.csproj | 2 +- tools/cgmanifest/main/cgmanifest.json | 8 +++--- .../src/ClearlyDefined/ClearlyDefined.psm1 | 27 +++++++++++++------ 8 files changed, 29 insertions(+), 18 deletions(-) diff --git a/DotnetRuntimeMetadata.json b/DotnetRuntimeMetadata.json index 1ae71bf073..7c4a219146 100644 --- a/DotnetRuntimeMetadata.json +++ b/DotnetRuntimeMetadata.json @@ -4,7 +4,7 @@ "quality": "daily", "qualityFallback": "preview", "packageVersionPattern": "9.0.0-preview.6", - "sdkImageVersion": "10.0.101", + "sdkImageVersion": "11.0.100-preview.1.26104.118", "nextChannel": "9.0.0-preview.7", "azureFeed": "", "sdkImageOverride": "" diff --git a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj index 56fa91bcf6..b2fae8b21e 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj +++ b/src/Microsoft.PowerShell.Commands.Utility/Microsoft.PowerShell.Commands.Utility.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj index daa29bd01f..5f680de58d 100644 --- a/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj +++ b/src/Microsoft.PowerShell.SDK/Microsoft.PowerShell.SDK.csproj @@ -19,7 +19,7 @@ - + diff --git a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj index 90143a9f48..58ea02cf1b 100644 --- a/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj +++ b/test/perf/dotnet-tools/ResultsComparer/ResultsComparer.csproj @@ -10,6 +10,6 @@ - +
diff --git a/test/tools/TestService/TestService.csproj b/test/tools/TestService/TestService.csproj index 5d5f1a9e4f..7580229ce9 100644 --- a/test/tools/TestService/TestService.csproj +++ b/test/tools/TestService/TestService.csproj @@ -16,7 +16,7 @@ - + diff --git a/test/xUnit/xUnit.tests.csproj b/test/xUnit/xUnit.tests.csproj index 6ce2ee32ce..4cf097c795 100644 --- a/test/xUnit/xUnit.tests.csproj +++ b/test/xUnit/xUnit.tests.csproj @@ -30,7 +30,7 @@ all
- + diff --git a/tools/cgmanifest/main/cgmanifest.json b/tools/cgmanifest/main/cgmanifest.json index a0746028a5..4d856723ea 100644 --- a/tools/cgmanifest/main/cgmanifest.json +++ b/tools/cgmanifest/main/cgmanifest.json @@ -1,4 +1,5 @@ { + "$schema": "https://json.schemastore.org/component-detection-manifest.json", "Registrations": [ { "Component": { @@ -65,7 +66,7 @@ "Type": "nuget", "Nuget": { "Name": "Markdig.Signed", - "Version": "0.45.0" + "Version": "1.0.0" } }, "DevelopmentDependency": false @@ -495,7 +496,7 @@ "Type": "nuget", "Nuget": { "Name": "System.Data.SqlClient", - "Version": "4.9.0" + "Version": "4.9.1" } }, "DevelopmentDependency": false @@ -750,6 +751,5 @@ }, "DevelopmentDependency": false } - ], - "$schema": "https://json.schemastore.org/component-detection-manifest.json" + ] } diff --git a/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 b/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 index 4e3d375dc5..88fc1f7cab 100644 --- a/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 +++ b/tools/clearlyDefined/src/ClearlyDefined/ClearlyDefined.psm1 @@ -40,19 +40,30 @@ function ConvertFrom-ClearlyDefinedCoordinates { [CmdletBinding()] param( [parameter(mandatory = $true, ValueFromPipeline = $true)] - [string] + [object] $Coordinates ) Begin {} Process { - $parts = $Coordinates.Split('/') - [PSCustomObject]@{ - type = $parts[0] - provider = $parts[1] - namespace = $parts[2] - name = $parts[3] - revision = $parts[4] + if ($Coordinates -is [string]) { + $parts = $Coordinates.Split('/') + [PSCustomObject]@{ + type = $parts[0] + provider = $parts[1] + namespace = $parts[2] + name = $parts[3] + revision = $parts[4] + } + } else { + # Coordinates is already an object (e.g., from ClearlyDefined API response) + [PSCustomObject]@{ + type = $Coordinates.type + provider = $Coordinates.provider + namespace = $Coordinates.namespace + name = $Coordinates.name + revision = $Coordinates.revision + } } } End {} From a76568c22d0a6015d304c8773e2fd53a56907c53 Mon Sep 17 00:00:00 2001 From: Justin Chung <124807742+jshigetomi@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:55:00 -0500 Subject: [PATCH 107/112] Add 7.4.14 changelog (#26998) Co-authored-by: Justin Chung Co-authored-by: Travis Plunk Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- CHANGELOG/7.4.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/CHANGELOG/7.4.md b/CHANGELOG/7.4.md index 89a3f64e37..f999c20315 100644 --- a/CHANGELOG/7.4.md +++ b/CHANGELOG/7.4.md @@ -1,5 +1,82 @@ # 7.4 Changelog +## [7.4.14] + +### General Cmdlet Updates and Fixes + +- Fix `PSMethodInvocationConstraints.GetHashCode` method (#26959) + +### Tools + +- Add merge conflict marker detection to `linux-ci` workflow and refactor existing actions to use reusable `get-changed-files` action (#26362) +- Add reusable `get-changed-files` action and refactor existing actions (#26361) +- Refactor analyze job to reusable workflow and enable on Windows CI (#26342) + +### Tests + +- Skip the flaky `Update-Help` test for the `PackageManagement` module (#26871) +- Fix `$PSDefaultParameterValues` leak causing tests to skip unexpectedly (#26869) +- Add GitHub Actions annotations for Pester test failures (#26800) +- Mark flaky `Update-Help` web tests as pending to unblock CI (#26805) +- Update the `Update-Help` tests to use `-Force` to remove read-only files (#26786) +- Fix merge conflict checker for empty file lists and filter `*.cs` files (#26387) +- Add markdown link verification for PRs (#26340) + +### Build and Packaging Improvements + +
+ + + +

Update .NET SDK to 8.0.419

+ +
+ +
    +
  • Update MaxVisitCount and MaxHashtableKeyCount if visitor safe value context indicates SkipLimitCheck is true (Internal 38882)
  • +
  • Hardcode Official templates (#26962)
  • +
  • Split TPN manifest and Component Governance manifest (#26961)
  • +
  • Correct the package name for .deb and .rpm packages (#26960)
  • +
  • Bring over all changes for MSIX packaging template (#26933)
  • +
  • .NET Resolution and Store Publishing Updates (#26930)
  • +
  • Update Application Insights package version to 2.23.0 (#26883)
  • +
  • Update metadata.json to update the Latest attribute with a better name (#26872)
  • +
  • Update Get-ChangeLog to handle backport PRs correctly (#26870)
  • +
  • Remove unused runCodesignValidationInjection variable from pipeline templates (#26868)
  • +
  • Refactor: Centralize xUnit tests into reusable workflow and remove legacy verification (#26864)
  • +
  • Fix buildinfo.json uploading for preview, LTS, and stable releases (#26863)
  • +
  • Fix macOS preview package identifier detection to use version string (#26774)
  • +
  • Update the macOS package name for preview releases to match the previous pattern (#26435)
  • +
  • Fix condition syntax for StoreBroker package tasks in MSIX pipeline (#26434)
  • +
  • Fix template path for rebuild branch check in package.yml (#26433)
  • +
  • Add rebuild branch support with conditional MSIX signing (#26418)
  • +
  • Move package validation to package pipeline (#26417)
  • +
  • Backport Store publishing improvements (#26401)
  • +
  • Fix path to metadata.json in channel selection script (#26399)
  • +
  • Optimize/split Windows package signing (#26413)
  • +
  • Improve ADO package build and validation across platforms (#26405)
  • +
  • Separate Store Automation Service Endpoints, Resolve AppID (#26396)
  • +
  • Fix the task name to not use the pre-release task (#26395)
  • +
  • Remove usage of fpm for DEB package generation (#26382)
  • +
  • Replace fpm with native macOS packaging tools (pkgbuild/productbuild) (#26344)
  • +
  • Replace fpm with native rpmbuild for RPM package generation (#26337)
  • +
  • Add log grouping to build.psm1 for collapsible GitHub Actions logs (#26363)
  • +
  • Convert Azure DevOps Linux Packaging pipeline to GitHub Actions workflow (#26336)
  • +
  • Integrate Windows packaging into windows-ci workflow using reusable workflow (#26335)
  • +
  • Add network isolation policy parameter to vPack pipeline (#26339)
  • +
  • GitHub Workflow cleanup (#26334)
  • +
  • Add build to vPack Pipeline (#25980)
  • +
  • Update vPack name (#26222)
  • +
+ +
+ +### Documentation and Help Content + +- Update Third Party Notices (#26892) + +[7.4.14]: https://github.com/PowerShell/PowerShell/compare/v7.4.13...v7.4.14 + ## [7.4.13] ### Build and Packaging Improvements From 7498139bae4a8ef40a6df9c845a4e9867dbcb22d Mon Sep 17 00:00:00 2001 From: Anam Navied Date: Wed, 11 Mar 2026 16:39:10 -0400 Subject: [PATCH 108/112] Update `Microsoft.PowerShell.PSResourceGet` version to 1.2.0 (#27003) --- src/Modules/PSGalleryModules.csproj | 2 +- tools/packaging/boms/windows.json | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Modules/PSGalleryModules.csproj b/src/Modules/PSGalleryModules.csproj index 9136df5c7b..3903b164b0 100644 --- a/src/Modules/PSGalleryModules.csproj +++ b/src/Modules/PSGalleryModules.csproj @@ -13,7 +13,7 @@ - + diff --git a/tools/packaging/boms/windows.json b/tools/packaging/boms/windows.json index 8248b3d03a..8d900902bc 100644 --- a/tools/packaging/boms/windows.json +++ b/tools/packaging/boms/windows.json @@ -1216,6 +1216,11 @@ "FileType": "NonProduct", "Architecture": null }, + { + "Pattern": "Modules\\Microsoft.PowerShell.PSResourceGet\\.signature.p7s", + "FileType": "NonProduct", + "Architecture": null + }, { "Pattern": "Modules\\Microsoft.PowerShell.PSResourceGet\\Microsoft.PowerShell.PSResourceGet.pdb", "FileType": "NonProduct", From fffe6094cf6d19593e77c803259c6d8a6ce0f18a Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 12 Mar 2026 11:37:25 -0700 Subject: [PATCH 109/112] Update changelog for release v7.5.5 (#27014) --- CHANGELOG/7.5.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/CHANGELOG/7.5.md b/CHANGELOG/7.5.md index 80b3ee0a8a..d908532ff8 100644 --- a/CHANGELOG/7.5.md +++ b/CHANGELOG/7.5.md @@ -1,5 +1,90 @@ # 7.5 Changelog +## [7.5.5] + +### Engine Updates and Fixes + +- Fix up `SSHConnectionInfo` ssh PATH checks (#26165) (Thanks @jborean93!) + +### General Cmdlet Updates and Fixes + +- Close pipe client handles after creating the child ssh process (#26822) +- Fix the progress preference variable in script cmdlets (#26791) (Thanks @cmkb3!) + +### Tools + +- Add merge conflict marker detection to `linux-ci` workflow and refactor existing actions to use reusable `get-changed-files` action (#26812) +- Add reusable `get-changed-files` action and refactor existing actions (#26811) +- Create GitHub Copilot setup workflow (#26807) +- Refactor analyze job to reusable workflow and enable on Windows CI (#26799) + +### Tests + +- Mark flaky `Update-Help` web tests as pending to unblock CI (#26837) +- Add GitHub Actions annotations for Pester test failures (#26836) +- Fix `$PSDefaultParameterValues` leak causing tests to skip unexpectedly (#26823) +- Fix merge conflict checker for empty file lists and filter `*.cs` files (#26813) +- Update the `Update-Help` tests to use `-Force` to remove read-only files (#26788) +- Add markdown link verification for PRs (#26407) + +### Build and Packaging Improvements + +
+ + +

Update to .NET SDK 9.0.312

+

We thank the following contributors!

+

@kasperk81, @RichardSlater

+ +
+ +
    +
  • Revert change to module name ThreadJob (#26997)
  • +
  • Update branch for release (#26990)
  • +
  • Fix ConvertFrom-ClearlyDefinedCoordinates to handle API object coordinates (#26987)
  • +
  • Update CGManifests (#26981)
  • +
  • Hardcode Official templates (#26968)
  • +
  • Split TPN manifest and Component Governance manifest (#26967)
  • +
  • Fix a preview detection test for the packaging script (#26966)
  • +
  • Correct the package name for .deb and .rpm packages (#26964)
  • +
  • Bring Release Changes from v7.6.0-preview.6 (#26963)
  • +
  • Merge the v7.6.0-preview.5 release branch back to master (#26958)
  • +
  • Fix macOS preview package identifier detection to use version string (#26835)
  • +
  • Update metadata.json to update the Latest attribute with a better name (#26826)
  • +
  • Remove unused runCodesignValidationInjection variable from pipeline templates (#26825)
  • +
  • Update Get-ChangeLog to handle backport PRs correctly (#26824)
  • +
  • Mirror .NET/runtime ICU version range in PowerShell (#26821) (Thanks @kasperk81!)
  • +
  • Update the macos package name for preview releases to match the previous pattern (#26820)
  • +
  • Fix condition syntax for StoreBroker package tasks in MSIX pipeline (#26819)
  • +
  • Fix template path for rebuild branch check in package.yml (#26818)
  • +
  • Add rebuild branch support with conditional MSIX signing (#26817)
  • +
  • Move package validation to package pipeline (#26816)
  • +
  • Optimize/split windows package signing (#26815)
  • +
  • Improve ADO package build and validation across platforms (#26814)
  • +
  • Add log grouping to build.psm1 for collapsible GitHub Actions logs (#26810)
  • +
  • Remove usage of fpm for DEB package generation (#26809)
  • +
  • Replace fpm with native macOS packaging tools (pkgbuild/productbuild) (#26801)
  • +
  • Fix build to only enable ready-to-run for the Release configuration (#26798)
  • +
  • Fix R2R for fxdependent packaging (#26797)
  • +
  • Refactor: Centralize xUnit tests into reusable workflow and remove legacy verification (#26794)
  • +
  • Replace fpm with native rpmbuild for RPM package generation (#26793)
  • +
  • Add libicu76 dependency to support Debian 13 (#26792) (Thanks @RichardSlater!)
  • +
  • Specify .NET search by build type (#26408)
  • +
  • Fix buildinfo.json uploading for preview, LTS, and stable releases (#26773)
  • +
  • Fix path to metadata.json in channel selection script (#26400)
  • +
  • Separate store automation service endpoints and resolve AppID (#26266)
  • +
  • Update a few packages to use the right version corresponding to .NET 9 (#26671)
  • +
  • Add network isolation policy parameter to vPack pipeline (#26393)
  • +
  • Convert Azure DevOps Linux Packaging pipeline to GitHub Actions workflow (#26391)
  • +
  • Integrate Windows packaging into windows-ci workflow using reusable workflow (#26390)
  • +
  • GitHub Workflow cleanup (#26389)
  • +
  • Update vPack name (#26221)
  • +
+ +
+ +[7.5.5]: https://github.com/PowerShell/PowerShell/compare/v7.5.4...v7.5.5 + ## [7.5.4] ### Build and Packaging Improvements From 1f8bbe1e5332bb8fe1b39c81c366ddadbf9dd1a2 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 12 Mar 2026 12:34:12 -0700 Subject: [PATCH 110/112] Fix the container image for vPack, MSIX vPack and Package pipelines (#27015) --- .pipelines/MSIXBundle-vPack-Official.yml | 5 ++++- .pipelines/PowerShell-Coordinated_Packages-Official.yml | 3 ++- .pipelines/PowerShell-vPack-Official.yml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.pipelines/MSIXBundle-vPack-Official.yml b/.pipelines/MSIXBundle-vPack-Official.yml index 997b7c458b..876da9c2af 100644 --- a/.pipelines/MSIXBundle-vPack-Official.yml +++ b/.pipelines/MSIXBundle-vPack-Official.yml @@ -22,7 +22,7 @@ variables: BuildSolution: $(Build.SourcesDirectory)\dirs.proj ReleaseTagVar: ${{ parameters.ReleaseTagVar }} BuildConfiguration: Release - WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2019/vse2022:latest' + WindowsContainerImage: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' Codeql.Enabled: false # pipeline is not building artifacts; it repackages existing artifacts into a vpack DOTNET_CLI_TELEMETRY_OPTOUT: 1 POWERSHELL_TELEMETRY_OPTOUT: 1 @@ -46,6 +46,9 @@ resources: extends: template: v2/Microsoft.Official.yml@onebranchTemplates parameters: + featureFlags: + WindowsHostVersion: + Version: 2022 platform: name: 'windows_undocked' # windows undocked diff --git a/.pipelines/PowerShell-Coordinated_Packages-Official.yml b/.pipelines/PowerShell-Coordinated_Packages-Official.yml index 380c9c5516..12460e2861 100644 --- a/.pipelines/PowerShell-Coordinated_Packages-Official.yml +++ b/.pipelines/PowerShell-Coordinated_Packages-Official.yml @@ -68,7 +68,7 @@ variables: - name: LinuxContainerImage value: mcr.microsoft.com/onebranch/azurelinux/build:3.0 - name: WindowsContainerImage - value: onebranch.azurecr.io/windows/ltsc2019/vse2022:latest + value: onebranch.azurecr.io/windows/ltsc2022/vse2022:latest - name: CDP_DEFINITION_BUILD_COUNT value: $[counter('', 0)] - name: ReleaseTagVar @@ -105,6 +105,7 @@ extends: LinuxHostVersion: Network: KS3 WindowsHostVersion: + Version: 2022 Network: KS3 incrementalSDLBinaryAnalysis: true globalSdl: diff --git a/.pipelines/PowerShell-vPack-Official.yml b/.pipelines/PowerShell-vPack-Official.yml index fbbf3683db..c49560bf2b 100644 --- a/.pipelines/PowerShell-vPack-Official.yml +++ b/.pipelines/PowerShell-vPack-Official.yml @@ -42,7 +42,7 @@ variables: - name: BuildConfiguration value: Release - name: WindowsContainerImage - value: 'onebranch.azurecr.io/windows/ltsc2019/vse2022:latest' + value: 'onebranch.azurecr.io/windows/ltsc2022/vse2022:latest' - name: Codeql.Enabled value: false # pipeline is not building artifacts; it repackages existing artifacts into a vpack - name: DOTNET_CLI_TELEMETRY_OPTOUT From 8c9c9e11872c8d98c2e7d7a1ed991fc7e1d07354 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 16 Mar 2026 12:40:26 -0700 Subject: [PATCH 111/112] Create LTS pkg and non-LTS pkg for macOS for LTS releases (#27039) --- .pipelines/templates/mac-package-build.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.pipelines/templates/mac-package-build.yml b/.pipelines/templates/mac-package-build.yml index def866173a..6585773c74 100644 --- a/.pipelines/templates/mac-package-build.yml +++ b/.pipelines/templates/mac-package-build.yml @@ -102,6 +102,10 @@ jobs: Restore-PSOptions -PSOptionsPath "$psoptionsPath" Get-PSOptions | Write-Verbose -Verbose + if (-not (Test-Path "$repoRoot/tools/metadata.json")) { + throw "metadata.json not found in $repoRoot/tools" + } + $metadata = Get-Content "$repoRoot/tools/metadata.json" -Raw | ConvertFrom-Json Write-Verbose -Verbose "metadata:" @@ -120,14 +124,19 @@ jobs: Write-Verbose -Verbose "LTS: $LTS" if ($LTS) { - Write-Verbose -Message "LTS Release: $LTS" + Write-Verbose -Message "LTS Release: $LTS" -Verbose } Start-PSBootstrap -Scenario Package $macosRuntime = "osx-$buildArch" - Start-PSPackage -Type osxpkg -SkipReleaseChecks -MacOSRuntime $macosRuntime -ReleaseTag $(ReleaseTagVar) -PackageBinPath $signedFilesPath -LTS:$LTS + Start-PSPackage -Type osxpkg -SkipReleaseChecks -MacOSRuntime $macosRuntime -ReleaseTag $(ReleaseTagVar) -PackageBinPath $signedFilesPath + + if ($LTS) { + Start-PSPackage -Type osxpkg -SkipReleaseChecks -MacOSRuntime $macosRuntime -ReleaseTag $(ReleaseTagVar) -PackageBinPath $signedFilesPath -LTS + } + $pkgNameFilter = "powershell-*$macosRuntime.pkg" Write-Verbose -Verbose "Looking for pkg packages with filter: $pkgNameFilter in '$(Pipeline.Workspace)' to upload..." $pkgPath = Get-ChildItem -Path $(Pipeline.Workspace) -Filter $pkgNameFilter -Recurse -File From e053a339a1109ed16d36d77b9a47ab35ad4d017d Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Tue, 17 Mar 2026 14:47:59 -0700 Subject: [PATCH 112/112] Create Linux LTS deb/rpm packages for LTS releases (#27049) --- .pipelines/templates/linux-package-build.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.pipelines/templates/linux-package-build.yml b/.pipelines/templates/linux-package-build.yml index ea49b9fea5..bb7ed723f4 100644 --- a/.pipelines/templates/linux-package-build.yml +++ b/.pipelines/templates/linux-package-build.yml @@ -155,10 +155,6 @@ jobs: Write-Verbose -Verbose "LTS: $LTS" - if ($LTS) { - Write-Verbose -Message "LTS Release: $LTS" - } - if (-not (Test-Path $(ob_outputDirectory))) { New-Item -ItemType Directory -Path $(ob_outputDirectory) -Force } @@ -168,6 +164,11 @@ jobs: Start-PSPackage -Type $packageType -ReleaseTag $(ReleaseTagVar) -PackageBinPath $signedFilesPath + if ($LTS) { + Write-Verbose -Message "LTS Release: $LTS" -Verbose + Start-PSPackage -Type $packageType -ReleaseTag $(ReleaseTagVar) -PackageBinPath $signedFilesPath -LTS + } + $vstsCommandString = "vso[task.setvariable variable=PackageFilter]$pkgFilter" Write-Host ("sending " + $vstsCommandString) Write-Host "##$vstsCommandString"