Add files via upload #67
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: PowerShell Corporate Lint (PSScriptAnalyzer + SARIF + Reports) [Report-Only] | |
| on: | |
| push: | |
| branches: [main, develop] | |
| paths: | |
| - "**/*.ps1" | |
| - "**/*.psm1" | |
| - "**/*.psd1" | |
| - ".psscriptanalyzer.psd1" | |
| - ".github/workflows/psscriptanalyzer-sarif-reports.yml" | |
| pull_request: | |
| branches: [main, develop] | |
| paths: | |
| - "**/*.ps1" | |
| - "**/*.psm1" | |
| - "**/*.psd1" | |
| - ".psscriptanalyzer.psd1" | |
| - ".github/workflows/psscriptanalyzer-sarif-reports.yml" | |
| workflow_dispatch: | |
| concurrency: | |
| group: powershell-lint-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| security-events: write | |
| jobs: | |
| psa-analyze: | |
| name: PSScriptAnalyzer Report-Only (SARIF + Artifacts + Summary) | |
| runs-on: windows-latest | |
| timeout-minutes: 35 | |
| env: | |
| PSA_VERSION: "1.24.0" | |
| SETTINGS_FILE: ".psscriptanalyzer.psd1" | |
| OUT_DIR: "lint-reports" | |
| OUT_JSON: "psa-results.json" | |
| OUT_CSV: "psa-results.csv" | |
| OUT_MD: "psa-results.md" | |
| OUT_SARIF: "psa-results.sarif" | |
| OUT_FAILTXT: "psa-invocation-failed.txt" | |
| OUT_SUMMARY: "psa-summary.txt" | |
| OUT_PS51: "ps51-compat.txt" | |
| ANALYZE_ROOTS: >- | |
| BlueTeam-Tools | |
| Core-ScriptLibrary | |
| ITSM-Templates-SVR | |
| ITSM-Templates-WKS | |
| ProSuite-Hub | |
| SysAdmin-Tools | |
| EXCLUDE_SUBTREE: "SysAdmin-Tools/GroupPolicyObjects-Templates" | |
| PRUNE_DIR_REGEX: '(?i)[\\/](\.git|node_modules|dist|build|artifacts|\.next)[\\/]' | |
| SARIF_CATEGORY: "powershell/psscriptanalyzer" | |
| # UI wrapper naming conventions (suppress ShouldProcess noise ONLY for these wrappers) | |
| UI_WRAPPER_FN_REGEX: '^(Show-|New-.*(Form|Dialog|Window)$|Initialize-.*(UI|Form)$|Update-.*UI$|Refresh-.*UI$|Set-.*(UI|Form)$)' | |
| steps: | |
| - name: Checkout (sparse) | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| sparse-checkout-cone-mode: false | |
| sparse-checkout: | | |
| BlueTeam-Tools | |
| Core-ScriptLibrary | |
| ITSM-Templates-SVR | |
| ITSM-Templates-WKS | |
| ProSuite-Hub | |
| SysAdmin-Tools | |
| .psscriptanalyzer.psd1 | |
| !SysAdmin-Tools/GroupPolicyObjects-Templates | |
| - name: Git long paths | |
| shell: pwsh | |
| run: | | |
| git config --global core.longpaths true | |
| - name: Initialize output directory + baselines (never fail) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR | |
| New-Item -ItemType Directory -Force -Path $outDir | Out-Null | |
| $jsonOut = Join-Path $outDir $env:OUT_JSON | |
| $csvOut = Join-Path $outDir $env:OUT_CSV | |
| $mdOut = Join-Path $outDir $env:OUT_MD | |
| $sarifOut = Join-Path $outDir $env:OUT_SARIF | |
| $failOut = Join-Path $outDir $env:OUT_FAILTXT | |
| $sumOut = Join-Path $outDir $env:OUT_SUMMARY | |
| $ps51Out = Join-Path $outDir $env:OUT_PS51 | |
| # Baselines so uploads/summary always have something to show | |
| '[]' | Set-Content -Encoding UTF8 $jsonOut | |
| 'RuleName,Severity,Message,ScriptName,Line,Column' | Set-Content -Encoding UTF8 $csvOut | |
| @( | |
| '# PSScriptAnalyzer Report (Report-Only)' | |
| '' | |
| 'Status: Not executed yet.' | |
| ) | Set-Content -Encoding UTF8 $mdOut | |
| @( | |
| 'PSScriptAnalyzer (Report-Only)' | |
| 'Files scanned: 0' | |
| "Settings file used: False ($($env:SETTINGS_FILE))" | |
| 'Total findings: 0' | |
| ) | Set-Content -Encoding UTF8 $sumOut | |
| @( | |
| 'PS 5.1 Compatibility Scan (Report-Only)' | |
| 'Files scanned: 0' | |
| "Excluded subtree: $($env:EXCLUDE_SUBTREE)" | |
| '' | |
| 'No PowerShell files found to scan.' | |
| ) | Set-Content -Encoding UTF8 $ps51Out | |
| '' | Set-Content -Encoding UTF8 $failOut | |
| $baselineSarif = @{ | |
| '$schema' = 'https://json.schemastore.org/sarif-2.1.0.json' | |
| version = '2.1.0' | |
| runs = @(@{ tool = @{ driver = @{ name = 'PSScriptAnalyzer'; version = '0' } }; results = @() }) | |
| } | |
| $baselineSarif | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $sarifOut | |
| exit 0 | |
| - name: Ensure module cache directories exist | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Force -Path "$HOME\Documents\PowerShell\Modules" | Out-Null | |
| New-Item -ItemType Directory -Force -Path "$HOME\Documents\WindowsPowerShell\Modules" | Out-Null | |
| exit 0 | |
| - name: Cache PowerShell modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~\Documents\PowerShell\Modules | |
| ~\Documents\WindowsPowerShell\Modules | |
| key: psmodules-${{ runner.os }}-psa-${{ env.PSA_VERSION }} | |
| - name: Install PSScriptAnalyzer (pinned) | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Stop' | |
| $v = $env:PSA_VERSION | |
| $have = Get-Module -ListAvailable PSScriptAnalyzer | | |
| Sort-Object Version -Descending | | |
| Select-Object -First 1 | |
| if (-not $have -or $have.Version.ToString() -ne $v) { | |
| Install-Module PSScriptAnalyzer -RequiredVersion $v -Force -Scope CurrentUser -AllowClobber | |
| } | |
| Import-Module PSScriptAnalyzer -RequiredVersion $v -Force | |
| Write-Host "PSScriptAnalyzer version loaded: $((Get-Module PSScriptAnalyzer).Version)" | |
| exit 0 | |
| - name: Discover PowerShell files to analyze | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $root = $env:GITHUB_WORKSPACE | |
| $prune = [regex]::new($env:PRUNE_DIR_REGEX) | |
| $excludeSub = ($env:EXCLUDE_SUBTREE -replace '/', [IO.Path]::DirectorySeparatorChar) | |
| $excludeSubEsc = [regex]::Escape($excludeSub) | |
| $roots = @() | |
| foreach ($token in ($env:ANALYZE_ROOTS -split '\s+')) { | |
| if ([string]::IsNullOrWhiteSpace($token)) { continue } | |
| $p = Join-Path $root $token | |
| if (Test-Path $p) { $roots += $p } | |
| } | |
| $files = @() | |
| if ($roots.Count -gt 0) { | |
| $files = foreach ($r in $roots) { | |
| Get-ChildItem -Path $r -Recurse -File -Include *.ps1,*.psm1,*.psd1 -ErrorAction SilentlyContinue | | |
| Where-Object { | |
| ($_.FullName -notmatch $prune) -and | |
| ($_.FullName -notmatch $excludeSubEsc) | |
| } | |
| } | |
| } | |
| $files = @($files | Sort-Object FullName -Unique) | |
| $count = @($files).Count | |
| "PSA_FILE_COUNT=$count" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | |
| # (Optional) write list for diagnostics/artifact | |
| $outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR | |
| $listPath = Join-Path $outDir "psa-files.txt" | |
| $files | ForEach-Object { $_.FullName.Replace("$env:GITHUB_WORKSPACE\", '').Replace("$env:GITHUB_WORKSPACE/", '') } | | |
| Set-Content -Encoding UTF8 $listPath | |
| exit 0 | |
| - name: PS 5.1 Compatibility Scan (report-only) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR | |
| $ps51Out = Join-Path $outDir $env:OUT_PS51 | |
| $root = $env:GITHUB_WORKSPACE | |
| $prune = [regex]::new($env:PRUNE_DIR_REGEX) | |
| $excludeSub = ($env:EXCLUDE_SUBTREE -replace '/', [IO.Path]::DirectorySeparatorChar) | |
| $excludeSubEsc = [regex]::Escape($excludeSub) | |
| $roots = @() | |
| foreach ($token in ($env:ANALYZE_ROOTS -split '\s+')) { | |
| if ([string]::IsNullOrWhiteSpace($token)) { continue } | |
| $p = Join-Path $root $token | |
| if (Test-Path $p) { $roots += $p } | |
| } | |
| $files = @() | |
| if ($roots.Count -gt 0) { | |
| $files = foreach ($r in $roots) { | |
| Get-ChildItem -Path $r -Recurse -File -Include *.ps1,*.psm1,*.psd1 -ErrorAction SilentlyContinue | | |
| Where-Object { | |
| ($_.FullName -notmatch $prune) -and | |
| ($_.FullName -notmatch $excludeSubEsc) | |
| } | |
| } | |
| } | |
| $files = @($files | Sort-Object FullName -Unique) | |
| $count = @($files).Count | |
| $patterns = @( | |
| @{ Name = 'NullCoalescingOperator'; Rx = '\?\?' }, | |
| @{ Name = 'NullConditionalOperator'; Rx = '\?\.' }, | |
| @{ Name = 'TernaryOperator'; Rx = '(?<!\?)\?(?!\?)[^\r\n:]+:' }, | |
| @{ Name = 'PipelineChainAndOr'; Rx = '(?<!`)\&\&|(?<!`)\|\|' }, | |
| @{ Name = 'ForEachObjectParallel'; Rx = 'ForEach-Object\s+-Parallel\b' } | |
| ) | |
| $lines = New-Object System.Collections.Generic.List[string] | |
| $lines.Add("PS 5.1 Compatibility Scan (Report-Only)") | |
| $lines.Add("Files scanned: $count") | |
| $lines.Add("Excluded subtree: $($env:EXCLUDE_SUBTREE)") | |
| $lines.Add("") | |
| if ($count -eq 0) { | |
| $lines.Add("No PowerShell files found to scan.") | |
| $lines | Set-Content -Encoding UTF8 $ps51Out | |
| exit 0 | |
| } | |
| $hitsTotal = 0 | |
| foreach ($p in $patterns) { | |
| $name = $p.Name | |
| $rx = [regex]::new($p.Rx) | |
| $hits = New-Object System.Collections.Generic.List[string] | |
| foreach ($f in $files) { | |
| try { | |
| $content = Get-Content -LiteralPath $f.FullName -Raw -ErrorAction Stop | |
| if ($rx.IsMatch($content)) { | |
| $allLines = Get-Content -LiteralPath $f.FullName -ErrorAction Stop | |
| for ($i=0; $i -lt $allLines.Count; $i++) { | |
| if ($rx.IsMatch($allLines[$i])) { | |
| $rel = $f.FullName.Replace("$env:GITHUB_WORKSPACE\", '').Replace("$env:GITHUB_WORKSPACE/", '') | |
| $hits.Add("${rel}:$($i+1): $($allLines[$i].Trim())") | |
| $hitsTotal++ | |
| if ($hits.Count -ge 20) { break } | |
| } | |
| } | |
| } | |
| } catch { } | |
| } | |
| $lines.Add("## $name") | |
| if ($hits.Count -eq 0) { $lines.Add("- None") } | |
| else { foreach ($h in $hits) { $lines.Add("- $h") } } | |
| $lines.Add("") | |
| } | |
| $lines.Add("Total hits (all patterns): $hitsTotal") | |
| $lines.Add("") | |
| $lines.Add("> Informational only. Fix hits to preserve PS 5.1 compatibility.") | |
| $lines | Set-Content -Encoding UTF8 $ps51Out | |
| exit 0 | |
| - name: Run PSScriptAnalyzer + Build Reports (REPORT-ONLY) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR | |
| $jsonOut = Join-Path $outDir $env:OUT_JSON | |
| $csvOut = Join-Path $outDir $env:OUT_CSV | |
| $mdOut = Join-Path $outDir $env:OUT_MD | |
| $sarifOut = Join-Path $outDir $env:OUT_SARIF | |
| $failOut = Join-Path $outDir $env:OUT_FAILTXT | |
| $sumOut = Join-Path $outDir $env:OUT_SUMMARY | |
| $ps51Out = Join-Path $outDir $env:OUT_PS51 | |
| $root = $env:GITHUB_WORKSPACE | |
| $prune = [regex]::new($env:PRUNE_DIR_REGEX) | |
| $excludeSub = ($env:EXCLUDE_SUBTREE -replace '/', [IO.Path]::DirectorySeparatorChar) | |
| $excludeSubEsc = [regex]::Escape($excludeSub) | |
| $roots = @() | |
| foreach ($token in ($env:ANALYZE_ROOTS -split '\s+')) { | |
| if ([string]::IsNullOrWhiteSpace($token)) { continue } | |
| $p = Join-Path $root $token | |
| if (Test-Path $p) { $roots += $p } | |
| } | |
| $files = @() | |
| if ($roots.Count -gt 0) { | |
| $files = foreach ($r in $roots) { | |
| Get-ChildItem -Path $r -Recurse -File -Include *.ps1,*.psm1,*.psd1 -ErrorAction SilentlyContinue | | |
| Where-Object { | |
| ($_.FullName -notmatch $prune) -and | |
| ($_.FullName -notmatch $excludeSubEsc) | |
| } | |
| } | |
| } | |
| $files = @($files | Sort-Object FullName -Unique) | |
| $count = @($files).Count | |
| $settingsPath = Join-Path $env:GITHUB_WORKSPACE $env:SETTINGS_FILE | |
| $useSettings = (Test-Path $settingsPath) | |
| $settingsArgs = @{} | |
| if ($useSettings) { $settingsArgs['Settings'] = $settingsPath } | |
| $results = @() | |
| try { | |
| if ($count -gt 0) { | |
| $paths = @($files | ForEach-Object { [string]$_.FullName }) | |
| $all = New-Object System.Collections.Generic.List[object] | |
| foreach ($p in $paths) { | |
| if ([string]::IsNullOrWhiteSpace($p)) { continue } | |
| try { | |
| $r = Invoke-ScriptAnalyzer -Path $p -Recurse:$false @settingsArgs | |
| if ($r) { foreach ($item in @($r)) { $all.Add($item) } } | |
| } catch { | |
| $_ | Out-String | Add-Content -Encoding UTF8 $failOut | |
| } | |
| } | |
| $results = $all.ToArray() | |
| } | |
| } catch { | |
| $_ | Out-String | Add-Content -Encoding UTF8 $failOut | |
| $results = @() | |
| } | |
| if (-not $results) { $results = @() } | |
| # Corporate-defensible suppression: suppress ShouldProcess only inside UI wrapper function names | |
| $uiWrapperRx = [regex]::new($env:UI_WRAPPER_FN_REGEX, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase) | |
| function Get-ContainingFunctionName { | |
| param( | |
| [Parameter(Mandatory=$true)][string]$Path, | |
| [Parameter(Mandatory=$true)][int]$Line | |
| ) | |
| try { | |
| if (-not (Test-Path $Path)) { return $null } | |
| $lines = Get-Content -LiteralPath $Path -ErrorAction Stop | |
| if ($Line -lt 1 -or $Line -gt $lines.Count) { return $null } | |
| for ($i = $Line; $i -ge 1; $i--) { | |
| $t = $lines[$i-1] | |
| if ($t -match '^\s*function\s+([A-Za-z_][\w\-]*)\s*(\(|\{|\s)') { | |
| return $Matches[1] | |
| } | |
| } | |
| return $null | |
| } catch { return $null } | |
| } | |
| $filtered = New-Object System.Collections.Generic.List[object] | |
| foreach ($r in @($results)) { | |
| if ([string]$r.RuleName -ne 'PSUseShouldProcessForStateChangingFunctions') { | |
| $filtered.Add($r) | |
| continue | |
| } | |
| $scriptPath = [string]$r.ScriptName | |
| $lineNum = 0 | |
| try { $lineNum = [int]$r.Line } catch { $lineNum = 0 } | |
| $fn = $null | |
| if (-not [string]::IsNullOrWhiteSpace($scriptPath) -and $lineNum -gt 0) { | |
| $fn = Get-ContainingFunctionName -Path $scriptPath -Line $lineNum | |
| } | |
| if ($fn -and $uiWrapperRx.IsMatch($fn)) { continue } | |
| $filtered.Add($r) | |
| } | |
| $results = $filtered.ToArray() | |
| # Reports | |
| $results | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $jsonOut | |
| $results | Select-Object RuleName, Severity, Message, ScriptName, Line, Column | | |
| Export-Csv -Path $csvOut -NoTypeInformation -Encoding UTF8 | |
| $bySev = $results | Group-Object Severity | Sort-Object Name | |
| $byRule = $results | Group-Object RuleName | Sort-Object Count -Descending | |
| $md = New-Object System.Collections.Generic.List[string] | |
| $md.Add("# PSScriptAnalyzer Report (Report-Only)") | |
| $md.Add("") | |
| $md.Add("Files scanned: $count") | |
| $md.Add("Settings file used: $useSettings ($($env:SETTINGS_FILE))") | |
| $md.Add("Total findings: $(@($results).Count)") | |
| $md.Add("") | |
| $md.Add("## Findings by Severity") | |
| if ($bySev.Count -eq 0) { $md.Add("- None") } | |
| else { foreach ($g in $bySev) { $md.Add("- $($g.Name): $($g.Count)") } } | |
| $md.Add("") | |
| $md.Add("## Top Rules") | |
| $top = $byRule | Select-Object -First 20 | |
| if ($top.Count -eq 0) { $md.Add("- None") } | |
| else { foreach ($g in $top) { $md.Add("- $($g.Name): $($g.Count)") } } | |
| $md.Add("") | |
| $md.Add("## PS 5.1 Compatibility Scan") | |
| if (Test-Path $ps51Out) { $md.Add("- See artifact file: $($env:OUT_PS51)") } | |
| else { $md.Add("- PS 5.1 scan report not found.") } | |
| $md.Add("") | |
| $md.Add("> Report-only: findings never fail CI. Artifacts are uploaded for review.") | |
| $md | Set-Content -Encoding UTF8 $mdOut | |
| $summary = New-Object System.Collections.Generic.List[string] | |
| $summary.Add("PSScriptAnalyzer (Report-Only)") | |
| $summary.Add("Files scanned: $count") | |
| $summary.Add("Settings file used: $useSettings ($($env:SETTINGS_FILE))") | |
| $summary.Add("Total findings: $(@($results).Count)") | |
| foreach ($g in $bySev) { $summary.Add(" $($g.Name): $($g.Count)") } | |
| $summary | Set-Content -Encoding UTF8 $sumOut | |
| function Get-SarifLevel([string]$sev) { | |
| switch -Regex ($sev) { | |
| 'Error' { 'error' } | |
| 'Warning' { 'warning' } | |
| default { 'note' } | |
| } | |
| } | |
| # SARIF | |
| try { | |
| $sarif = @{ | |
| '$schema' = 'https://json.schemastore.org/sarif-2.1.0.json' | |
| version = '2.1.0' | |
| runs = @( | |
| @{ | |
| tool = @{ | |
| driver = @{ | |
| name = 'PSScriptAnalyzer' | |
| informationUri = 'https://github.com/PowerShell/PSScriptAnalyzer' | |
| version = (Get-Module PSScriptAnalyzer).Version.ToString() | |
| } | |
| } | |
| results = @() | |
| } | |
| ) | |
| } | |
| foreach ($r in $results) { | |
| $scriptRel = [string]$r.ScriptName | |
| if (-not [string]::IsNullOrWhiteSpace($scriptRel)) { | |
| $scriptRel = $scriptRel.Replace("$env:GITHUB_WORKSPACE\", '').Replace("$env:GITHUB_WORKSPACE/", '') | |
| } | |
| $sarif.runs[0].results += @{ | |
| ruleId = [string]$r.RuleName | |
| level = (Get-SarifLevel ([string]$r.Severity)) | |
| message = @{ text = [string]$r.Message } | |
| locations = @( | |
| @{ | |
| physicalLocation = @{ | |
| artifactLocation = @{ uri = $scriptRel } | |
| region = @{ | |
| startLine = [int]$r.Line | |
| startColumn = [int]$r.Column | |
| } | |
| } | |
| } | |
| ) | |
| } | |
| } | |
| $sarif | ConvertTo-Json -Depth 12 | Set-Content -Encoding UTF8 $sarifOut | |
| } catch { | |
| $_ | Out-String | Add-Content -Encoding UTF8 $failOut | |
| } | |
| exit 0 | |
| - name: Publish report to Run Summary (View Runs) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR | |
| $mdOut = Join-Path $outDir $env:OUT_MD | |
| $sumOut = Join-Path $outDir $env:OUT_SUMMARY | |
| $ps51Out = Join-Path $outDir $env:OUT_PS51 | |
| $jsonOut = Join-Path $outDir $env:OUT_JSON | |
| $csvOut = Join-Path $outDir $env:OUT_CSV | |
| $sarifOut = Join-Path $outDir $env:OUT_SARIF | |
| $failOut = Join-Path $outDir $env:OUT_FAILTXT | |
| $findings = 0 | |
| try { | |
| if (Test-Path $jsonOut) { | |
| $raw = Get-Content -LiteralPath $jsonOut -Raw -ErrorAction Stop | |
| if (-not [string]::IsNullOrWhiteSpace($raw)) { | |
| $obj = $raw | ConvertFrom-Json -ErrorAction Stop | |
| $findings = @($obj).Count | |
| } | |
| } | |
| } catch { $findings = 0 } | |
| $summaryFile = $env:GITHUB_STEP_SUMMARY | |
| @( | |
| "## 🧪 PSScriptAnalyzer Corporate Lint (Report-Only)" | |
| "" | |
| "**Event:** ``$($env:GITHUB_EVENT_NAME)``" | |
| "**Ref:** ``$($env:GITHUB_REF)``" | |
| "**Findings:** $findings" | |
| "" | |
| "### Summary" | |
| "" | |
| ) | Set-Content -LiteralPath $summaryFile -Encoding UTF8 | |
| if (Test-Path $sumOut) { Get-Content -LiteralPath $sumOut | Add-Content -LiteralPath $summaryFile -Encoding UTF8 } | |
| else { "_No summary file found: $($env:OUT_DIR)/$($env:OUT_SUMMARY)_" | Add-Content -LiteralPath $summaryFile -Encoding UTF8 } | |
| @( | |
| "" | |
| "### Report (Markdown)" | |
| "" | |
| ) | Add-Content -LiteralPath $summaryFile -Encoding UTF8 | |
| if (Test-Path $mdOut) { Get-Content -LiteralPath $mdOut | Add-Content -LiteralPath $summaryFile -Encoding UTF8 } | |
| else { "_No markdown report found: $($env:OUT_DIR)/$($env:OUT_MD)_" | Add-Content -LiteralPath $summaryFile -Encoding UTF8 } | |
| @( | |
| "" | |
| "### Artifacts" | |
| "" | |
| "- ``$($env:OUT_DIR)/$($env:OUT_MD)``" | |
| "- ``$($env:OUT_DIR)/$($env:OUT_SUMMARY)``" | |
| "- ``$($env:OUT_DIR)/$($env:OUT_PS51)``" | |
| "- ``$($env:OUT_DIR)/$($env:OUT_JSON)``" | |
| "- ``$($env:OUT_DIR)/$($env:OUT_CSV)``" | |
| "- ``$($env:OUT_DIR)/$($env:OUT_SARIF)``" | |
| "- ``$($env:OUT_DIR)/$($env:OUT_FAILTXT)``" | |
| ) | Add-Content -LiteralPath $summaryFile -Encoding UTF8 | |
| - name: Pre-upload diagnostics (ensure SARIF exists) | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $outDir = Join-Path $env:GITHUB_WORKSPACE $env:OUT_DIR | |
| $sarifOut = Join-Path $outDir $env:OUT_SARIF | |
| Write-Host "GITHUB_WORKSPACE=$env:GITHUB_WORKSPACE" | |
| Write-Host "OUT_DIR=$outDir" | |
| if (Test-Path $outDir) { | |
| Get-ChildItem -Recurse -Force $outDir | Format-Table FullName, Length | |
| } else { | |
| New-Item -ItemType Directory -Force -Path $outDir | Out-Null | |
| } | |
| if (-not (Test-Path $sarifOut)) { | |
| Write-Host "SARIF missing — creating baseline now." | |
| $baseline = @{ | |
| '$schema' = 'https://json.schemastore.org/sarif-2.1.0.json' | |
| version = '2.1.0' | |
| runs = @(@{ tool = @{ driver = @{ name = 'PSScriptAnalyzer'; version = '0' } }; results = @() }) | |
| } | |
| $baseline | ConvertTo-Json -Depth 8 | Set-Content -Encoding UTF8 $sarifOut | |
| } | |
| exit 0 | |
| - name: Upload lint artifacts (never fail if empty) | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: powershell-lint-reports | |
| path: ${{ env.OUT_DIR }}/** | |
| if-no-files-found: warn | |
| retention-days: 30 | |
| - name: Upload SARIF to GitHub Code Scanning (report-only) | |
| if: always() && hashFiles(format('{0}/{1}', env.OUT_DIR, env.OUT_SARIF)) != '' | |
| uses: github/codeql-action/upload-sarif@v4 | |
| with: | |
| sarif_file: ${{ env.OUT_DIR }}/${{ env.OUT_SARIF }} | |
| category: ${{ env.SARIF_CATEGORY }} |