From 991b1a6be70eea2e84283503df569374c6180a2c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:29:53 +0000 Subject: [PATCH 1/3] Initial plan From cb96549442d92d5be13c0e006584d887dfe71406 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:56:29 +0000 Subject: [PATCH 2/3] Fix PSSA indentation, add per-step JSON warnings, beautify PSSA tool output Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../ConvertTo-IdlePlanExportObject.ps1 | 17 ++ .../Public/New-IdleExchangeOnlineProvider.ps1 | 282 +++++++++--------- tests/Core/Export-IdlePlan.Tests.ps1 | 3 + .../plan-export/expected/plan-export.json | 3 +- tools/Invoke-IdleScriptAnalyzer.ps1 | 119 +++++++- 5 files changed, 273 insertions(+), 151 deletions(-) diff --git a/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 b/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 index e524ddbe..a17419ea 100644 --- a/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 +++ b/src/IdLE.Core/Private/ConvertTo-IdlePlanExportObject.ps1 @@ -276,6 +276,23 @@ function ConvertTo-IdlePlanExportObject { $stepMap.inputs = $redactedInputs $stepMap.expectedState = $redactedExpectedState + # Per-step planning warnings (e.g. unresolved precondition context paths). + $rawStepWarnings = Get-FirstPropertyValue -Object $step -Names @('Warnings', 'PlanningWarnings', 'StepWarnings') + $stepWarningList = @() + foreach ($sw in @($rawStepWarnings)) { + if ($null -eq $sw) { continue } + + $stepWarningMap = New-OrderedMap + $stepWarningMap.code = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Code', 'code')) + $stepWarningMap.type = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Type', 'type')) + $stepWarningMap.source = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Source', 'source')) + $stepWarningMap.paths = Get-FirstPropertyValue -Object $sw -Names @('Paths', 'paths') + $stepWarningMap.message = ConvertTo-NullIfEmptyString -Value (Get-FirstPropertyValue -Object $sw -Names @('Message', 'message')) + + $stepWarningList += $stepWarningMap + } + $stepMap.warnings = $stepWarningList + $stepList += $stepMap } diff --git a/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 b/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 index 281b29a1..2a6fb3cf 100644 --- a/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 +++ b/src/IdLE.Provider.ExchangeOnline/Public/New-IdleExchangeOnlineProvider.ps1 @@ -456,173 +456,171 @@ function New-IdleExchangeOnlineProvider { $currentPerms = $this.Adapter.GetMailboxPermissions($mailboxSmtp, $accessToken) # Normalize current delegates (case-insensitive) - $currentFullAccessUsers = @($currentPerms | - Where-Object { $_.AccessRight -eq 'FullAccess' -and -not $_.IsInherited } | - ForEach-Object { $_.User.ToLowerInvariant() }) - - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Evaluated', - "FullAccess current state evaluated for '$mailboxSmtp'", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; CurrentUsers = $currentFullAccessUsers } - ) - } + $filteredFullAccessPerms = $currentPerms | Where-Object { $_.AccessRight -eq 'FullAccess' -and -not $_.IsInherited } + $currentFullAccessUsers = @($filteredFullAccessPerms | ForEach-Object { $_.User.ToLowerInvariant() }) + + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Evaluated', + "FullAccess current state evaluated for '$mailboxSmtp'", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; CurrentUsers = $currentFullAccessUsers } + ) + } + + foreach ($entry in $desiredFullAccess) { + $userLower = ([string]$entry.AssignedUser).ToLowerInvariant() + $isPresent = $currentFullAccessUsers -contains $userLower - foreach ($entry in $desiredFullAccess) { - $userLower = ([string]$entry.AssignedUser).ToLowerInvariant() - $isPresent = $currentFullAccessUsers -contains $userLower - - if ($entry.Ensure -eq 'Present' -and -not $isPresent) { - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Applying', - "Granting FullAccess on '$mailboxSmtp' to '$($entry.AssignedUser)'", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; User = [string]$entry.AssignedUser; Action = 'Add' } - ) - } - $this.Adapter.AddMailboxPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) - $changed = $true + if ($entry.Ensure -eq 'Present' -and -not $isPresent) { + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Applying', + "Granting FullAccess on '$mailboxSmtp' to '$($entry.AssignedUser)'", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; User = [string]$entry.AssignedUser; Action = 'Add' } + ) } - elseif ($entry.Ensure -eq 'Absent' -and $isPresent) { - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Applying', - "Revoking FullAccess on '$mailboxSmtp' from '$($entry.AssignedUser)'", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; User = [string]$entry.AssignedUser; Action = 'Remove' } - ) - } - $this.Adapter.RemoveMailboxPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) - $changed = $true + $this.Adapter.AddMailboxPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) + $changed = $true + } + elseif ($entry.Ensure -eq 'Absent' -and $isPresent) { + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Applying', + "Revoking FullAccess on '$mailboxSmtp' from '$($entry.AssignedUser)'", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Right = 'FullAccess'; User = [string]$entry.AssignedUser; Action = 'Remove' } + ) } + $this.Adapter.RemoveMailboxPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) + $changed = $true } } + } - # --- SendAs --- - $desiredSendAs = @($Permissions | Where-Object { $_.Right -eq 'SendAs' }) - if ($desiredSendAs.Count -gt 0) { - $currentRecipientPerms = $this.Adapter.GetRecipientPermissions($mailboxSmtp, $accessToken) + # --- SendAs --- + $desiredSendAs = @($Permissions | Where-Object { $_.Right -eq 'SendAs' }) + if ($desiredSendAs.Count -gt 0) { + $currentRecipientPerms = $this.Adapter.GetRecipientPermissions($mailboxSmtp, $accessToken) + + $filteredSendAsPerms = $currentRecipientPerms | Where-Object { $_.AccessRight -match 'SendAs' -and -not $_.IsInherited } + $currentSendAsTrustees = @($filteredSendAsPerms | ForEach-Object { $_.Trustee.ToLowerInvariant() }) + + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Evaluated', + "SendAs current state evaluated for '$mailboxSmtp'", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; CurrentUsers = $currentSendAsTrustees } + ) + } - $currentSendAsTrustees = @($currentRecipientPerms | - Where-Object { $_.AccessRight -match 'SendAs' -and -not $_.IsInherited } | - ForEach-Object { $_.Trustee.ToLowerInvariant() }) + foreach ($entry in $desiredSendAs) { + $trusteeLower = ([string]$entry.AssignedUser).ToLowerInvariant() + $isPresent = $currentSendAsTrustees -contains $trusteeLower + if ($entry.Ensure -eq 'Present' -and -not $isPresent) { if ($hasEventSink) { $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Evaluated', - "SendAs current state evaluated for '$mailboxSmtp'", + 'Provider.ExchangeOnline.Permissions.Applying', + "Granting SendAs on '$mailboxSmtp' to '$($entry.AssignedUser)'", 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; CurrentUsers = $currentSendAsTrustees } + @{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; User = [string]$entry.AssignedUser; Action = 'Add' } ) } - - foreach ($entry in $desiredSendAs) { - $trusteeLower = ([string]$entry.AssignedUser).ToLowerInvariant() - $isPresent = $currentSendAsTrustees -contains $trusteeLower - - if ($entry.Ensure -eq 'Present' -and -not $isPresent) { - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Applying', - "Granting SendAs on '$mailboxSmtp' to '$($entry.AssignedUser)'", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; User = [string]$entry.AssignedUser; Action = 'Add' } - ) - } - $this.Adapter.AddRecipientPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) - $changed = $true - } - elseif ($entry.Ensure -eq 'Absent' -and $isPresent) { - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Applying', - "Revoking SendAs on '$mailboxSmtp' from '$($entry.AssignedUser)'", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; User = [string]$entry.AssignedUser; Action = 'Remove' } - ) - } - $this.Adapter.RemoveRecipientPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) - $changed = $true - } + $this.Adapter.AddRecipientPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) + $changed = $true + } + elseif ($entry.Ensure -eq 'Absent' -and $isPresent) { + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Applying', + "Revoking SendAs on '$mailboxSmtp' from '$($entry.AssignedUser)'", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Right = 'SendAs'; User = [string]$entry.AssignedUser; Action = 'Remove' } + ) } + $this.Adapter.RemoveRecipientPermission($mailboxSmtp, [string]$entry.AssignedUser, $accessToken) + $changed = $true } + } + } - # --- SendOnBehalf --- - $desiredSendOnBehalf = @($Permissions | Where-Object { $_.Right -eq 'SendOnBehalf' }) - if ($desiredSendOnBehalf.Count -gt 0) { - $currentDelegates = $this.Adapter.GetMailboxSendOnBehalf($mailboxSmtp, $accessToken) - $currentDelegatesLower = @($currentDelegates | ForEach-Object { $_.ToLowerInvariant() }) + # --- SendOnBehalf --- + $desiredSendOnBehalf = @($Permissions | Where-Object { $_.Right -eq 'SendOnBehalf' }) + if ($desiredSendOnBehalf.Count -gt 0) { + $currentDelegates = $this.Adapter.GetMailboxSendOnBehalf($mailboxSmtp, $accessToken) + $currentDelegatesLower = @($currentDelegates | ForEach-Object { $_.ToLowerInvariant() }) + + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Evaluated', + "SendOnBehalf current state evaluated for '$mailboxSmtp'", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; CurrentUsers = $currentDelegatesLower } + ) + } + + # Compute desired final list based on Present/Absent entries + $updatedDelegates = [System.Collections.Generic.List[string]]::new() + foreach ($d in $currentDelegates) { $updatedDelegates.Add($d) } + + $sobChanged = $false + foreach ($entry in $desiredSendOnBehalf) { + $userLower = ([string]$entry.AssignedUser).ToLowerInvariant() + $isPresent = $currentDelegatesLower -contains $userLower + if ($entry.Ensure -eq 'Present' -and -not $isPresent) { if ($hasEventSink) { $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Evaluated', - "SendOnBehalf current state evaluated for '$mailboxSmtp'", + 'Provider.ExchangeOnline.Permissions.Applying', + "Granting SendOnBehalf on '$mailboxSmtp' to '$($entry.AssignedUser)'", 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; CurrentUsers = $currentDelegatesLower } + @{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; User = [string]$entry.AssignedUser; Action = 'Add' } ) } - - # Compute desired final list based on Present/Absent entries - $updatedDelegates = [System.Collections.Generic.List[string]]::new() - foreach ($d in $currentDelegates) { $updatedDelegates.Add($d) } - - $sobChanged = $false - foreach ($entry in $desiredSendOnBehalf) { - $userLower = ([string]$entry.AssignedUser).ToLowerInvariant() - $isPresent = $currentDelegatesLower -contains $userLower - - if ($entry.Ensure -eq 'Present' -and -not $isPresent) { - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Applying', - "Granting SendOnBehalf on '$mailboxSmtp' to '$($entry.AssignedUser)'", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; User = [string]$entry.AssignedUser; Action = 'Add' } - ) - } - $updatedDelegates.Add([string]$entry.AssignedUser) - $sobChanged = $true - } - elseif ($entry.Ensure -eq 'Absent' -and $isPresent) { - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Applying', - "Revoking SendOnBehalf on '$mailboxSmtp' from '$($entry.AssignedUser)'", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; User = [string]$entry.AssignedUser; Action = 'Remove' } - ) - } - # Remove case-insensitively - $toRemove = $updatedDelegates | Where-Object { $_.ToLowerInvariant() -eq $userLower } - foreach ($r in @($toRemove)) { $updatedDelegates.Remove($r) | Out-Null } - $sobChanged = $true - } - } - - if ($sobChanged) { - $this.Adapter.SetMailboxSendOnBehalf($mailboxSmtp, [string[]]$updatedDelegates, $accessToken) - $changed = $true + $updatedDelegates.Add([string]$entry.AssignedUser) + $sobChanged = $true + } + elseif ($entry.Ensure -eq 'Absent' -and $isPresent) { + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Applying', + "Revoking SendOnBehalf on '$mailboxSmtp' from '$($entry.AssignedUser)'", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Right = 'SendOnBehalf'; User = [string]$entry.AssignedUser; Action = 'Remove' } + ) } + # Remove case-insensitively + $toRemove = $updatedDelegates | Where-Object { $_.ToLowerInvariant() -eq $userLower } + foreach ($r in @($toRemove)) { $updatedDelegates.Remove($r) | Out-Null } + $sobChanged = $true } + } - if ($hasEventSink) { - $null = $this.EventSink.WriteEvent( - 'Provider.ExchangeOnline.Permissions.Result', - "EnsureMailboxPermissions completed for '$mailboxSmtp': Changed=$changed", - 'EnsureMailboxPermissions', - @{ MailboxSmtp = $mailboxSmtp; Changed = $changed } - ) - } + if ($sobChanged) { + $this.Adapter.SetMailboxSendOnBehalf($mailboxSmtp, [string[]]$updatedDelegates, $accessToken) + $changed = $true + } + } - return [pscustomobject]@{ - PSTypeName = 'IdLE.ProviderResult' - Operation = 'EnsureMailboxPermissions' - IdentityKey = $mailboxSmtp - Changed = $changed - } - } -Force + if ($hasEventSink) { + $null = $this.EventSink.WriteEvent( + 'Provider.ExchangeOnline.Permissions.Result', + "EnsureMailboxPermissions completed for '$mailboxSmtp': Changed=$changed", + 'EnsureMailboxPermissions', + @{ MailboxSmtp = $mailboxSmtp; Changed = $changed } + ) + } - return $provider + return [pscustomobject]@{ + PSTypeName = 'IdLE.ProviderResult' + Operation = 'EnsureMailboxPermissions' + IdentityKey = $mailboxSmtp + Changed = $changed } + } -Force + + return $provider +} diff --git a/tests/Core/Export-IdlePlan.Tests.ps1 b/tests/Core/Export-IdlePlan.Tests.ps1 index cc38bebc..00ff40cd 100644 --- a/tests/Core/Export-IdlePlan.Tests.ps1 +++ b/tests/Core/Export-IdlePlan.Tests.ps1 @@ -121,6 +121,9 @@ Describe 'Export-IdlePlan' { @($json.plan.warnings).Count | Should -BeGreaterThan 0 $json.plan.warnings[0].code | Should -Be 'PreconditionContextPathUnresolvedAtPlan' $json.plan.warnings[0].step | Should -Be 'Check Context' + + @($json.plan.steps[0].warnings).Count | Should -BeGreaterThan 0 + $json.plan.steps[0].warnings[0].code | Should -Be 'PreconditionContextPathUnresolvedAtPlan' } } Context 'Contract invariants' { diff --git a/tests/fixtures/plan-export/expected/plan-export.json b/tests/fixtures/plan-export/expected/plan-export.json index adbf1042..8c6a5095 100644 --- a/tests/fixtures/plan-export/expected/plan-export.json +++ b/tests/fixtures/plan-export/expected/plan-export.json @@ -33,7 +33,8 @@ "inputs": { "mailboxType": "User" }, - "expectedState": null + "expectedState": null, + "warnings": [] } ], "warnings": [] diff --git a/tools/Invoke-IdleScriptAnalyzer.ps1 b/tools/Invoke-IdleScriptAnalyzer.ps1 index b066495f..bf6ec6b1 100644 --- a/tools/Invoke-IdleScriptAnalyzer.ps1 +++ b/tools/Invoke-IdleScriptAnalyzer.ps1 @@ -179,13 +179,112 @@ function Initialize-Module { throw "Module '$Name' ($RequiredVersion) is required, but Install-Module is not available. Install the module manually and retry." } - Write-Host "Installing module '$Name' ($RequiredVersion) in CurrentUser scope..." + Write-Host " Installing module '$Name' ($RequiredVersion) in CurrentUser scope..." -ForegroundColor DarkGray Install-Module -Name $Name -Scope CurrentUser -Force -RequiredVersion $RequiredVersion -AllowClobber | Out-Null } Import-Module -Name $Name -RequiredVersion $RequiredVersion -Force } +function Write-PssaFinding { + <# + .SYNOPSIS + Writes a single PSScriptAnalyzer finding to the host with color coding. + + .DESCRIPTION + Errors are written in red, warnings in yellow. Suppresses raw diagnostic + object noise and formats the output for human readability. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object] $Finding + ) + + $severity = [string]$Finding.Severity + $color = if ($severity -eq 'Error') { 'Red' } else { 'Yellow' } + $icon = if ($severity -eq 'Error') { [char]0x2717 } else { [char]0x26A0 } + $label = if ($severity -eq 'Error') { 'Error ' } else { 'Warning' } + $scriptName = [System.IO.Path]::GetFileName([string]$Finding.ScriptPath) + + Write-Host " $icon [$label] $($Finding.RuleName)" -ForegroundColor $color + Write-Host " File : $scriptName (line $($Finding.Line), col $($Finding.Column))" -ForegroundColor DarkGray + Write-Host " Msg : $($Finding.Message)" -ForegroundColor DarkGray + Write-Host '' +} + +function Write-PssaHeader { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [version] $Version, + + [Parameter(Mandatory)] + [string] $SettingsPath, + + [Parameter(Mandatory)] + [string[]] $AnalyzedPaths + ) + + $separator = '-' * 64 + Write-Host $separator -ForegroundColor DarkCyan + Write-Host " PSScriptAnalyzer $Version" -ForegroundColor Cyan + Write-Host " Settings : $(Split-Path -Leaf $SettingsPath)" -ForegroundColor DarkCyan + Write-Host ' Paths :' -ForegroundColor DarkCyan + foreach ($p in $AnalyzedPaths) { + Write-Host " > $p" -ForegroundColor DarkGray + } + Write-Host $separator -ForegroundColor DarkCyan + Write-Host '' +} + +function Write-PssaSummary { + <# + .SYNOPSIS + Writes a color-coded summary of PSScriptAnalyzer results. + + .DESCRIPTION + - Green : no findings at all + - Yellow: only warnings present (local run passes, CI may flag if FailOnSeverity=Warning) + - Red : errors present (blocking findings) + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [object[]] $Findings, + + [Parameter(Mandatory)] + [string] $FailOnSeverity + ) + + $errorCount = @($Findings | Where-Object { $_.Severity -eq 'Error' }).Count + $warnCount = @($Findings | Where-Object { $_.Severity -eq 'Warning' }).Count + $separator = '-' * 64 + + Write-Host $separator -ForegroundColor DarkCyan + + if ($errorCount -eq 0 -and $warnCount -eq 0) { + Write-Host " $([char]0x2713) No findings — all checks passed." -ForegroundColor Green + } + else { + Write-Host " Findings : $($errorCount + $warnCount) total | $errorCount error(s) | $warnCount warning(s)" -ForegroundColor DarkGray + + if ($errorCount -gt 0) { + Write-Host " $([char]0x2717) $errorCount error(s) found — this run will FAIL." -ForegroundColor Red + } + + if ($warnCount -gt 0 -and $FailOnSeverity -eq 'Error') { + Write-Host " $([char]0x26A0) $warnCount warning(s) found — these pass locally but CI will flag them if FailOnSeverity=Warning." -ForegroundColor Yellow + } + elseif ($warnCount -gt 0) { + Write-Host " $([char]0x26A0) $warnCount warning(s) found — this run will FAIL." -ForegroundColor Yellow + } + } + + Write-Host $separator -ForegroundColor DarkCyan + Write-Host '' +} + function Write-JsonFile { <# .SYNOPSIS @@ -250,15 +349,18 @@ Initialize-Module -Name 'PSScriptAnalyzer' -RequiredVersion $PSScriptAnalyzerVer # Run analysis using the repo settings file. # We rely on the settings file for rule selection and severities. -Write-Host "Running PSScriptAnalyzer ($PSScriptAnalyzerVersion) using settings: $resolvedSettingsPath" -Write-Host "Analyzing paths:" -$resolvedPaths | ForEach-Object { Write-Host " - $_" } +Write-PssaHeader -Version $PSScriptAnalyzerVersion -SettingsPath $resolvedSettingsPath -AnalyzedPaths $resolvedPaths $findings = @() foreach ($path in $resolvedPaths) { $findings += Invoke-ScriptAnalyzer -Path $path -Recurse -Settings $resolvedSettingsPath } +# Display each finding with color coding (errors in red, warnings in yellow). +foreach ($f in ($findings | Sort-Object ScriptName, Line, Column, RuleName)) { + Write-PssaFinding -Finding $f +} + # Create a stable, small JSON payload (DiagnosticRecord contains complex members). $summary = @( foreach ($f in ($findings | Sort-Object ScriptName, Line, Column, RuleName)) { @@ -274,7 +376,7 @@ $summary = @( ) if ($CI -and $resolvedJsonOutputPath) { - Write-Host "Writing PSScriptAnalyzer JSON results: $resolvedJsonOutputPath" + Write-Host " Writing JSON results : $resolvedJsonOutputPath" -ForegroundColor DarkGray Write-JsonFile -Path $resolvedJsonOutputPath -Object $summary } @@ -288,10 +390,13 @@ if ($emitSarif -and $resolvedSarifOutputPath) { throw "ConvertToSARIF module is installed, but 'ConvertTo-SARIF' cmdlet was not found." } - Write-Host "Writing SARIF results: $resolvedSarifOutputPath" + Write-Host " Writing SARIF results : $resolvedSarifOutputPath" -ForegroundColor DarkGray $findings | & $convertCommand -FilePath $resolvedSarifOutputPath } +# Display color-coded summary (green/yellow/red) with CI hint for warnings. +Write-PssaSummary -Findings $findings -FailOnSeverity $FailOnSeverity + # Determine whether we should fail this run. $failSeverities = @($FailOnSeverity) if ($FailOnSeverity -eq 'Warning') { @@ -308,5 +413,3 @@ if ($blockingFindings) { $message = "PSScriptAnalyzer found blocking issues (FailOnSeverity: $FailOnSeverity). Errors: $errorCount, Warnings: $warningCount." throw $message } - -Write-Host "PSScriptAnalyzer completed with no blocking findings (FailOnSeverity: $FailOnSeverity)." From 5febd4b224563c165eed1a3f720b74d7e91de230 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 18:16:07 +0000 Subject: [PATCH 3/3] Fix unreliable @($null).Count step warnings assertion in export test Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tests/Core/Export-IdlePlan.Tests.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Core/Export-IdlePlan.Tests.ps1 b/tests/Core/Export-IdlePlan.Tests.ps1 index 00ff40cd..bea1602f 100644 --- a/tests/Core/Export-IdlePlan.Tests.ps1 +++ b/tests/Core/Export-IdlePlan.Tests.ps1 @@ -122,7 +122,8 @@ Describe 'Export-IdlePlan' { $json.plan.warnings[0].code | Should -Be 'PreconditionContextPathUnresolvedAtPlan' $json.plan.warnings[0].step | Should -Be 'Check Context' - @($json.plan.steps[0].warnings).Count | Should -BeGreaterThan 0 + $json.plan.steps[0].warnings | Should -Not -BeNullOrEmpty + ($json.plan.steps[0].warnings | Measure-Object).Count | Should -BeGreaterThan 0 $json.plan.steps[0].warnings[0].code | Should -Be 'PreconditionContextPathUnresolvedAtPlan' } }