Skip to content

Commit 1555ad1

Browse files
tablackburnclaude
andcommitted
fix: restore Windows PowerShell 5.1 import compatibility
Get-PSBuildCertificate used the PowerShell 7+-only ternary operator, which fails to parse under Windows PowerShell 5.1. The module loader dot-sources every Public/*.ps1 at import, so this file broke import of the entire module on 5.1 (Desktop) even though the manifest still declares support for it. - Replace the ternary with an if/else expression (5.1-safe). - Make the $IsWindows platform guard 5.1-safe using the repo's existing idiom ($null -ne $IsWindows -and -not $IsWindows); on Desktop edition the automatic variable is absent and the platform is always Windows. PS7 behavior is unchanged. Add guardrails so this cannot silently regress: - Enable PSUseCompatibleSyntax/Commands/Types (targeting 5.1 and 7.0) in ScriptAnalyzerSettings.psd1, with justified suppressions for two documented false positives (Get-ChildItem -CodeSigningCert provider dynamic parameter; Invoke-Pester -Configuration from required Pester 5+). - Fail the Analyze build task on any compatibility violation. - Add an import-smoke CI job that parses and imports the module on the real Windows PowerShell 5.1 engine. The manifest support contract (PowerShellVersion/CompatiblePSEditions) is intentionally left unchanged here; formalizing the real floor is tracked for the v1.0.0 roadmap (#120). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 08b191a commit 1555ad1

6 files changed

Lines changed: 129 additions & 7 deletions

File tree

.github/workflows/test.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,46 @@ jobs:
2525
$DebugPreference = 'Continue'
2626
}
2727
./build.ps1 -Task Test -Bootstrap
28+
29+
# Smoke test on the lowest supported engine. The manifest declares support for Windows
30+
# PowerShell (Desktop), so the module must parse and import there. This job caught the 0.8.0
31+
# regression where a PowerShell 7+-only ternary operator broke module import on 5.1.
32+
# The root .psm1 is imported directly (rather than the .psd1) so the real dot-source loader runs
33+
# without the manifest's RequiredModules bootstrap, keeping the smoke test fast and dependency-free.
34+
import-smoke:
35+
name: Import smoke (Windows PowerShell 5.1)
36+
runs-on: windows-latest
37+
steps:
38+
- uses: actions/checkout@v4
39+
- name: Parse and import module under Windows PowerShell 5.1
40+
shell: powershell
41+
run: |
42+
$ErrorActionPreference = 'Stop'
43+
"Engine: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)"
44+
45+
$moduleRoot = Join-Path $PWD 'PowerShellBuild'
46+
47+
# 1. Parse every module file with the 5.1 language parser to catch syntax the lowest
48+
# supported engine cannot load (for example the PowerShell 7+ ternary operator).
49+
$files = Get-ChildItem -Path $moduleRoot -Recurse -Include *.ps1, *.psm1, *.psd1
50+
$parseFailed = $false
51+
foreach ($file in $files) {
52+
$tokens = $parseErrors = $null
53+
[System.Management.Automation.Language.Parser]::ParseFile($file.FullName, [ref]$tokens, [ref]$parseErrors) > $null
54+
if ($parseErrors) {
55+
$parseFailed = $true
56+
Write-Host "Parse errors in $($file.FullName):"
57+
$parseErrors | ForEach-Object { Write-Host " line $($_.Extent.StartLineNumber): $($_.Message)" }
58+
}
59+
}
60+
if ($parseFailed) {
61+
throw 'One or more module files failed to parse under Windows PowerShell 5.1.'
62+
}
63+
"Parsed $($files.Count) module files with no errors."
64+
65+
# 2. Import the root module to run the real dot-source loader and confirm it loads on 5.1.
66+
Import-Module (Join-Path $moduleRoot 'PowerShellBuild.psm1') -Force -ErrorAction Stop
67+
if (-not (Get-Command Get-PSBuildCertificate -ErrorAction SilentlyContinue)) {
68+
throw 'PowerShellBuild imported but expected functions are missing.'
69+
}
70+
'PowerShellBuild imported successfully under Windows PowerShell 5.1.'

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
77

88
## Unreleased
99

10+
### Fixed
11+
12+
- Restore Windows PowerShell 5.1 (Desktop edition) compatibility, which regressed
13+
in 0.8.0. `Get-PSBuildCertificate` used the PowerShell 7+-only ternary operator,
14+
causing the file to fail to parse and the whole module to fail to import under
15+
Windows PowerShell 5.1 — even though the manifest still declares support for it.
16+
The ternary is replaced with an `if`/`else` expression, and the `$IsWindows`
17+
platform guard now treats the absent automatic variable on Desktop edition as
18+
Windows (matching the existing pattern in `Build-PSBuildUpdatableHelp`). Behavior
19+
on PowerShell 7+ is unchanged.
20+
21+
### Added
22+
23+
- Cross-version compatibility guardrails so a PowerShell 7+-only construct cannot
24+
silently break the lowest supported engine again:
25+
- The `PSUseCompatibleSyntax`, `PSUseCompatibleCommands`, and
26+
`PSUseCompatibleTypes` rules are enabled in
27+
`PowerShellBuild/ScriptAnalyzerSettings.psd1` (targeting Windows PowerShell 5.1
28+
and PowerShell 7), and the `Analyze` build task now fails on any compatibility
29+
violation.
30+
- A new `Import smoke (Windows PowerShell 5.1)` CI job parses and imports the
31+
module on the real lowest-supported engine.
32+
1033
## [0.8.0] 2026-02-20
1134

1235
### Added

PowerShellBuild/Public/Get-PSBuildCertificate.ps1

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ function Get-PSBuildCertificate {
8888
'CertificatePasswordEnvVar',
8989
Justification = 'This is not a password in plain text. It is the name of an environment variable that contains the password, which is a common pattern for CI/CD pipelines and secrets management.'
9090
)]
91+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
92+
'PSUseCompatibleCommands',
93+
'',
94+
Justification = 'Get-ChildItem -CodeSigningCert is a dynamic parameter supplied by the Certificate (Cert:) provider, not a static parameter of Get-ChildItem, so the static command data used by PSUseCompatibleCommands cannot see it. It is available on both Windows PowerShell 5.1 and PowerShell 7+.'
95+
)]
9196
param(
9297
[ValidateSet('Auto', 'Store', 'Thumbprint', 'EnvVar', 'PfxFile')]
9398
[string]$CertificateSource = 'Auto',
@@ -122,8 +127,11 @@ function Get-PSBuildCertificate {
122127

123128
switch ($resolvedSource) {
124129
'Store' {
125-
# Throw if running on a non-Windows platform since the certificate store is not supported
126-
if (-not $IsWindows) {
130+
# Throw if running on a non-Windows platform since the certificate store is not supported.
131+
# $IsWindows does not exist on Windows PowerShell 5.1 (Desktop edition), where it is $null
132+
# and the platform is always Windows; only treat the platform as non-Windows when $IsWindows
133+
# is explicitly $false (PowerShell 7+ on Linux/macOS).
134+
if ($null -ne $IsWindows -and -not $IsWindows) {
127135
throw $LocalizedData.CertificateSourceStoreNotSupported
128136
}
129137
$cert = Get-ChildItem -Path $CertStoreLocation -CodeSigningCert |
@@ -195,6 +203,7 @@ function Get-PSBuildCertificate {
195203
Write-Verbose "Certificate validation passed: HasPrivateKey=$($cert.HasPrivateKey), NotAfter=$($cert.NotAfter), CodeSigningEKU=Present"
196204
}
197205

198-
Write-Verbose ('Certificate resolution complete: ' + ($cert ? $cert.Subject : 'No certificate found'))
206+
$certSubject = if ($cert) { $cert.Subject } else { 'No certificate found' }
207+
Write-Verbose ('Certificate resolution complete: ' + $certSubject)
199208
$cert
200209
}

PowerShellBuild/Public/Test-PSBuildPester.ps1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ function Test-PSBuildPester {
3636
Run Pester tests in ./tests and save results to ./out/testResults.xml
3737
#>
3838
[CmdletBinding()]
39+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
40+
'PSUseCompatibleCommands',
41+
'',
42+
Justification = 'Invoke-Pester -Configuration is a Pester 5+ parameter. This module requires Pester >= 5.6.1 (see RequiredModules in the manifest) and imports Pester with -MinimumVersion 5.0.0 before use, so the parameter is always available at runtime. The bundled PSUseCompatibleCommands profiles captured an older Pester, producing a false positive on both Windows PowerShell 5.1 and PowerShell 7.'
43+
)]
3944
param(
4045
[parameter(Mandatory)]
4146
[string]$Path,

PowerShellBuild/ScriptAnalyzerSettings.psd1

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,30 @@
99
PSAvoidUsingCmdletAliases = @{
1010
Whitelist = @('task')
1111
}
12+
13+
# Cross-version compatibility guardrails. PowerShellBuild still supports
14+
# Windows PowerShell 5.1 (Desktop) per the manifest, so these rules flag any
15+
# syntax, command, or type that would not work on the lowest supported engine.
16+
# PSUseCompatibleSyntax checks against the target language versions regardless
17+
# of the engine the analyzer runs under, so a PowerShell 7+-only construct (for
18+
# example the ternary operator) is caught even when the analyzer runs from pwsh.
19+
PSUseCompatibleSyntax = @{
20+
Enable = $true
21+
TargetVersions = @('5.1', '7.0')
22+
}
23+
PSUseCompatibleCommands = @{
24+
Enable = $true
25+
TargetProfiles = @(
26+
'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework'
27+
'win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core'
28+
)
29+
}
30+
PSUseCompatibleTypes = @{
31+
Enable = $true
32+
TargetProfiles = @(
33+
'win-8_x64_10.0.17763.0_5.1.17763.316_x64_4.0.30319.42000_framework'
34+
'win-8_x64_10.0.17763.0_7.0.0_x64_3.1.2_core'
35+
)
36+
}
1237
}
1338
}

psakeFile.ps1

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,29 @@ task Init {
1616
task Test -Depends Init, Analyze, Pester -description 'Run test suite'
1717

1818
task Analyze -depends Build {
19-
$analysis = Invoke-ScriptAnalyzer -Path $settings.ModuleOutDir -Recurse -Verbose:$false -Settings ([IO.Path]::Combine($env:BHModulePath, 'ScriptAnalyzerSettings.psd1'))
20-
$errors = $analysis | Where-Object {$_.Severity -eq 'Error'}
21-
$warnings = $analysis | Where-Object {$_.Severity -eq 'Warning'}
19+
$analyzerSettings = [IO.Path]::Combine($env:BHModulePath, 'ScriptAnalyzerSettings.psd1')
20+
$analysis = Invoke-ScriptAnalyzer -Path $settings.ModuleOutDir -Recurse -Verbose:$false -Settings $analyzerSettings
21+
$errors = $analysis | Where-Object { $_.Severity -eq 'Error' }
22+
23+
# Cross-version compatibility violations (PSUseCompatibleSyntax/Commands/Types) are reported at
24+
# Warning severity, but must always fail the build. A PowerShell 7+-only construct such as the
25+
# ternary operator parses fine under pwsh yet breaks module import on Windows PowerShell 5.1,
26+
# which the manifest still supports. PSUseCompatibleSyntax checks the target language versions
27+
# regardless of the engine the analyzer runs under, so this gate catches such a regression even
28+
# though CI runs the analysis from pwsh.
29+
$compatibilityIssues = $analysis | Where-Object { $_.RuleName -like 'PSUseCompatible*' }
30+
31+
# Remaining warnings are reported but, per project policy, do not fail the build.
32+
$warnings = $analysis | Where-Object { $_.Severity -eq 'Warning' -and $_.RuleName -notlike 'PSUseCompatible*' }
33+
34+
if (@($compatibilityIssues).Count -gt 0) {
35+
$compatibilityIssues | Format-Table RuleName, ScriptName, Line, Message -AutoSize -Wrap
36+
Write-Error -Message 'One or more cross-version compatibility issues were found. Build cannot continue!'
37+
}
38+
2239
if (@($errors).Count -gt 0) {
23-
Write-Error -Message 'One or more Script Analyzer errors were found. Build cannot continue!'
2440
$errors | Format-Table -AutoSize
41+
Write-Error -Message 'One or more Script Analyzer errors were found. Build cannot continue!'
2542
}
2643

2744
if (@($warnings).Count -gt 0) {

0 commit comments

Comments
 (0)