diff --git a/.github/workflows/validate-powershell.yml b/.github/workflows/validate-powershell.yml new file mode 100644 index 0000000..8c13d88 --- /dev/null +++ b/.github/workflows/validate-powershell.yml @@ -0,0 +1,42 @@ +name: Validate PowerShell + +on: + push: + paths: + - '**/*.ps1' + - 'PSScriptAnalyzerSettings.psd1' + - '.github/workflows/validate-powershell.yml' + pull_request: + paths: + - '**/*.ps1' + - 'PSScriptAnalyzerSettings.psd1' + - '.github/workflows/validate-powershell.yml' + +jobs: + lint: + name: PSScriptAnalyzer + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Install PSScriptAnalyzer + shell: pwsh + run: | + if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer)) { + Install-Module -Name PSScriptAnalyzer -Force -SkipPublisherCheck -Scope CurrentUser + } + Import-Module PSScriptAnalyzer + + - name: Run PSScriptAnalyzer on all PowerShell scripts + shell: pwsh + run: | + $results = Invoke-ScriptAnalyzer -Path . -Recurse -Settings PSScriptAnalyzerSettings.psd1 + if ($results) { + $results | Format-Table -AutoSize + Write-Error "PSScriptAnalyzer found $($results.Count) issue(s). Please fix them before merging." + exit 1 + } + Write-Host "PSScriptAnalyzer: No issues found." -ForegroundColor Green diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..6532fae --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,44 @@ +@{ + ExcludeRules = @( + # Cosmetic only - trailing whitespace does not affect script behaviour + 'PSAvoidTrailingWhitespace', + + # Write-Host is appropriate in interactive console/profile scripts + 'PSAvoidUsingWriteHost', + + # Private helper functions in personal scripts are not exported module + # cmdlets, so approved-verb and singular-noun requirements do not apply + 'PSUseApprovedVerbs', + 'PSUseSingularNouns', + + # Invoke-Expression is used for standard install patterns such as the + # Chocolatey bootstrap and oh-my-posh shell initialisation + 'PSAvoidUsingInvokeExpression', + + # UTF-8 without BOM is the preferred encoding for cross-platform scripts + 'PSUseBOMForUnicodeEncodedFile', + + # These are standalone scripts and git hooks, not reusable pipeline cmdlets + 'PSUseShouldProcessForStateChangingFunctions', + 'PSUseProcessBlockForPipelineCommand', + + # Script-level variables ($UserBucket etc.) are intentionally shared + # across dot-sourced scripts and are not accidental globals + 'PSAvoidGlobalVars', + + # False positive: $Filter is used inside a pipeline Where-Object block + # that PSScriptAnalyzer cannot statically trace + 'PSReviewUnusedParameter', + + # Information level only; [OutputType] declarations are documentation + 'PSUseOutputTypeCorrectly', + + # Profile.ps1 uses intentionally empty catch blocks for TLS setup and + # mutex cleanup that must not interrupt an interactive shell session + 'PSAvoidUsingEmptyCatchBlock', + + # Third-party CLI wrappers (choco, scoop) use positional parameters + # by convention; enforcing named params here would be incorrect + 'PSAvoidUsingPositionalParameters' + ) +}