From 305fb9ea4fe4ade0d80be4cc3941e7034485d998 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Fri, 29 May 2026 00:52:12 -0400 Subject: [PATCH] fix: enforce PSScriptAnalyzer in the build and drop the redundant lint job PowerShellBuild's 'Analyze' task never fails the build: its Test-PSBuildScriptAnalysis filters results with `$_Severity` (an undefined variable) instead of `$_.Severity`, so error/warning counts are always zero. Present in every released version (through 0.8.0) and on main, so a version bump cannot fix it. Reported upstream: psake/PowerShellBuild#125. Add a custom 'Lint' task that runs Invoke-ScriptAnalyzer directly and fails at the configured FailBuildOnSeverityLevel ('Error'), counting ParseError (syntax-broken files) as errors too. The Test task depends on 'Lint' instead of PowerShellBuild's no-op 'Analyze' (which psake loads via -FromModule and won't let us redefine; it is left unused). This mirrors how other PowerShellBuild consumers define their own analysis task rather than relying on the module's no-op. Point ScriptAnalysis.SettingsPath at the repo's own PSScriptAnalyzerSettings.psd1 (PowerShellBuild defaulted to its bundled ruleset) and drop the obsolete Linux/macOS analysis-disable. Remove the standalone 'PSScriptAnalyzer Lint' CI job: now that analysis runs and enforces during the build on every platform, a separate job re-running PSScriptAnalyzer on the source tree is redundant. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/CI.yaml | 65 ------------------------------------- build.psake.ps1 | 68 +++++++++++++++++++++++++++++++++------ 2 files changed, 59 insertions(+), 74 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index e8a7ce7..94a0ebc 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -11,71 +11,6 @@ permissions: pull-requests: write jobs: - lint: - name: PSScriptAnalyzer Lint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - - name: Cache PowerShell modules - uses: actions/cache@v5 - with: - path: ~/.local/share/powershell/Modules - key: ${{ runner.os }}-psmodules-lint-${{ hashFiles('build.depend.psd1') }} - restore-keys: | - ${{ runner.os }}-psmodules-lint- - - - name: Install PSScriptAnalyzer - shell: pwsh - run: | - # Pin to the same version the rest of the build uses (build.depend.psd1) so the - # lint job never loads a version that mismatches the runner's PowerShell runtime. - $manifest = Import-PowerShellDataFile -Path './build.depend.psd1' - $requiredVersion = $manifest['PSScriptAnalyzer'].Version - if (-not $requiredVersion) { - throw 'PSScriptAnalyzer version not found in build.depend.psd1' - } - - # Verify the exact version is present even on a cache hit; a cache restored from an - # earlier (unpinned) run can hold a different version, so check before trusting it. - $installed = Get-Module -ListAvailable -Name 'PSScriptAnalyzer' | - Where-Object { $_.Version -eq $requiredVersion } - if ($installed) { - Write-Host "PSScriptAnalyzer $requiredVersion already available" - } - else { - Write-Host "Installing PSScriptAnalyzer $requiredVersion..." - Set-PSRepository -Name 'PSGallery' -InstallationPolicy 'Trusted' - Install-Module -Name 'PSScriptAnalyzer' -RequiredVersion $requiredVersion -Force -Scope 'CurrentUser' -ErrorAction 'Stop' - } - - - name: Run PSScriptAnalyzer - shell: pwsh - run: | - # Import the pinned version explicitly so exactly one PSScriptAnalyzer assembly loads. - # Without this, a bare Invoke-ScriptAnalyzer can auto-load the runner image's bundled - # copy alongside the cached one and crash with "more than one dynamic module in each - # dynamic assembly in this version of the runtime." - $manifest = Import-PowerShellDataFile -Path './build.depend.psd1' - $requiredVersion = $manifest['PSScriptAnalyzer'].Version - Import-Module -Name 'PSScriptAnalyzer' -RequiredVersion $requiredVersion -Force -ErrorAction 'Stop' - - $results = Invoke-ScriptAnalyzer -Path ./PlexAutomationToolkit -Recurse -Settings PSGallery -ReportSummary - $errors = $results | Where-Object { $_.Severity -eq 'Error' } - - if ($results) { - Write-Host "::group::PSScriptAnalyzer Results" - $results | Format-Table -AutoSize - Write-Host "::endgroup::" - } - - if ($errors) { - Write-Host "::error::PSScriptAnalyzer found $($errors.Count) error(s)" - exit 1 - } - - Write-Host "PSScriptAnalyzer passed with no errors" - unit-tests: name: Unit Tests (${{ matrix.os }}) runs-on: ${{ matrix.os }} diff --git a/build.psake.ps1 b/build.psake.ps1 index b1f8a94..b827204 100644 --- a/build.psake.ps1 +++ b/build.psake.ps1 @@ -10,13 +10,12 @@ properties { # Set this to $true to create a module with a monolithic PSM1 $PSBPreference.Build.CompileModule = $false $PSBPreference.Help.DefaultLocale = 'en-US' - # Workaround: PSScriptAnalyzer has a bug on non-Windows platforms when loading settings files - # Disable in-build analysis on Linux and macOS; the dedicated PSScriptAnalyzer Lint job provides coverage - # Note: $IsLinux and $IsMacOS are undefined ($null/falsy) in Windows PowerShell 5.x, - # so this correctly keeps ScriptAnalysis enabled there - if ($IsLinux -or $IsMacOS) { - $PSBPreference.Test.ScriptAnalysis.Enabled = $false - } + # Analyze against the repo's own PSScriptAnalyzer ruleset instead of PowerShellBuild's + # bundled default. Analysis runs on every platform: loading a settings file no longer + # crashes PSScriptAnalyzer on Linux/macOS (verified 2026-05 on pwsh 7.5), so the previous + # non-Windows disable is no longer needed. + $PSBPreference.Test.ScriptAnalysis.SettingsPath = + Join-Path -Path $PSScriptRoot -ChildPath 'PSScriptAnalyzerSettings.psd1' # Use absolute paths for test output (relative paths resolve from tests directory) $PSBPreference.Test.OutputFile = [IO.Path]::Combine($PSScriptRoot, 'out', 'testResults.xml') $PSBPreference.Test.OutputFormat = 'NUnitXml' @@ -108,5 +107,56 @@ Task -Name 'UpdateReleaseNotes' -Depends 'Build' -Description 'Set built manifes # defaults to depending only on 'Test'). $PSBPublishDependency = @('Test', 'UpdateReleaseNotes') -# Note: -Depends replaces PowerShellBuild's default dependencies, so we must include Pester and Analyze explicitly -Task -Name 'Test' -FromModule 'PowerShellBuild' -MinimumVersion '0.7.3' -Depends 'Init_Integration', 'Pester', 'Analyze' +# Note: -Depends replaces PowerShellBuild's default dependencies, so we must include Pester and +# our own analysis task explicitly. We depend on a custom 'Lint' task (defined below) instead of +# PowerShellBuild's 'Analyze', because that task never fails the build: its Test-PSBuildScriptAnalysis +# - in every released PowerShellBuild version (through 0.8.0) and on its main branch - filters +# results with '$_Severity' (an undefined variable) instead of '$_.Severity', so its error/warning +# counts are always zero (upstream psake/PowerShellBuild#125). psake won't let us redefine the +# 'Analyze' task it loads via -FromModule (duplicate-name error), so we leave it unused and run our +# own task under a distinct name. +Task -Name 'Test' -FromModule 'PowerShellBuild' -MinimumVersion '0.7.3' -Depends 'Init_Integration', 'Pester', 'Lint' + +Task -Name 'Lint' -Depends 'Build' -Description 'Run PSScriptAnalyzer against the built module and fail on the configured severity threshold' { + if (-not $PSBPreference.Test.ScriptAnalysis.Enabled) { + Write-Host 'Script analysis is disabled; skipping.' + return + } + + $analyzeParameters = @{ + Path = $PSBPreference.Build.ModuleOutDir + Settings = $PSBPreference.Test.ScriptAnalysis.SettingsPath + Recurse = $true + } + $analysisResult = Invoke-ScriptAnalyzer @analyzeParameters + if ($analysisResult) { + $analysisResult | Format-Table -AutoSize | Out-String | Write-Host + } + + # Count ParseError (unparseable/syntax-broken files) as an error too - PSScriptAnalyzer + # reports those with Severity 'ParseError', which would otherwise bypass the 'Error' gate. + $errorCount = @($analysisResult).Where({ $_.Severity -eq 'Error' -or $_.Severity -eq 'ParseError' }).Count + $warningCount = @($analysisResult).Where({ $_.Severity -eq 'Warning' }).Count + $informationCount = @($analysisResult).Where({ $_.Severity -eq 'Information' }).Count + + $failOnSeverity = $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel + switch ($failOnSeverity) { + 'Error' { + if ($errorCount -gt 0) { + throw "PSScriptAnalyzer found $errorCount error(s)." + } + } + 'Warning' { + if ($errorCount -gt 0 -or $warningCount -gt 0) { + throw "PSScriptAnalyzer found $errorCount error(s) and $warningCount warning(s)." + } + } + default { + if ($errorCount -gt 0 -or $warningCount -gt 0 -or $informationCount -gt 0) { + throw "PSScriptAnalyzer found $errorCount error(s), $warningCount warning(s), and $informationCount information record(s)." + } + } + } + + Write-Host "PSScriptAnalyzer passed (errors: $errorCount, warnings: $warningCount, information: $informationCount; fail level: $failOnSeverity)." +}