microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/1873-devcontainer

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/linting/Invoke-PSScriptAnalyzer.ps1

249lines Ā· modecode

1#!/usr/bin/env pwsh
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4#
5# Invoke-PSScriptAnalyzer.ps1
6#
7# Purpose: Wrapper for PSScriptAnalyzer with GitHub Actions integration
8# Author: HVE Core Team
9
10#Requires -Version 7.0
11
12[CmdletBinding()]
13param(
14 [Parameter(Mandatory = $false)]
15 [switch]$ChangedFilesOnly,
16
17 [Parameter(Mandatory = $false)]
18 [string]$BaseBranch = "origin/main",
19
20 [Parameter(Mandatory = $false)]
21 [string]$ConfigPath = (Join-Path $PSScriptRoot "PSScriptAnalyzer.psd1"),
22
23 [Parameter(Mandatory = $false)]
24 [string]$OutputPath = "logs/psscriptanalyzer-results.json"
25)
26
27$ErrorActionPreference = 'Stop'
28
29# Import shared helpers
30Import-Module (Join-Path $PSScriptRoot "Modules/LintingHelpers.psm1") -Force
31Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force
32
33#region Functions
34
35function Invoke-ScriptAnalyzerIsolated {
36 [CmdletBinding()]
37 [OutputType([System.Object[]])]
38 [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseUsingScopeModifierInNewRunspaces', '', Justification = 'Variables are passed into the Start-Job script block via param() and -ArgumentList, so the $using: modifier does not apply.')]
39 param(
40 [Parameter(Mandatory = $true)]
41 [string]$Path,
42
43 [Parameter(Mandatory = $true)]
44 [string]$SettingsPath
45 )
46
47 # Analyze each file in its own child process so every file compiles into a
48 # fresh dynamic assembly. On Linux/CoreCLR a process permits only one dynamic
49 # module per dynamic assembly, so analyzing multiple module files in a shared
50 # runspace throws "more than one dynamic module" on the second .psm1 file.
51 $job = Start-Job -ScriptBlock {
52 param($FilePath, $ConfigPath)
53 Import-Module PSScriptAnalyzer -RequiredVersion 1.25.0
54 Invoke-ScriptAnalyzer -Path $FilePath -Settings $ConfigPath | ForEach-Object {
55 [pscustomobject]@{
56 RuleName = $_.RuleName
57 Message = $_.Message
58 Severity = $_.Severity.ToString()
59 Line = $_.Line
60 Column = $_.Column
61 }
62 }
63 } -ArgumentList $Path, $SettingsPath
64
65 try {
66 $null = Wait-Job -Job $job
67 return @(Receive-Job -Job $job)
68 }
69 finally {
70 Remove-Job -Job $job -Force
71 }
72}
73
74function Invoke-PSScriptAnalyzerCore {
75 [CmdletBinding()]
76 [OutputType([void])]
77 param(
78 [Parameter(Mandatory = $false)]
79 [switch]$ChangedFilesOnly,
80
81 [Parameter(Mandatory = $false)]
82 [string]$BaseBranch = "origin/main",
83
84 [Parameter(Mandatory = $false)]
85 [string]$ConfigPath = (Join-Path $PSScriptRoot "PSScriptAnalyzer.psd1"),
86
87 [Parameter(Mandatory = $false)]
88 [string]$OutputPath = "logs/psscriptanalyzer-results.json"
89 )
90
91 Write-Host "šŸ” Running PSScriptAnalyzer..." -ForegroundColor Cyan
92
93 # Ensure PSScriptAnalyzer 1.25.0 is available (presence-only check would allow a different installed version to bypass the pin)
94 if (-not (Get-Module -ListAvailable -Name PSScriptAnalyzer | Where-Object { $_.Version -eq [version]'1.25.0' })) {
95 Write-Host "Installing PSScriptAnalyzer 1.25.0..." -ForegroundColor Yellow
96 Install-Module -Name PSScriptAnalyzer -RequiredVersion 1.25.0 -Force -Scope CurrentUser -Repository PSGallery
97 }
98
99 Import-Module PSScriptAnalyzer -RequiredVersion 1.25.0
100
101 # Get files to analyze
102 $filesToAnalyze = @()
103
104 if ($ChangedFilesOnly) {
105 Write-Host "Detecting changed PowerShell files..." -ForegroundColor Cyan
106 $filesToAnalyze = @(Get-ChangedFilesFromGit -BaseBranch $BaseBranch -FileExtensions @('*.ps1', '*.psm1', '*.psd1'))
107 }
108 else {
109 Write-Host "Analyzing all PowerShell files..." -ForegroundColor Cyan
110 $filesToAnalyze = @(Get-FilesRecursive -Path "." -Include @('*.ps1', '*.psm1', '*.psd1'))
111 }
112
113 if (@($filesToAnalyze).Count -eq 0) {
114 Write-Host "āœ… No PowerShell files to analyze" -ForegroundColor Green
115 Set-CIOutput -Name "count" -Value "0"
116 Set-CIOutput -Name "issues" -Value "0"
117 return
118 }
119
120 Write-Host "Analyzing $($filesToAnalyze.Count) PowerShell files..." -ForegroundColor Cyan
121 Set-CIOutput -Name "count" -Value $filesToAnalyze.Count
122
123 # Run PSScriptAnalyzer
124 $allResults = @()
125 $hasErrors = $false
126
127 $resolvedConfigPath = (Resolve-Path -LiteralPath $ConfigPath).Path
128
129 foreach ($file in $filesToAnalyze) {
130 $filePath = if ($file -is [System.IO.FileInfo]) { $file.FullName } else { $file }
131 Write-Host "`nšŸ“„ Analyzing: $filePath" -ForegroundColor Cyan
132
133 $resolvedFilePath = (Resolve-Path -LiteralPath $filePath).Path
134 $results = @(Invoke-ScriptAnalyzerIsolated -Path $resolvedFilePath -SettingsPath $resolvedConfigPath)
135
136 if ($results) {
137 $allResults += $results
138
139 foreach ($result in $results) {
140 $annotationLevel = switch ($result.Severity) {
141 'Error' { 'Error' }
142 'Warning' { 'Warning' }
143 'Information' { 'Notice' }
144 default { 'Notice' }
145 }
146
147 Write-CIAnnotation `
148 -Message "$($result.RuleName): $($result.Message)" `
149 -Level $annotationLevel `
150 -File $filePath `
151 -Line $result.Line `
152 -Column $result.Column
153
154 $icon = switch ($result.Severity) {
155 'Error' { 'āŒ'; $hasErrors = $true }
156 'Warning' { 'āš ļø' }
157 default { 'ā„¹ļø' }
158 }
159
160 Write-Host " $icon [$($result.Severity)] $($result.RuleName): $($result.Message) (Line $($result.Line))" -ForegroundColor $(
161 if ($result.Severity -eq 'Error') { 'Red' }
162 elseif ($result.Severity -eq 'Warning') { 'Yellow' }
163 else { 'Cyan' }
164 )
165 }
166 }
167 else {
168 Write-Host " āœ… No issues found" -ForegroundColor Green
169 }
170 }
171
172 # Export results
173 $summary = @{
174 TotalFiles = @($filesToAnalyze).Count
175 TotalIssues = @($allResults).Count
176 Errors = @($allResults | Where-Object Severity -eq 'Error').Count
177 Warnings = @($allResults | Where-Object Severity -eq 'Warning').Count
178 Information = @($allResults | Where-Object Severity -eq 'Information').Count
179 HasErrors = $hasErrors
180 Timestamp = Get-StandardTimestamp
181 }
182
183 # Ensure logs directory exists
184 $logsDir = Split-Path $OutputPath -Parent
185 if (-not (Test-Path $logsDir)) {
186 New-Item -ItemType Directory -Force -Path $logsDir | Out-Null
187 }
188
189 $allResults | ConvertTo-Json -Depth 5 | Out-File $OutputPath
190 $summary | ConvertTo-Json | Out-File (Join-Path $logsDir "psscriptanalyzer-summary.json")
191
192 # Set outputs
193 Set-CIOutput -Name "issues" -Value $summary.TotalIssues
194 Set-CIOutput -Name "errors" -Value $summary.Errors
195 Set-CIOutput -Name "warnings" -Value $summary.Warnings
196
197 if ($hasErrors) {
198 Set-CIEnv -Name "PSSCRIPTANALYZER_FAILED" -Value "true"
199 }
200
201 # Write summary
202 Write-CIStepSummary -Content "## PSScriptAnalyzer Results`n"
203
204 if ($summary.TotalIssues -eq 0) {
205 Write-CIStepSummary -Content "āœ… **Status**: Passed`n`nAll $($summary.TotalFiles) PowerShell files passed linting checks."
206 Write-Host "`nāœ… All PowerShell files passed PSScriptAnalyzer checks!" -ForegroundColor Green
207 return
208 }
209 else {
210 Write-CIStepSummary -Content @"
211āŒ **Status**: Failed
212
213| Metric | Count |
214|--------|-------|
215| Files Analyzed | $($summary.TotalFiles) |
216| Total Issues | $($summary.TotalIssues) |
217| Errors | $($summary.Errors) |
218| Warnings | $($summary.Warnings) |
219| Information | $($summary.Information) |
220"@
221
222 Write-Host "`nāŒ PSScriptAnalyzer found $($summary.TotalIssues) issue(s)" -ForegroundColor Red
223 throw "PSScriptAnalyzer found $($summary.TotalIssues) issue(s)"
224 }
225}
226
227#endregion Functions
228
229#region Main Execution
230
231if ($MyInvocation.InvocationName -ne '.') {
232 # Strip /mnt/* paths from PATH to avoid slow 9P cross-filesystem
233 # lookups in WSL. PSScriptAnalyzer resolves commands by scanning every
234 # PATH directory per file; Windows mount points add ~40s per file.
235 $env:PATH = ($env:PATH -split [System.IO.Path]::PathSeparator |
236 Where-Object { $_ -notlike '/mnt/*' }) -join [System.IO.Path]::PathSeparator
237
238 try {
239 Invoke-PSScriptAnalyzerCore -ChangedFilesOnly:$ChangedFilesOnly -BaseBranch $BaseBranch -ConfigPath $ConfigPath -OutputPath $OutputPath
240 exit 0
241 }
242 catch {
243 Write-Error -ErrorAction Continue "PSScriptAnalyzer failed: $($_.Exception.Message)"
244 Write-CIAnnotation -Message $_.Exception.Message -Level Error
245 exit 1
246 }
247}
248
249#endregion Main Execution
250