From 0d755c28915c6ab867562164c194fe85afcd3dd9 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:17:56 +0100 Subject: [PATCH 01/20] chore: remove issue auto assign github action as now in issue templates --- .github/workflows/issue-auto-assign.yml | 32 ------------------------- 1 file changed, 32 deletions(-) delete mode 100644 .github/workflows/issue-auto-assign.yml diff --git a/.github/workflows/issue-auto-assign.yml b/.github/workflows/issue-auto-assign.yml deleted file mode 100644 index 436a919..0000000 --- a/.github/workflows/issue-auto-assign.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Auto label and assign new issues - -on: workflow_dispatch -jobs: - label_and_assign: - if: github.event.issue.user.login != 'blindzero' - runs-on: ubuntu-latest - permissions: - issues: write - - steps: - - name: Add label and assign issue - uses: actions/github-script@v8 - with: - script: | - const issueNumber = context.payload.issue.number; - const repo = context.repo.repo; - const owner = context.repo.owner; - - await github.rest.issues.addLabels({ - owner, - repo, - issue_number: issueNumber, - labels: ["new"] - }); - - await github.rest.issues.addAssignees({ - owner, - repo, - issue_number: issueNumber, - assignees: ["blindzero"] - }); From ed2c0fb16119004ade79ea3b7618eb6af63e8f46 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:26:45 +0100 Subject: [PATCH 02/20] ci: add PSScriptAnalyzer linting with repo settings --- tools/Invoke-IdlePesterTests.ps1 | 241 +++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 tools/Invoke-IdlePesterTests.ps1 diff --git a/tools/Invoke-IdlePesterTests.ps1 b/tools/Invoke-IdlePesterTests.ps1 new file mode 100644 index 0000000..7028639 --- /dev/null +++ b/tools/Invoke-IdlePesterTests.ps1 @@ -0,0 +1,241 @@ +<# +.SYNOPSIS +Runs IdLE Pester tests and optionally emits CI artifacts (test results + coverage). + +.DESCRIPTION +This script is the canonical entry point for running Pester in the IdLE repository. + +It is designed to be: +- deterministic (fixed artifact paths under repo root) +- CI-friendly (NUnitXml results + coverage output on demand) +- robust against different working directories (resolves paths relative to repo root) + +The script ensures Pester is available and imports it before running tests. + +.PARAMETER TestPath +Path to the tests folder. Defaults to 'tests' relative to the repository root. + +.PARAMETER CI +Enables CI mode: +- Writes NUnitXml test results to -TestResultsPath +- Enables code coverage and writes a coverage report to -CoverageOutputPath + +.PARAMETER TestResultsPath +Path to the NUnitXml test results file. Defaults to 'artifacts/test-results.xml' +relative to the repository root. + +.PARAMETER EnableCoverage +Enables code coverage when not running in -CI mode. + +.PARAMETER CoverageOutputPath +Path to the coverage report file. Defaults to 'artifacts/coverage.xml' +relative to the repository root. + +.PARAMETER CoverageOutputFormat +Coverage output format supported by Pester. + +.PARAMETER CoveragePath +One or more paths to include for coverage (e.g. 'src'). Defaults to 'src' +relative to the repository root. + +.PARAMETER MinimumPesterVersion +Minimum supported Pester version. The script will install Pester in CurrentUser scope +when missing or below the minimum version. + +.EXAMPLE +pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 + +.EXAMPLE +pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -CI + +.EXAMPLE +pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -EnableCoverage -CoverageOutputFormat Cobertura + +.OUTPUTS +None. Throws on failures and uses Pester exit codes when configured. +#> + +[CmdletBinding()] +param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $TestPath = 'tests', + + [Parameter()] + [switch] $CI, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $TestResultsPath = 'artifacts/test-results.xml', + + [Parameter()] + [switch] $EnableCoverage, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $CoverageOutputPath = 'artifacts/coverage.xml', + + [Parameter()] + [ValidateSet('JaCoCo', 'Cobertura', 'CoverageGutters')] + [string] $CoverageOutputFormat = 'JaCoCo', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string[]] $CoveragePath = @('src'), + + [Parameter()] + [ValidateNotNullOrEmpty()] + [version] $MinimumPesterVersion = '5.0.0' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Resolve-IdleRepoRoot { + <# + .SYNOPSIS + Resolves the repository root path. + + .DESCRIPTION + The repo root is assumed to be the parent directory of the 'tools' folder. + This avoids relying on the current working directory. + #> + [CmdletBinding()] + param() + + return (Resolve-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..')).Path +} + +function Get-IdleFullPath { + <# + .SYNOPSIS + Returns a full path for a repository-relative or absolute path. + + .DESCRIPTION + Resolve-Path fails if the path does not exist (e.g. output files). + This helper returns a normalized full path regardless of existence. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $RepoRootPath, + + [Parameter(Mandatory)] + [string] $Path + ) + + if ([System.IO.Path]::IsPathRooted($Path)) { + return [System.IO.Path]::GetFullPath($Path) + } + + return [System.IO.Path]::GetFullPath((Join-Path -Path $RepoRootPath -ChildPath $Path)) +} + +function Ensure-Directory { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } +} + +function Ensure-Pester { + <# + .SYNOPSIS + Ensures a compatible Pester module is installed and imported. + + .DESCRIPTION + CI runners are ephemeral. If Pester is missing, the script installs it + in CurrentUser scope. Local users can also benefit from auto-install. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [version] $MinimumVersion + ) + + $pester = Get-Module -ListAvailable -Name Pester | + Sort-Object Version -Descending | + Select-Object -First 1 + + if (-not $pester -or $pester.Version -lt $MinimumVersion) { + if (-not (Get-Command -Name Install-Module -ErrorAction SilentlyContinue)) { + throw "Pester >= $MinimumVersion is required, but Install-Module is not available. Install Pester manually and retry." + } + + Write-Host "Installing Pester >= $MinimumVersion (CurrentUser scope)..." + Install-Module -Name Pester -Scope CurrentUser -Force -MinimumVersion $MinimumVersion | Out-Null + } + + Import-Module -Name Pester -MinimumVersion $MinimumVersion -Force +} + +$repoRoot = Resolve-IdleRepoRoot + +$resolvedTestPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $TestPath +if (-not (Test-Path -LiteralPath $resolvedTestPath)) { + throw "TestPath does not exist: $resolvedTestPath" +} + +$emitTestResults = $CI.IsPresent +$coverageEnabled = $CI.IsPresent -or $EnableCoverage.IsPresent + +$resolvedTestResultsPath = $null +if ($emitTestResults) { + $resolvedTestResultsPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $TestResultsPath + Ensure-Directory -Path (Split-Path -Path $resolvedTestResultsPath -Parent) +} + +$resolvedCoverageOutputPath = $null +$resolvedCoveragePaths = @() + +if ($coverageEnabled) { + $resolvedCoverageOutputPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $CoverageOutputPath + Ensure-Directory -Path (Split-Path -Path $resolvedCoverageOutputPath -Parent) + + foreach ($p in $CoveragePath) { + $resolvedCoveragePaths += (Get-IdleFullPath -RepoRootPath $repoRoot -Path $p) + } +} + +Ensure-Pester -MinimumVersion $MinimumPesterVersion + +$config = New-PesterConfiguration +$config.Run.Path = $resolvedTestPath +$config.Run.Exit = $true +$config.Output.Verbosity = 'Detailed' + +if ($emitTestResults -and $resolvedTestResultsPath) { + $config.TestResult.Enabled = $true + $config.TestResult.OutputFormat = 'NUnitXml' + $config.TestResult.OutputPath = $resolvedTestResultsPath +} + +if ($coverageEnabled -and $resolvedCoverageOutputPath) { + if (-not $config.PSObject.Properties.Name.Contains('CodeCoverage')) { + throw "Installed Pester does not expose CodeCoverage configuration. Install a newer Pester version and retry." + } + + $config.CodeCoverage.Enabled = $true + $config.CodeCoverage.Path = $resolvedCoveragePaths + + if ($config.CodeCoverage.PSObject.Properties.Name.Contains('OutputPath')) { + $config.CodeCoverage.OutputPath = $resolvedCoverageOutputPath + } + else { + throw "Installed Pester does not support CodeCoverage.OutputPath. Install a newer Pester version and retry." + } + + if ($config.CodeCoverage.PSObject.Properties.Name.Contains('OutputFormat')) { + $config.CodeCoverage.OutputFormat = $CoverageOutputFormat + } + elseif ($CI) { + throw "Installed Pester does not support CodeCoverage.OutputFormat. Install a newer Pester version and retry." + } +} + +Invoke-Pester -Configuration $config From 96220be7b1d5d9d99818afd09cbee66dc396d41a Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:31:14 +0100 Subject: [PATCH 03/20] ci: run-tests as minimalistic backward wrapper --- tools/run-tests.ps1 | 71 ++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/tools/run-tests.ps1 b/tools/run-tests.ps1 index f05b807..96b2222 100644 --- a/tools/run-tests.ps1 +++ b/tools/run-tests.ps1 @@ -1,54 +1,53 @@ +<# +.SYNOPSIS +Legacy wrapper for running IdLE Pester tests. + +.DESCRIPTION +DEPRECATED: Use './tools/Invoke-IdlePesterTests.ps1' instead. + +This wrapper exists for backward compatibility because older documentation and +workflows still reference 'run-tests.ps1'. It will be removed in a future release. + +.PARAMETER TestPath +Path to the tests folder. Defaults to 'tests'. + +.PARAMETER CI +Enables CI mode. Passed through to Invoke-IdlePesterTests.ps1. + +.PARAMETER TestResultsPath +Path to the NUnitXml test results file. Passed through to Invoke-IdlePesterTests.ps1. + +.EXAMPLE +pwsh -NoProfile -File ./tools/run-tests.ps1 -CI +#> + [CmdletBinding()] param( [Parameter()] + [ValidateNotNullOrEmpty()] [string] $TestPath = 'tests', [Parameter()] [switch] $CI, [Parameter()] + [ValidateNotNullOrEmpty()] [string] $TestResultsPath = 'artifacts/test-results.xml' ) -Set-StrictMode -Off +Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -function Test-Pester { - [CmdletBinding()] - param( - [Parameter()] - [version] $MinimumVersion = '5.0.0' - ) - - $pester = Get-Module -ListAvailable -Name Pester | - Sort-Object Version -Descending | - Select-Object -First 1 - - if (-not $pester -or $pester.Version -lt $MinimumVersion) { - Write-Host "Installing Pester >= $MinimumVersion (CurrentUser scope)..." - Install-Module -Name Pester -Scope CurrentUser -Force -MinimumVersion $MinimumVersion - } - - Import-Module -Name Pester -MinimumVersion $MinimumVersion -Force +$targetScript = Join-Path -Path $PSScriptRoot -ChildPath 'Invoke-IdlePesterTests.ps1' +if (-not (Test-Path -LiteralPath $targetScript)) { + throw "Missing required script '$targetScript'. Ensure your working copy includes 'tools/Invoke-IdlePesterTests.ps1'." } -# Ensure output folder exists (for CI artifacts) -$resultsDir = Split-Path -Path $TestResultsPath -Parent -if ($resultsDir -and -not (Test-Path -Path $resultsDir)) { - New-Item -Path $resultsDir -ItemType Directory -Force | Out-Null -} - -Test-Pester -MinimumVersion '5.0.0' - -$config = New-PesterConfiguration -$config.Run.Path = $TestPath -$config.Run.Exit = $true -$config.Output.Verbosity = 'Detailed' - -if ($CI) { - $config.TestResult.Enabled = $true - $config.TestResult.OutputFormat = 'NUnitXml' - $config.TestResult.OutputPath = $TestResultsPath +if (-not $CI) { + Write-Warning "DEPRECATED: './tools/run-tests.ps1' is deprecated. Use './tools/Invoke-IdlePesterTests.ps1' instead." } -Invoke-Pester -Configuration $config +& $targetScript ` + -TestPath $TestPath ` + -CI:$CI ` + -TestResultsPath $TestResultsPath From a4f4bdfd5361953d3be23330cc12c6572383ca54 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:41:07 +0100 Subject: [PATCH 04/20] ci: change to new Invoke-IdlePesterTests.ps1 --- .github/workflows/ci.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 705d385..e33c852 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,15 +32,17 @@ jobs: - name: Run Pester shell: pwsh - run: pwsh -NoProfile -File ./tools/run-tests.ps1 -CI + run: pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -CI - - name: Upload test results + - name: Upload Pester artifacts if: always() - continue-on-error: true uses: actions/upload-artifact@v6 with: - name: test-results-${{ matrix.os }} - path: artifacts/test-results.xml + name: pester-artifacts-${{ matrix.os }} + if-no-files-found: warn + path: | + artifacts/test-results.xml + artifacts/coverage.xml docs-cmdlet-reference: name: Verify cmdlet reference is up to date @@ -63,12 +65,12 @@ jobs: # Ignore if not supported in this environment } } - + # platyPS is pinned for deterministic Markdown output. # See CONTRIBUTING.md for upgrade procedure. Install-Module -Name platyPS -RequiredVersion 0.14.2 -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop - + - name: Debug platyPS version shell: pwsh run: | From 6632857f6b3de16f55f6209aed54956eebea9f1f Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:47:09 +0100 Subject: [PATCH 05/20] ci: remove old run-tests.ps1 sript legacy --- tools/run-tests.ps1 | 53 --------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 tools/run-tests.ps1 diff --git a/tools/run-tests.ps1 b/tools/run-tests.ps1 deleted file mode 100644 index 96b2222..0000000 --- a/tools/run-tests.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -<# -.SYNOPSIS -Legacy wrapper for running IdLE Pester tests. - -.DESCRIPTION -DEPRECATED: Use './tools/Invoke-IdlePesterTests.ps1' instead. - -This wrapper exists for backward compatibility because older documentation and -workflows still reference 'run-tests.ps1'. It will be removed in a future release. - -.PARAMETER TestPath -Path to the tests folder. Defaults to 'tests'. - -.PARAMETER CI -Enables CI mode. Passed through to Invoke-IdlePesterTests.ps1. - -.PARAMETER TestResultsPath -Path to the NUnitXml test results file. Passed through to Invoke-IdlePesterTests.ps1. - -.EXAMPLE -pwsh -NoProfile -File ./tools/run-tests.ps1 -CI -#> - -[CmdletBinding()] -param( - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] $TestPath = 'tests', - - [Parameter()] - [switch] $CI, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [string] $TestResultsPath = 'artifacts/test-results.xml' -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$targetScript = Join-Path -Path $PSScriptRoot -ChildPath 'Invoke-IdlePesterTests.ps1' -if (-not (Test-Path -LiteralPath $targetScript)) { - throw "Missing required script '$targetScript'. Ensure your working copy includes 'tools/Invoke-IdlePesterTests.ps1'." -} - -if (-not $CI) { - Write-Warning "DEPRECATED: './tools/run-tests.ps1' is deprecated. Use './tools/Invoke-IdlePesterTests.ps1' instead." -} - -& $targetScript ` - -TestPath $TestPath ` - -CI:$CI ` - -TestResultsPath $TestResultsPath From 9e468adb772edcc94f589262860cf4738de456c4 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:54:44 +0100 Subject: [PATCH 06/20] ci: add repo PSScriptAnalyzer settings --- PSScriptAnalyzerSettings.psd1 | 58 +++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 PSScriptAnalyzerSettings.psd1 diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..d6385ee --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,58 @@ +# PSScriptAnalyzer settings for IdentityLifecycleEngine (IdLE) +# +# This file is intentionally data-only (no script blocks, no expressions). +# It is used by CI and can also be referenced from VS Code workspace settings. +# +# Notes: +# - We explicitly list IncludeRules to keep the first rollout focused and low-noise. +# - Formatting rules are enabled to align with STYLEGUIDE.md (4 spaces, consistent whitespace). + +@{ + Severity = @('Error', 'Warning') + + IncludeRules = @( + # Naming / API hygiene + 'PSUseApprovedVerbs', + 'PSAvoidGlobalVars', + 'PSAvoidUsingCmdletAliases', + 'PSAvoidUsingPositionalParameters', + 'PSUseCorrectCasing', + + # Common correctness issues + 'PSAvoidUsingEmptyCatchBlock', + 'PSReviewUnusedParameter', + 'PSUseDeclaredVarsMoreThanAssignments', + 'PSAvoidTrailingWhitespace', + + # Security / risky constructs + 'PSAvoidUsingInvokeExpression', + 'PSAvoidUsingPlainTextForPassword', + 'PSAvoidUsingConvertToSecureStringWithPlainText', + + # Style / formatting (enabled explicitly) + 'PSUseConsistentIndentation', + 'PSUseConsistentWhitespace' + ) + + Rules = @{ + PSUseConsistentIndentation = @{ + Enable = $true + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + Kind = 'space' + } + + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $false + CheckSeparator = $true + CheckParameter = $false + IgnoreAssignmentOperatorInsideHashTable = $false + } + } +} From 8f09b1ae45d6387d2ee5f187a02e37cea5f3fe23 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:58:54 +0100 Subject: [PATCH 07/20] ci: add Invoke-IdleScriptAnalyzer runner with pinned toolchain and optional SARIF --- tools/Invoke-IdleScriptAnalyzer.ps1 | 309 ++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 tools/Invoke-IdleScriptAnalyzer.ps1 diff --git a/tools/Invoke-IdleScriptAnalyzer.ps1 b/tools/Invoke-IdleScriptAnalyzer.ps1 new file mode 100644 index 0000000..3b31908 --- /dev/null +++ b/tools/Invoke-IdleScriptAnalyzer.ps1 @@ -0,0 +1,309 @@ +<# +.SYNOPSIS +Runs PSScriptAnalyzer for the IdLE repository with optional SARIF export. + +.DESCRIPTION +This script is the canonical entry point for running PSScriptAnalyzer in the IdLE repository. + +Design goals: +- Deterministic and CI-friendly defaults (repo-root settings, stable output paths) +- Optional SARIF output for GitHub Code Scanning integration +- No reliance on the current working directory +- Minimal dependencies with explicit version pinning + +By default, the script analyzes 'src' and 'tools' using the repository-root +'PSScriptAnalyzerSettings.psd1'. + +.PARAMETER Paths +One or more directories/files to analyze (relative to repo root by default). +Defaults to @('src', 'tools'). + +.PARAMETER SettingsPath +Path to the PSScriptAnalyzer settings file (relative to repo root by default). +Defaults to 'PSScriptAnalyzerSettings.psd1'. + +.PARAMETER CI +Enables CI mode: +- Writes a JSON summary file to -JsonOutputPath (default under artifacts/) +- Writes SARIF when -SarifOutputPath is provided (default under artifacts/) +- Fails the run when findings meet -FailOnSeverity (default: Error) + +.PARAMETER JsonOutputPath +Where to write a machine-readable JSON summary of findings +(relative to repo root by default). Defaults to 'artifacts/pssa-results.json'. + +.PARAMETER SarifOutputPath +When provided, writes SARIF output to this path (relative to repo root by default). +Defaults to 'artifacts/pssa-results.sarif'. + +.PARAMETER FailOnSeverity +Controls which findings should fail the run. Defaults to 'Error'. +Valid values: Error, Warning + +.PARAMETER PSScriptAnalyzerVersion +Pinned PSScriptAnalyzer version to use. Defaults to 1.24.0. + +.PARAMETER ConvertToSarifVersion +Pinned ConvertToSARIF module version to use (for SARIF export). Defaults to 1.0.0. + +.EXAMPLE +pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1 + +.EXAMPLE +pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1 -CI + +.EXAMPLE +pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1 -CI -SarifOutputPath artifacts/pssa.sarif + +.OUTPUTS +None. Writes optional output files and throws on failure. +#> + +[CmdletBinding()] +param( + [Parameter()] + [ValidateNotNullOrEmpty()] + [string[]] $Paths = @('src', 'tools'), + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $SettingsPath = 'PSScriptAnalyzerSettings.psd1', + + [Parameter()] + [switch] $CI, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $JsonOutputPath = 'artifacts/pssa-results.json', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string] $SarifOutputPath = 'artifacts/pssa-results.sarif', + + [Parameter()] + [ValidateSet('Error', 'Warning')] + [string] $FailOnSeverity = 'Error', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [version] $PSScriptAnalyzerVersion = '1.24.0', + + [Parameter()] + [ValidateNotNullOrEmpty()] + [version] $ConvertToSarifVersion = '1.0.0' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Resolve-IdleRepoRoot { + <# + .SYNOPSIS + Resolves the repository root path. + + .DESCRIPTION + The repo root is assumed to be the parent directory of the 'tools' folder. + This avoids relying on the current working directory. + #> + [CmdletBinding()] + param() + + return (Resolve-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..')).Path +} + +function Get-IdleFullPath { + <# + .SYNOPSIS + Returns a full path for a repository-relative or absolute path. + + .DESCRIPTION + Resolve-Path fails if the path does not exist (e.g. output files). + This helper returns a normalized full path regardless of existence. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $RepoRootPath, + + [Parameter(Mandatory)] + [string] $Path + ) + + if ([System.IO.Path]::IsPathRooted($Path)) { + return [System.IO.Path]::GetFullPath($Path) + } + + return [System.IO.Path]::GetFullPath((Join-Path -Path $RepoRootPath -ChildPath $Path)) +} + +function Ensure-Directory { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path + ) + + if (-not (Test-Path -LiteralPath $Path)) { + New-Item -Path $Path -ItemType Directory -Force | Out-Null + } +} + +function Ensure-Module { + <# + .SYNOPSIS + Ensures a module is installed (pinned version) and imported. + + .DESCRIPTION + CI runners are ephemeral. When missing, we install the module in CurrentUser scope. + We explicitly pin versions for determinism. + + IMPORTANT: + - We keep this logic self-contained and consistent across local + CI runs. + - We avoid auto-upgrading to newer versions unless the pinned version is changed in code. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Name, + + [Parameter(Mandatory)] + [version] $RequiredVersion + ) + + $installed = Get-Module -ListAvailable -Name $Name | + Where-Object { $_.Version -eq $RequiredVersion } | + Select-Object -First 1 + + if (-not $installed) { + if (-not (Get-Command -Name Install-Module -ErrorAction SilentlyContinue)) { + throw "Module '$Name' ($RequiredVersion) is required, but Install-Module is not available. Install the module manually and retry." + } + + Write-Host "Installing module '$Name' ($RequiredVersion) in CurrentUser scope..." + Install-Module -Name $Name -Scope CurrentUser -Force -RequiredVersion $RequiredVersion -AllowClobber | Out-Null + } + + Import-Module -Name $Name -RequiredVersion $RequiredVersion -Force +} + +function Write-JsonFile { + <# + .SYNOPSIS + Writes a JSON file with deterministic encoding. + + .DESCRIPTION + PowerShell defaults can differ across versions. We enforce UTF8 without BOM and + ensure we always write a file (even when there are no findings). + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Path, + + [Parameter(Mandatory)] + [object] $Object + ) + + $json = $Object | ConvertTo-Json -Depth 10 + $encoding = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($Path, $json, $encoding) +} + +$repoRoot = Resolve-IdleRepoRoot + +# Resolve and validate settings path. +$resolvedSettingsPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $SettingsPath +if (-not (Test-Path -LiteralPath $resolvedSettingsPath)) { + throw "PSScriptAnalyzer settings file not found: $resolvedSettingsPath" +} + +# Resolve analysis paths relative to repo root. +$resolvedPaths = @() +foreach ($p in $Paths) { + $full = Get-IdleFullPath -RepoRootPath $repoRoot -Path $p + if (-not (Test-Path -LiteralPath $full)) { + throw "Analysis path not found: $full" + } + + $resolvedPaths += $full +} + +# Resolve output paths. We always write JSON in CI mode for artifact upload. +$resolvedJsonOutputPath = $null +if ($CI) { + $resolvedJsonOutputPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $JsonOutputPath + Ensure-Directory -Path (Split-Path -Path $resolvedJsonOutputPath -Parent) +} + +# SARIF is optional: only generate when CI is on (or user explicitly wants it later) +# AND ConvertToSARIF is available. +$resolvedSarifOutputPath = $null +$emitSarif = $false +if ($CI -and $SarifOutputPath) { + $resolvedSarifOutputPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $SarifOutputPath + Ensure-Directory -Path (Split-Path -Path $resolvedSarifOutputPath -Parent) + $emitSarif = $true +} + +# Ensure analyzer module is present (pinned). +Ensure-Module -Name 'PSScriptAnalyzer' -RequiredVersion $PSScriptAnalyzerVersion + +# Run analysis using the repo settings file. +# We rely on the settings file for rule selection and severities. +Write-Host "Running PSScriptAnalyzer ($PSScriptAnalyzerVersion) using settings: $resolvedSettingsPath" +Write-Host "Analyzing paths:" +$resolvedPaths | ForEach-Object { Write-Host " - $_" } + +$findings = Invoke-ScriptAnalyzer -Path $resolvedPaths -Recurse -Settings $resolvedSettingsPath + +# Create a stable, small JSON payload (DiagnosticRecord contains complex members). +$summary = @( + foreach ($f in ($findings | Sort-Object ScriptName, Line, Column, RuleName)) { + [pscustomobject]@{ + RuleName = $f.RuleName + Severity = $f.Severity + Message = $f.Message + ScriptName = $f.ScriptName + Line = $f.Line + Column = $f.Column + } + } +) + +if ($CI -and $resolvedJsonOutputPath) { + Write-Host "Writing PSScriptAnalyzer JSON results: $resolvedJsonOutputPath" + Write-JsonFile -Path $resolvedJsonOutputPath -Object $summary +} + +if ($emitSarif -and $resolvedSarifOutputPath) { + # ConvertToSARIF provides the ConvertTo-SARIF cmdlet which accepts -FilePath. + # We install it only when SARIF output is requested. + Ensure-Module -Name 'ConvertToSARIF' -RequiredVersion $ConvertToSarifVersion + + $convertCommand = Get-Command -Name 'ConvertTo-SARIF' -ErrorAction SilentlyContinue + if (-not $convertCommand) { + throw "ConvertToSARIF module is installed, but 'ConvertTo-SARIF' cmdlet was not found." + } + + Write-Host "Writing SARIF results: $resolvedSarifOutputPath" + $findings | & $convertCommand -FilePath $resolvedSarifOutputPath +} + +# Determine whether we should fail this run. +$failSeverities = @($FailOnSeverity) +if ($FailOnSeverity -eq 'Warning') { + # Warning implies: fail on both Warning and Error. + $failSeverities = @('Warning', 'Error') +} + +$blockingFindings = $findings | Where-Object { $failSeverities -contains $_.Severity } + +if ($blockingFindings) { + $errorCount = ($findings | Where-Object { $_.Severity -eq 'Error' }).Count + $warningCount = ($findings | Where-Object { $_.Severity -eq 'Warning' }).Count + + $message = "PSScriptAnalyzer found blocking issues (FailOnSeverity: $FailOnSeverity). Errors: $errorCount, Warnings: $warningCount." + throw $message +} + +Write-Host "PSScriptAnalyzer completed with no blocking findings (FailOnSeverity: $FailOnSeverity)." From c8fe007adfb8d90fe0f9084c735d874ff65da01d Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:01:24 +0100 Subject: [PATCH 08/20] ci: add lint job and switch tests to Invoke-IdlePesterTests; remove run-tests --- .github/workflows/ci.yml | 63 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e33c852..7fe62c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,6 @@ jobs: name: Pester (${{ matrix.os }}) runs-on: ${{ matrix.os }} - # add job-scoped permissions needed for artifact upload permissions: contents: read actions: write @@ -44,6 +43,67 @@ jobs: artifacts/test-results.xml artifacts/coverage.xml + lint: + name: PSScriptAnalyzer + runs-on: ubuntu-latest + + permissions: + contents: read + actions: read + security-events: write + + steps: + - uses: actions/checkout@v6 + + - name: Run PSScriptAnalyzer + shell: pwsh + run: pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1 -CI + + - name: Upload PSScriptAnalyzer artifacts + if: always() + uses: actions/upload-artifact@v6 + with: + name: psscriptanalyzer-artifacts + if-no-files-found: warn + path: | + artifacts/pssa-results.json + artifacts/pssa-results.sarif + + - name: Decide whether to upload SARIF + id: sarif_check + shell: pwsh + run: | + Set-StrictMode -Version Latest + $ErrorActionPreference = 'Stop' + + $jsonPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'artifacts/pssa-results.json' + $sarifPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'artifacts/pssa-results.sarif' + + $shouldUpload = $false + + if (Test-Path -LiteralPath $jsonPath -and Test-Path -LiteralPath $sarifPath) { + $content = Get-Content -LiteralPath $jsonPath -Raw + if ($content) { + $items = $content | ConvertFrom-Json + if ($null -ne $items) { + # ConvertFrom-Json returns PSCustomObject for single element, array for multiple. + if ($items -is [System.Array]) { + $shouldUpload = ($items.Count -gt 0) + } else { + $shouldUpload = $true + } + } + } + } + + "should_upload_sarif=$($shouldUpload.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + - name: Upload SARIF to GitHub Code Scanning + if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.sarif_check.outputs.should_upload_sarif == 'true' + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: artifacts/pssa-results.sarif + docs-cmdlet-reference: name: Verify cmdlet reference is up to date runs-on: ubuntu-latest @@ -68,7 +128,6 @@ jobs: # platyPS is pinned for deterministic Markdown output. # See CONTRIBUTING.md for upgrade procedure. - Install-Module -Name platyPS -RequiredVersion 0.14.2 -Scope CurrentUser -Force -AllowClobber -ErrorAction Stop - name: Debug platyPS version From 300f7b67bd2f22f3cfa834405d471a7559544c06 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:02:07 +0100 Subject: [PATCH 09/20] docs: updated releases.md to new Invoke-IdlePesterTests.ps1 --- docs/advanced/releases.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index e69c2c0..5912481 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -66,7 +66,7 @@ Pre-release tags do **not** publish to PowerShell Gallery. 3. Run tests: ```powershell - pwsh -NoProfile -File ./tools/run-tests.ps1 + pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 ``` 4. Commit and push the changes. From b86f5782611c746c71f1b511945027111fd35e8c Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:08:43 +0100 Subject: [PATCH 10/20] docs: updating to new Pester Tests adding Script Analyzers infos --- CONTRIBUTING.md | 51 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59dd703..b300db6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,14 +123,54 @@ Pull Requests must: A contribution is complete when: -- all tests pass (`Invoke-Pester -Path ./tests`) -- no architecture rules are violated (see `docs/01-architecture.md`) +- all tests pass (`pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1`) +- static analysis passes (`pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1`) +- no architecture rules are violated (see `docs/advanced/architecture.md`) - public APIs are documented (comment-based help for exported functions) - documentation is updated where required: - README.md (only high-level overview + pointers) - docs/ (usage/concepts/examples) - provider/step module READMEs if behavior/auth changes +## Local quality checks + +IdLE provides canonical scripts under `tools/` so you can reproduce the same checks locally that CI runs. + +### Run tests (Pester) + +Run the test suite: + +- `pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1` + +To generate CI-like artifacts (test results + coverage) under `artifacts/`: + +- `pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -CI` + +Outputs: + +- `artifacts/test-results.xml` (NUnitXml) +- `artifacts/coverage.xml` (coverage report) + +### Run static analysis (PSScriptAnalyzer) + +Run PSScriptAnalyzer using the repository settings: + +- `pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1` + +To generate CI-like artifacts under `artifacts/` (including SARIF for GitHub Code Scanning): + +- `pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1 -CI` + +Outputs: + +- `artifacts/pssa-results.json` (summary) +- `artifacts/pssa-results.sarif` (SARIF) + +The rule set is defined in `PSScriptAnalyzerSettings.psd1` at the repository root. +The runner pins tool versions for deterministic CI results; update pins intentionally and document the change in the PR. + +> Note: `artifacts/` is a build output folder and should not be committed. + --- ## Generated cmdlet reference (platyPS) @@ -253,11 +293,6 @@ To fix a failing PR: Repository maintainers should configure branch protection so that required status checks include this workflow. - - - - - ## Documentation Keep docs short and linkable: @@ -269,7 +304,7 @@ Keep docs short and linkable: Key links: - Docs map: `docs/00-index.md` -- Architecture: `docs/01-architecture.md` +- Architecture: `docs/advanced/architecture.md` - Examples: `docs/02-examples.md` - Coding & in-code documentation rules: `STYLEGUIDE.md` From 83b9c072f377cbf6d7b7c3a15ffc9e74f38b2414 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:08:43 +0100 Subject: [PATCH 11/20] docs: updating to new Pester Tests adding Script Analyzers infos --- CONTRIBUTING.md | 51 +++++++++++++++++++++++++++++++++++++++++-------- STYLEGUIDE.md | 14 ++++++++++++++ 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 59dd703..b300db6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -123,14 +123,54 @@ Pull Requests must: A contribution is complete when: -- all tests pass (`Invoke-Pester -Path ./tests`) -- no architecture rules are violated (see `docs/01-architecture.md`) +- all tests pass (`pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1`) +- static analysis passes (`pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1`) +- no architecture rules are violated (see `docs/advanced/architecture.md`) - public APIs are documented (comment-based help for exported functions) - documentation is updated where required: - README.md (only high-level overview + pointers) - docs/ (usage/concepts/examples) - provider/step module READMEs if behavior/auth changes +## Local quality checks + +IdLE provides canonical scripts under `tools/` so you can reproduce the same checks locally that CI runs. + +### Run tests (Pester) + +Run the test suite: + +- `pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1` + +To generate CI-like artifacts (test results + coverage) under `artifacts/`: + +- `pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 -CI` + +Outputs: + +- `artifacts/test-results.xml` (NUnitXml) +- `artifacts/coverage.xml` (coverage report) + +### Run static analysis (PSScriptAnalyzer) + +Run PSScriptAnalyzer using the repository settings: + +- `pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1` + +To generate CI-like artifacts under `artifacts/` (including SARIF for GitHub Code Scanning): + +- `pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1 -CI` + +Outputs: + +- `artifacts/pssa-results.json` (summary) +- `artifacts/pssa-results.sarif` (SARIF) + +The rule set is defined in `PSScriptAnalyzerSettings.psd1` at the repository root. +The runner pins tool versions for deterministic CI results; update pins intentionally and document the change in the PR. + +> Note: `artifacts/` is a build output folder and should not be committed. + --- ## Generated cmdlet reference (platyPS) @@ -253,11 +293,6 @@ To fix a failing PR: Repository maintainers should configure branch protection so that required status checks include this workflow. - - - - - ## Documentation Keep docs short and linkable: @@ -269,7 +304,7 @@ Keep docs short and linkable: Key links: - Docs map: `docs/00-index.md` -- Architecture: `docs/01-architecture.md` +- Architecture: `docs/advanced/architecture.md` - Examples: `docs/02-examples.md` - Coding & in-code documentation rules: `STYLEGUIDE.md` diff --git a/STYLEGUIDE.md b/STYLEGUIDE.md index 4ccd8f3..efb8a2f 100644 --- a/STYLEGUIDE.md +++ b/STYLEGUIDE.md @@ -99,6 +99,20 @@ Providers: - No live system calls in unit tests - Providers require contract tests +## Quality Gates + +IdLE uses static analysis and automated tests to keep the codebase consistent and maintainable. + +- **PSScriptAnalyzer** is the required linter for PowerShell code. + - Repository policy is defined in `PSScriptAnalyzerSettings.psd1` (repo root). + - Run locally via `pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1`. + - CI publishes analyzer outputs under `artifacts/`. + - On default-branch runs, CI also uploads SARIF to GitHub Code Scanning. + +- **Pester** is the required test framework. + - Run locally via `pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1`. + - CI publishes test results and coverage under `artifacts/`. + --- ## Documentation Responsibilities From 3c2c6f3dc70fb9f24e75a85699d7e4afbca5e942 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:19:05 +0100 Subject: [PATCH 12/20] ci: fixing multiple path invoaction with foreach --- tools/Invoke-IdleScriptAnalyzer.ps1 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/Invoke-IdleScriptAnalyzer.ps1 b/tools/Invoke-IdleScriptAnalyzer.ps1 index 3b31908..0b29d1b 100644 --- a/tools/Invoke-IdleScriptAnalyzer.ps1 +++ b/tools/Invoke-IdleScriptAnalyzer.ps1 @@ -254,7 +254,10 @@ Write-Host "Running PSScriptAnalyzer ($PSScriptAnalyzerVersion) using settings: Write-Host "Analyzing paths:" $resolvedPaths | ForEach-Object { Write-Host " - $_" } -$findings = Invoke-ScriptAnalyzer -Path $resolvedPaths -Recurse -Settings $resolvedSettingsPath +$findings = @() +foreach ($path in $resolvedPaths) { + $findings += Invoke-ScriptAnalyzer -Path $path -Recurse -Settings $resolvedSettingsPath +} # Create a stable, small JSON payload (DiagnosticRecord contains complex members). $summary = @( From 43b50daa382d136f5ce15117ddf87a13a5410328 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:19:05 +0100 Subject: [PATCH 13/20] ci: fixing multiple path invoaction with foreach --- docs/advanced/releases.md | 16 ++++++++++++---- tools/Invoke-IdleScriptAnalyzer.ps1 | 5 ++++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/advanced/releases.md b/docs/advanced/releases.md index 5912481..3f77899 100644 --- a/docs/advanced/releases.md +++ b/docs/advanced/releases.md @@ -63,11 +63,19 @@ Pre-release tags do **not** publish to PowerShell Gallery. pwsh -NoProfile -File ./tools/Set-IdleModuleVersion.ps1 -TargetVersion 1.2.0 ``` -3. Run tests: +3. Run quality checks locally: - ```powershell - pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 - ``` + - Pester tests: + + ```powershell + pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 + ``` + + - Static analysis (PSScriptAnalyzer): + + ```powershell + pwsh -NoProfile -File ./tools/Invoke-IdleScriptAnalyzer.ps1 + ``` 4. Commit and push the changes. 5. Open a Pull Request to `main` and wait for CI to pass. diff --git a/tools/Invoke-IdleScriptAnalyzer.ps1 b/tools/Invoke-IdleScriptAnalyzer.ps1 index 3b31908..0b29d1b 100644 --- a/tools/Invoke-IdleScriptAnalyzer.ps1 +++ b/tools/Invoke-IdleScriptAnalyzer.ps1 @@ -254,7 +254,10 @@ Write-Host "Running PSScriptAnalyzer ($PSScriptAnalyzerVersion) using settings: Write-Host "Analyzing paths:" $resolvedPaths | ForEach-Object { Write-Host " - $_" } -$findings = Invoke-ScriptAnalyzer -Path $resolvedPaths -Recurse -Settings $resolvedSettingsPath +$findings = @() +foreach ($path in $resolvedPaths) { + $findings += Invoke-ScriptAnalyzer -Path $path -Recurse -Settings $resolvedSettingsPath +} # Create a stable, small JSON payload (DiagnosticRecord contains complex members). $summary = @( From 49911bc6f2d1f81d8ad9a25f00a661b9fb2556bc Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:21:38 +0100 Subject: [PATCH 14/20] ci: fixing .count() issue with entitlement tests --- tests/ProviderContracts/EntitlementProvider.Contract.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/ProviderContracts/EntitlementProvider.Contract.ps1 b/tests/ProviderContracts/EntitlementProvider.Contract.ps1 index 191f8ec..13a53aa 100644 --- a/tests/ProviderContracts/EntitlementProvider.Contract.ps1 +++ b/tests/ProviderContracts/EntitlementProvider.Contract.ps1 @@ -117,8 +117,8 @@ function Invoke-IdleEntitlementProviderContractTests { [void]$script:Provider.RevokeEntitlement($id, $entitlement) $afterRevoke = @($script:Provider.ListEntitlements($id)) - ($afterGrant | Where-Object { $_.Kind -eq $entitlement.Kind -and $_.Id -eq $entitlement.Id }).Count | Should -Be 1 - ($afterRevoke | Where-Object { $_.Kind -eq $entitlement.Kind -and $_.Id -eq $entitlement.Id }).Count | Should -Be 0 + @($afterGrant | Where-Object { $_.Kind -eq $entitlement.Kind -and $_.Id -eq $entitlement.Id }).Count | Should -Be 1 + @($afterRevoke | Where-Object { $_.Kind -eq $entitlement.Kind -and $_.Id -eq $entitlement.Id }).Count | Should -Be 0 # Sanity: $null is treated as empty. ($before -is [object[]]) | Should -BeTrue From 557d64ed0b0f7ed7796503f5a2d3f55539785d24 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:35:32 +0100 Subject: [PATCH 15/20] ci: fixed missing paranthesis issue in check SARIF upload step --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fe62c6..8eb1dba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,7 @@ jobs: $shouldUpload = $false - if (Test-Path -LiteralPath $jsonPath -and Test-Path -LiteralPath $sarifPath) { + if ((Test-Path -LiteralPath $jsonPath) -and (Test-Path -LiteralPath $sarifPath)) { $content = Get-Content -LiteralPath $jsonPath -Raw if ($content) { $items = $content | ConvertFrom-Json From 4a5ba02bd160ba79e1a58a9343aca8d94f3ace97 Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 00:41:02 +0100 Subject: [PATCH 16/20] ci: remove check to upload SARIF to always upload (for create + auto-close) --- .github/workflows/ci.yml | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8eb1dba..8455b75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,37 +69,8 @@ jobs: artifacts/pssa-results.json artifacts/pssa-results.sarif - - name: Decide whether to upload SARIF - id: sarif_check - shell: pwsh - run: | - Set-StrictMode -Version Latest - $ErrorActionPreference = 'Stop' - - $jsonPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'artifacts/pssa-results.json' - $sarifPath = Join-Path -Path $env:GITHUB_WORKSPACE -ChildPath 'artifacts/pssa-results.sarif' - - $shouldUpload = $false - - if ((Test-Path -LiteralPath $jsonPath) -and (Test-Path -LiteralPath $sarifPath)) { - $content = Get-Content -LiteralPath $jsonPath -Raw - if ($content) { - $items = $content | ConvertFrom-Json - if ($null -ne $items) { - # ConvertFrom-Json returns PSCustomObject for single element, array for multiple. - if ($items -is [System.Array]) { - $shouldUpload = ($items.Count -gt 0) - } else { - $shouldUpload = $true - } - } - } - } - - "should_upload_sarif=$($shouldUpload.ToString().ToLowerInvariant())" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - - name: Upload SARIF to GitHub Code Scanning - if: github.event_name == 'push' && github.ref == 'refs/heads/main' && steps.sarif_check.outputs.should_upload_sarif == 'true' + if: github.event_name == 'push' && github.ref == 'refs/heads/main' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: artifacts/pssa-results.sarif From ae62818bce1fd7ee73fb6d12e7bbb4d30c9198e5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:44:22 +0000 Subject: [PATCH 17/20] Initial plan From d79e67ca18fbdda619d859c8da46eb57e0ade51a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:48:32 +0000 Subject: [PATCH 18/20] fix: pin Pester to specific version 5.7.1 for supply-chain security Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tools/Invoke-IdlePesterTests.ps1 | 35 +++++++++++++++++--------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/tools/Invoke-IdlePesterTests.ps1 b/tools/Invoke-IdlePesterTests.ps1 index 7028639..3e71bd1 100644 --- a/tools/Invoke-IdlePesterTests.ps1 +++ b/tools/Invoke-IdlePesterTests.ps1 @@ -38,9 +38,8 @@ Coverage output format supported by Pester. One or more paths to include for coverage (e.g. 'src'). Defaults to 'src' relative to the repository root. -.PARAMETER MinimumPesterVersion -Minimum supported Pester version. The script will install Pester in CurrentUser scope -when missing or below the minimum version. +.PARAMETER PesterVersion +Pinned Pester version to use. Defaults to 5.7.1. .EXAMPLE pwsh -NoProfile -File ./tools/Invoke-IdlePesterTests.ps1 @@ -85,7 +84,7 @@ param( [Parameter()] [ValidateNotNullOrEmpty()] - [version] $MinimumPesterVersion = '5.0.0' + [version] $PesterVersion = '5.7.1' ) Set-StrictMode -Version Latest @@ -146,32 +145,36 @@ function Ensure-Directory { function Ensure-Pester { <# .SYNOPSIS - Ensures a compatible Pester module is installed and imported. + Ensures Pester is installed (pinned version) and imported. .DESCRIPTION - CI runners are ephemeral. If Pester is missing, the script installs it - in CurrentUser scope. Local users can also benefit from auto-install. + CI runners are ephemeral. When missing, we install Pester in CurrentUser scope. + We explicitly pin versions for determinism. + + IMPORTANT: + - We keep this logic self-contained and consistent across local + CI runs. + - We avoid auto-upgrading to newer versions unless the pinned version is changed in code. #> [CmdletBinding()] param( [Parameter(Mandatory)] - [version] $MinimumVersion + [version] $RequiredVersion ) - $pester = Get-Module -ListAvailable -Name Pester | - Sort-Object Version -Descending | + $installed = Get-Module -ListAvailable -Name Pester | + Where-Object { $_.Version -eq $RequiredVersion } | Select-Object -First 1 - if (-not $pester -or $pester.Version -lt $MinimumVersion) { + if (-not $installed) { if (-not (Get-Command -Name Install-Module -ErrorAction SilentlyContinue)) { - throw "Pester >= $MinimumVersion is required, but Install-Module is not available. Install Pester manually and retry." + throw "Pester ($RequiredVersion) is required, but Install-Module is not available. Install Pester manually and retry." } - Write-Host "Installing Pester >= $MinimumVersion (CurrentUser scope)..." - Install-Module -Name Pester -Scope CurrentUser -Force -MinimumVersion $MinimumVersion | Out-Null + Write-Host "Installing Pester ($RequiredVersion) in CurrentUser scope..." + Install-Module -Name Pester -Scope CurrentUser -Force -RequiredVersion $RequiredVersion -AllowClobber | Out-Null } - Import-Module -Name Pester -MinimumVersion $MinimumVersion -Force + Import-Module -Name Pester -RequiredVersion $RequiredVersion -Force } $repoRoot = Resolve-IdleRepoRoot @@ -202,7 +205,7 @@ if ($coverageEnabled) { } } -Ensure-Pester -MinimumVersion $MinimumPesterVersion +Ensure-Pester -RequiredVersion $PesterVersion $config = New-PesterConfiguration $config.Run.Path = $resolvedTestPath From 7818a342290d8e377aaf7f92a3dc9cf6ed0add54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 10 Jan 2026 23:53:56 +0000 Subject: [PATCH 19/20] refactor: rename Ensure-* functions to Initialize-* to use approved verbs Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- tools/Invoke-IdlePesterTests.ps1 | 12 ++++++------ tools/Invoke-IdleScriptAnalyzer.ps1 | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tools/Invoke-IdlePesterTests.ps1 b/tools/Invoke-IdlePesterTests.ps1 index 3e71bd1..d37a7a1 100644 --- a/tools/Invoke-IdlePesterTests.ps1 +++ b/tools/Invoke-IdlePesterTests.ps1 @@ -130,7 +130,7 @@ function Get-IdleFullPath { return [System.IO.Path]::GetFullPath((Join-Path -Path $RepoRootPath -ChildPath $Path)) } -function Ensure-Directory { +function Initialize-Directory { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -142,10 +142,10 @@ function Ensure-Directory { } } -function Ensure-Pester { +function Initialize-Pester { <# .SYNOPSIS - Ensures Pester is installed (pinned version) and imported. + Initializes Pester by ensuring it is installed (pinned version) and imported. .DESCRIPTION CI runners are ephemeral. When missing, we install Pester in CurrentUser scope. @@ -190,7 +190,7 @@ $coverageEnabled = $CI.IsPresent -or $EnableCoverage.IsPresent $resolvedTestResultsPath = $null if ($emitTestResults) { $resolvedTestResultsPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $TestResultsPath - Ensure-Directory -Path (Split-Path -Path $resolvedTestResultsPath -Parent) + Initialize-Directory -Path (Split-Path -Path $resolvedTestResultsPath -Parent) } $resolvedCoverageOutputPath = $null @@ -198,14 +198,14 @@ $resolvedCoveragePaths = @() if ($coverageEnabled) { $resolvedCoverageOutputPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $CoverageOutputPath - Ensure-Directory -Path (Split-Path -Path $resolvedCoverageOutputPath -Parent) + Initialize-Directory -Path (Split-Path -Path $resolvedCoverageOutputPath -Parent) foreach ($p in $CoveragePath) { $resolvedCoveragePaths += (Get-IdleFullPath -RepoRootPath $repoRoot -Path $p) } } -Ensure-Pester -RequiredVersion $PesterVersion +Initialize-Pester -RequiredVersion $PesterVersion $config = New-PesterConfiguration $config.Run.Path = $resolvedTestPath diff --git a/tools/Invoke-IdleScriptAnalyzer.ps1 b/tools/Invoke-IdleScriptAnalyzer.ps1 index 0b29d1b..b066495 100644 --- a/tools/Invoke-IdleScriptAnalyzer.ps1 +++ b/tools/Invoke-IdleScriptAnalyzer.ps1 @@ -136,7 +136,7 @@ function Get-IdleFullPath { return [System.IO.Path]::GetFullPath((Join-Path -Path $RepoRootPath -ChildPath $Path)) } -function Ensure-Directory { +function Initialize-Directory { [CmdletBinding()] param( [Parameter(Mandatory)] @@ -148,10 +148,10 @@ function Ensure-Directory { } } -function Ensure-Module { +function Initialize-Module { <# .SYNOPSIS - Ensures a module is installed (pinned version) and imported. + Initializes a module by ensuring it is installed (pinned version) and imported. .DESCRIPTION CI runners are ephemeral. When missing, we install the module in CurrentUser scope. @@ -232,7 +232,7 @@ foreach ($p in $Paths) { $resolvedJsonOutputPath = $null if ($CI) { $resolvedJsonOutputPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $JsonOutputPath - Ensure-Directory -Path (Split-Path -Path $resolvedJsonOutputPath -Parent) + Initialize-Directory -Path (Split-Path -Path $resolvedJsonOutputPath -Parent) } # SARIF is optional: only generate when CI is on (or user explicitly wants it later) @@ -241,12 +241,12 @@ $resolvedSarifOutputPath = $null $emitSarif = $false if ($CI -and $SarifOutputPath) { $resolvedSarifOutputPath = Get-IdleFullPath -RepoRootPath $repoRoot -Path $SarifOutputPath - Ensure-Directory -Path (Split-Path -Path $resolvedSarifOutputPath -Parent) + Initialize-Directory -Path (Split-Path -Path $resolvedSarifOutputPath -Parent) $emitSarif = $true } # Ensure analyzer module is present (pinned). -Ensure-Module -Name 'PSScriptAnalyzer' -RequiredVersion $PSScriptAnalyzerVersion +Initialize-Module -Name 'PSScriptAnalyzer' -RequiredVersion $PSScriptAnalyzerVersion # Run analysis using the repo settings file. # We rely on the settings file for rule selection and severities. @@ -281,7 +281,7 @@ if ($CI -and $resolvedJsonOutputPath) { if ($emitSarif -and $resolvedSarifOutputPath) { # ConvertToSARIF provides the ConvertTo-SARIF cmdlet which accepts -FilePath. # We install it only when SARIF output is requested. - Ensure-Module -Name 'ConvertToSARIF' -RequiredVersion $ConvertToSarifVersion + Initialize-Module -Name 'ConvertToSARIF' -RequiredVersion $ConvertToSarifVersion $convertCommand = Get-Command -Name 'ConvertTo-SARIF' -ErrorAction SilentlyContinue if (-not $convertCommand) { From 0d6eca9753c65375fb8b088dd43f5e26e1162dfa Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Sun, 11 Jan 2026 01:02:35 +0100 Subject: [PATCH 20/20] ci: let SARIF upload run always, even if ScriptAnalyzer fails. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8455b75..5277b7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,7 @@ jobs: artifacts/pssa-results.sarif - name: Upload SARIF to GitHub Code Scanning - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: always() && github.event_name == 'push' && github.ref == 'refs/heads/main' uses: github/codeql-action/upload-sarif@v4 with: sarif_file: artifacts/pssa-results.sarif