From 5a82f728d6b8ec6b6ddb329adf0fe10aaf1c79f1 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Thu, 28 May 2026 23:39:46 -0400 Subject: [PATCH 1/2] ci: pin PSScriptAnalyzer version in the lint job The PSScriptAnalyzer Lint job installed PSScriptAnalyzer unpinned and called Invoke-ScriptAnalyzer without an explicit Import-Module, so it ignored the 1.24.0 pin in build.depend.psd1 (which only the build/test jobs honor via PSDepend). On a cache hit the install step was skipped entirely, letting the runner image's bundled copy load alongside the cached one and crash with "You cannot have more than one dynamic module in each dynamic assembly in this version of the runtime" (seen on main after #55, while the identical PR tree had passed minutes earlier). Read the pinned version from build.depend.psd1, install that exact version (verified present even on a cache hit), and import it explicitly with -RequiredVersion before Invoke-ScriptAnalyzer so exactly one assembly version loads. The cache key already hashes build.depend.psd1, so the installed version and cache stay coupled with a single source of truth. Verified in a pwsh 7.5 / ubuntu-24.04 container: with a polluted module path (1.25.0 pre-installed), the step still installs and loads exactly 1.24.0 and analyzes cleanly. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/CI.yaml | 42 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index d2ea5dc..d5cf5ed 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -18,7 +18,6 @@ jobs: - uses: actions/checkout@v6 - name: Cache PowerShell modules - id: cache-lint-modules uses: actions/cache@v5 with: path: ~/.local/share/powershell/Modules @@ -27,31 +26,40 @@ jobs: ${{ runner.os }}-psmodules-lint- - name: Install PSScriptAnalyzer - if: steps.cache-lint-modules.outputs.cache-hit != 'true' shell: pwsh run: | - $moduleFound = Get-Module -ListAvailable -Name PSScriptAnalyzer - if ($moduleFound) { - try { - Import-Module PSScriptAnalyzer -Force -ErrorAction Stop - Get-Command Invoke-ScriptAnalyzer -ErrorAction Stop | Out-Null - Write-Host 'PSScriptAnalyzer already available' - } - catch { - Write-Host 'PSScriptAnalyzer cache appears invalid; reinstalling...' - Set-PSRepository -Name PSGallery -InstallationPolicy Trusted - Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser - } + # 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...' - Set-PSRepository -Name PSGallery -InstallationPolicy Trusted - Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser + Write-Host "Installing PSScriptAnalyzer $requiredVersion..." + Set-PSRepository -Name 'PSGallery' -InstallationPolicy 'Trusted' + Install-Module -Name 'PSScriptAnalyzer' -RequiredVersion $requiredVersion -Force -Scope 'CurrentUser' } - 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 + $results = Invoke-ScriptAnalyzer -Path ./PlexAutomationToolkit -Recurse -Settings PSGallery -ReportSummary $errors = $results | Where-Object { $_.Severity -eq 'Error' } From b0faa4a93eab6ef0e49e17c53be3ef8342910e32 Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Fri, 29 May 2026 01:02:33 -0400 Subject: [PATCH 2/2] ci: fail fast on PSScriptAnalyzer install/import errors Add -ErrorAction 'Stop' to Install-Module and Import-Module in the lint job. In PowerShell 7 these emit non-terminating errors by default, so a failed install or import would not stop the step and Invoke-ScriptAnalyzer could run with a missing or wrong module version - the exact condition this job pins against. Addresses CodeRabbit review feedback on #62. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/CI.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yaml b/.github/workflows/CI.yaml index d5cf5ed..e8a7ce7 100644 --- a/.github/workflows/CI.yaml +++ b/.github/workflows/CI.yaml @@ -46,7 +46,7 @@ jobs: else { Write-Host "Installing PSScriptAnalyzer $requiredVersion..." Set-PSRepository -Name 'PSGallery' -InstallationPolicy 'Trusted' - Install-Module -Name 'PSScriptAnalyzer' -RequiredVersion $requiredVersion -Force -Scope 'CurrentUser' + Install-Module -Name 'PSScriptAnalyzer' -RequiredVersion $requiredVersion -Force -Scope 'CurrentUser' -ErrorAction 'Stop' } - name: Run PSScriptAnalyzer @@ -58,7 +58,7 @@ jobs: # 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 + 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' }