Skip to content

Update fmt.Println message from 'Hello' to 'Goodbye' #58

Update fmt.Println message from 'Hello' to 'Goodbye'

Update fmt.Println message from 'Hello' to 'Goodbye' #58

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 }}