Skip to content

Commit 147fa5a

Browse files
committed
fix(#139): resolve CI dependency installation issues
- Explicitly install `Pester 5.7.1` instead of relying on transitive dependencies - Update documentation to clarify `Pester` as a test-time dependency - Ensure `Test-NovaBuild` fails with a clear error if `Pester` is not installed
1 parent 8949380 commit 147fa5a

9 files changed

Lines changed: 270 additions & 14 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ Keep stable `Update-NovaModuleVersion` / `% nova bump` releases on the SemVer ma
3939
- Regular markdown elsewhere under `docs/` no longer breaks the help-generation step.
4040
- When PlatyPS export does not create the expected help folder, Nova now raises a stable documentation error instead
4141
of a raw rename-path failure.
42+
- Fix CI test dependency installation so `Pester 5.7.1` is installed explicitly instead of being pulled in implicitly
43+
through the published `NovaModuleTools` manifest.
44+
- Published prerelease manifests no longer force `Pester` as an install-time required module.
45+
- `Test-NovaBuild` now fails with a clear dependency error when `Pester` is not installed.
46+
- Fix the repository `run.ps1` quality loop after the CI installer refactor introduced new ScriptAnalyzer warnings.
47+
- The internal CI installer helper now follows ScriptAnalyzer naming and `ShouldProcess` expectations.
48+
- `run.ps1` no longer stops in the analyzer step because of those helper warnings.
4249

4350
### Security
4451

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,8 @@ Notes:
262262
- `Test-NovaBuild` validates the built module output, not just loose source files
263263
- it writes NUnit XML to `artifacts/TestResults.xml`
264264
- it respects `BuildRecursiveFolders` when discovering tests
265+
- `Pester` is a test-time dependency, not a transitive install dependency of the published `NovaModuleTools` module
266+
- install `Pester 5.7.1` explicitly in contributor or CI environments before running `Test-NovaBuild`
265267

266268
### Create a package artifact
267269

@@ -619,6 +621,8 @@ Responsibilities currently covered by the release pipeline include:
619621

620622
The workflow now uses `KeepAChangelog` for changelog release moves, creates annotated git tags named directly from the
621623
release version, and bootstraps the local PSResourceGet repository store before calling `Publish-NovaModule`.
624+
The shared CI installer also installs `Pester 5.7.1` explicitly before it installs prerelease gallery modules so test
625+
workflows do not rely on transitive manifest dependency resolution.
622626

623627
### Where NovaModuleTools cmdlets fit
624628

project.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@
1212
"PowerShellHostVersion": "7.4",
1313
"GUID": "6b9202c8-0353-473b-b73c-afab632125a6",
1414
"RequiredModules": [
15-
{
16-
"ModuleName": "Pester",
17-
"ModuleVersion": "5.7.1"
18-
},
1915
{
2016
"ModuleName": "Microsoft.PowerShell.PlatyPS",
2117
"ModuleVersion": "1.0.1"
Lines changed: 104 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,126 @@
11
param(
22
[string[]]$ModuleName = @(
3+
'Pester',
34
'NovaModuleTools',
45
'KeepAChangelog'
56
)
67
)
78

89
Set-StrictMode -Version Latest
910

10-
function Install-CiModule {
11+
function Get-CiModuleInstallOption {
1112
param(
1213
[Parameter(Mandatory)][string]$Name
1314
)
1415

15-
$repositoryName = 'PSGallery'
16-
Write-Host "Installing PowerShell module '$Name'..."
17-
Install-Module -Name $Name -Repository $repositoryName -AllowPrerelease -Scope CurrentUser -Force -ErrorAction Stop | Out-Null
16+
if ($Name -eq 'Pester') {
17+
return [pscustomobject]@{
18+
RequiredVersion = '5.7.1'
19+
AllowPrerelease = $false
20+
}
21+
}
22+
23+
return [pscustomobject]@{
24+
RequiredVersion = $null
25+
AllowPrerelease = $true
26+
}
27+
}
1828

19-
$installedModule = Get-InstalledModule -Name $Name -ErrorAction Stop |
29+
function Get-InstalledCiModule {
30+
param(
31+
[Parameter(Mandatory)][string]$Name
32+
)
33+
34+
return Get-InstalledModule -Name $Name -ErrorAction SilentlyContinue |
2035
Sort-Object Version -Descending |
2136
Select-Object -First 1
37+
}
38+
39+
function Write-CiInstalledModule {
40+
param(
41+
[Parameter(Mandatory)][string]$Name
42+
)
43+
44+
$installedModule = Get-InstalledCiModule -Name $Name
45+
if (-not $installedModule) {
46+
throw "Expected PowerShell module '$Name' to be installed, but no installed module record was found."
47+
}
2248

2349
Write-Host "Installed PowerShell module '$( $installedModule.Name )' version '$( $installedModule.Version )' from '$( $installedModule.Repository )'."
2450
}
2551

26-
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
52+
function Test-CiModuleInstallSkip {
53+
param(
54+
[Parameter(Mandatory)][pscustomobject]$InstallOptions,
55+
$InstalledModule
56+
)
57+
58+
if (-not $InstallOptions.RequiredVersion) {
59+
return $false
60+
}
61+
62+
if (-not $InstalledModule) {
63+
return $false
64+
}
65+
66+
return $InstalledModule.Version -eq [version]$InstallOptions.RequiredVersion
67+
}
68+
69+
function Invoke-CiInstallModuleCommand {
70+
param(
71+
[Parameter(Mandatory)][hashtable]$InstallParams
72+
)
73+
74+
Install-Module @InstallParams | Out-Null
75+
}
76+
77+
function Install-CiModule {
78+
param(
79+
[Parameter(Mandatory)][string]$Name
80+
)
81+
82+
$installOptions = Get-CiModuleInstallOption -Name $Name
83+
$installedModule = Get-InstalledCiModule -Name $Name
84+
if (Test-CiModuleInstallSkip -InstallOptions $installOptions -InstalledModule $installedModule) {
85+
Write-Host "Skipping PowerShell module '$Name' because required version '$( $installOptions.RequiredVersion )' is already installed."
86+
Write-CiInstalledModule -Name $Name
87+
return
88+
}
89+
90+
$installParams = @{Name = $Name; Repository = 'PSGallery'; Scope = 'CurrentUser'; Force = $true; ErrorAction = 'Stop'}
91+
if ($installOptions.RequiredVersion) {
92+
$installParams.RequiredVersion = $installOptions.RequiredVersion
93+
Write-Host "Installing PowerShell module '$Name' version '$( $installOptions.RequiredVersion )'..."
94+
} else {
95+
$installParams.AllowPrerelease = $installOptions.AllowPrerelease
96+
Write-Host "Installing PowerShell module '$Name' with AllowPrerelease=$( $installOptions.AllowPrerelease )..."
97+
}
98+
99+
Invoke-CiInstallModuleCommand -InstallParams $installParams
100+
Write-CiInstalledModule -Name $Name
101+
}
102+
103+
function Set-CiRepositoryTrust {
104+
[CmdletBinding(SupportsShouldProcess)]
105+
param()
106+
107+
if ( $PSCmdlet.ShouldProcess('PSGallery', 'Set repository installation policy to Trusted')) {
108+
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
109+
}
110+
}
111+
112+
function Invoke-CiPowerShellModuleInstall {
113+
param(
114+
[string[]]$ModuleName = @()
115+
)
116+
117+
Set-CiRepositoryTrust
118+
119+
foreach ($name in $ModuleName) {
120+
Install-CiModule -Name $name
121+
}
122+
}
27123

28-
foreach ($name in $ModuleName) {
29-
Install-CiModule -Name $name
124+
if ($MyInvocation.InvocationName -ne '.') {
125+
Invoke-CiPowerShellModuleInstall -ModuleName $ModuleName
30126
}

src/private/quality/GetNovaTestWorkflowContext.ps1

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ function Get-NovaTestWorkflowOperation {
1111
return 'Run Pester tests and write test results'
1212
}
1313

14+
function Assert-NovaPesterAvailable {
15+
[CmdletBinding()]
16+
param()
17+
18+
if (-not (Get-Module -Name Pester -ListAvailable)) {
19+
Stop-NovaOperation -Message 'The module Pester must be installed for Test-NovaBuild to run. Install Pester 5.7.1 and try again.' -ErrorId 'Nova.Dependency.PesterDependencyMissing' -Category ResourceUnavailable -TargetObject 'Pester'
20+
}
21+
}
22+
1423
function Get-NovaTestWorkflowContext {
1524
[CmdletBinding()]
1625
param(
@@ -19,6 +28,7 @@ function Get-NovaTestWorkflowContext {
1928
)
2029

2130
Test-ProjectSchema Pester | Out-Null
31+
Assert-NovaPesterAvailable
2232
$projectInfo = Get-NovaProjectInfo
2333
$pesterConfig = New-PesterConfiguration -Hashtable $projectInfo.Pester
2434

tests/CiCoverage.Tests.ps1

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,3 +425,94 @@ Describe 'Coverage gaps for quality helpers' {
425425
}
426426
}
427427
}
428+
429+
Describe 'CI PowerShell module installer' {
430+
BeforeAll {
431+
$script:getTestCiInstalledModuleRecord = {
432+
param(
433+
[Parameter(Mandatory)]$Name,
434+
[Parameter(Mandatory)][hashtable]$InstalledVersionMap
435+
)
436+
437+
$moduleName = [string]$Name
438+
439+
if (-not $InstalledVersionMap.ContainsKey($moduleName)) {
440+
return $null
441+
}
442+
443+
return [pscustomobject]@{
444+
Name = $moduleName
445+
Version = $InstalledVersionMap[$moduleName]
446+
Repository = 'PSGallery'
447+
}
448+
}
449+
$script:setTestCiInstallerMocks = {
450+
param(
451+
[Parameter(Mandatory)][hashtable]$InstalledVersionMap,
452+
[System.Collections.Generic.List[string]]$InstallOrder,
453+
[switch]$FailOnInstall
454+
)
455+
456+
$script:ciInstalledVersionMap = $InstalledVersionMap
457+
$script:ciInstallOrder = $InstallOrder
458+
Mock Set-CiRepositoryTrust {}
459+
Mock Get-InstalledCiModule {
460+
& $script:getTestCiInstalledModuleRecord -Name $Name -InstalledVersionMap $script:ciInstalledVersionMap
461+
}
462+
463+
if ($FailOnInstall) {
464+
Mock Invoke-CiInstallModuleCommand {throw 'Pester should not be reinstalled when the required version is already installed.'}
465+
return
466+
}
467+
468+
Mock Invoke-CiInstallModuleCommand {
469+
$script:ciInstallOrder.Add($InstallParams.Name) | Out-Null
470+
$script:ciInstalledVersionMap[$InstallParams.Name] = if ( $InstallParams.ContainsKey('RequiredVersion')) {
471+
[version]$InstallParams.RequiredVersion
472+
} else {
473+
[version]'9.9.9'
474+
}
475+
}
476+
}
477+
}
478+
479+
It 'installs Pester explicitly before prerelease gallery modules' {
480+
$scriptPath = Join-Path $script:repoRoot 'scripts/build/ci/Install-CiPowerShellModules.ps1'
481+
$installOrder = [System.Collections.Generic.List[string]]::new()
482+
483+
. $scriptPath -ModuleName @('Pester', 'NovaModuleTools', 'KeepAChangelog')
484+
& $script:setTestCiInstallerMocks -InstalledVersionMap @{} -InstallOrder $installOrder
485+
486+
Invoke-CiPowerShellModuleInstall -ModuleName @('Pester', 'NovaModuleTools', 'KeepAChangelog')
487+
488+
$installOrder | Should -Be @('Pester', 'NovaModuleTools', 'KeepAChangelog')
489+
Assert-MockCalled Invoke-CiInstallModuleCommand -Times 1 -ParameterFilter {
490+
$InstallParams.Name -eq 'Pester' -and
491+
$InstallParams.Repository -eq 'PSGallery' -and
492+
$InstallParams.RequiredVersion -eq '5.7.1' -and
493+
-not $InstallParams.ContainsKey('AllowPrerelease')
494+
}
495+
Assert-MockCalled Invoke-CiInstallModuleCommand -Times 1 -ParameterFilter {
496+
$InstallParams.Name -eq 'NovaModuleTools' -and
497+
$InstallParams.Repository -eq 'PSGallery' -and
498+
$InstallParams.AllowPrerelease
499+
}
500+
Assert-MockCalled Invoke-CiInstallModuleCommand -Times 1 -ParameterFilter {
501+
$InstallParams.Name -eq 'KeepAChangelog' -and
502+
$InstallParams.Repository -eq 'PSGallery' -and
503+
$InstallParams.AllowPrerelease
504+
}
505+
}
506+
507+
It 'skips reinstalling Pester when the required version is already installed' {
508+
$scriptPath = Join-Path $script:repoRoot 'scripts/build/ci/Install-CiPowerShellModules.ps1'
509+
510+
. $scriptPath -ModuleName @('Pester')
511+
& $script:setTestCiInstallerMocks -InstalledVersionMap @{Pester = [version]'5.7.1'} -FailOnInstall
512+
513+
Invoke-CiPowerShellModuleInstall -ModuleName @('Pester')
514+
515+
Assert-MockCalled Invoke-CiInstallModuleCommand -Times 0
516+
}
517+
}
518+

tests/CoverageGaps.BuildInternals.Tests.ps1

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,16 @@ Describe 'Coverage gaps for build and duplicate-analysis internals' {
443443
PublicDir = '/tmp/public'
444444
ResourcesDir = '/tmp/resources'
445445
CopyResourcesToModuleRoot = $ManifestCase.CopyResourcesToModuleRoot
446-
Manifest = [ordered]@{Author = 'Tester'; CompanyName = 'Nova'}
446+
Manifest = [ordered]@{
447+
Author = 'Tester'
448+
CompanyName = 'Nova'
449+
RequiredModules = @(
450+
@{
451+
ModuleName = 'Microsoft.PowerShell.PlatyPS'
452+
ModuleVersion = '1.0.1'
453+
}
454+
)
455+
}
447456
Version = '1.2.3-preview'
448457
Description = 'Example'
449458
ProjectName = 'NovaModuleTools'
@@ -468,7 +477,10 @@ Describe 'Coverage gaps for build and duplicate-analysis internals' {
468477
$TypesToProcess -eq @($ManifestCase.ExpectedTypePath) -and
469478
$Prerelease -eq 'preview' -and
470479
$Author -eq 'Tester' -and
471-
$CompanyName -eq 'Nova'
480+
$CompanyName -eq 'Nova' -and
481+
@($RequiredModules).Count -eq 1 -and
482+
$RequiredModules[0].ModuleName -eq 'Microsoft.PowerShell.PlatyPS' -and
483+
$RequiredModules[0].ModuleVersion -eq '1.0.1'
472484
}
473485
}
474486
}

tests/Module.Tests.ps1

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,4 +54,19 @@ Describe 'General Module Control' {
5454
$result | Should -Not -BeNullOrEmpty
5555
$result.PSObject.Properties.Name | Should -Contain 'NewVersion'
5656
}
57+
58+
It 'Publishes only the intended install-time required modules in the built manifest' {
59+
$manifestPath = Join-Path $data.OutputModuleDir "$( $data.ProjectName ).psd1"
60+
$manifest = Import-PowerShellDataFile -Path $manifestPath
61+
$requiredModuleNames = @($manifest.RequiredModules | ForEach-Object {
62+
if ($_ -is [string]) {
63+
return $_
64+
}
65+
66+
return $_.ModuleName
67+
})
68+
69+
$requiredModuleNames | Should -Contain 'Microsoft.PowerShell.PlatyPS'
70+
$requiredModuleNames | Should -Not -Contain 'Pester'
71+
}
5772
}

tests/RemainingCommandCoverage.Tests.ps1

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ Describe 'Coverage for remaining command and filesystem branches' {
187187
$reportWriter = [pscustomobject]@{ScriptBlock = {}}
188188

189189
Mock Test-ProjectSchema {}
190+
Mock Get-Module {[pscustomobject]@{Name = 'Pester'}} -ParameterFilter {$Name -eq 'Pester' -and $ListAvailable}
190191
Mock Get-NovaProjectInfo {
191192
[pscustomobject]@{
192193
Pester = @{}
@@ -216,6 +217,30 @@ Describe 'Coverage for remaining command and filesystem branches' {
216217
}
217218
}
218219

220+
It 'Get-NovaTestWorkflowContext fails clearly when Pester is unavailable' {
221+
InModuleScope $script:moduleName {
222+
Mock Test-ProjectSchema {}
223+
Mock Get-Module {$null} -ParameterFilter {$Name -eq 'Pester' -and $ListAvailable}
224+
Mock Get-NovaProjectInfo {throw 'should not resolve project info without Pester'}
225+
Mock New-PesterConfiguration {throw 'should not create config without Pester'}
226+
227+
$thrown = $null
228+
try {
229+
Get-NovaTestWorkflowContext -TestOption @{} -BoundParameters @{}
230+
}
231+
catch {
232+
$thrown = $_
233+
}
234+
235+
$thrown.Exception.Message | Should -Be 'The module Pester must be installed for Test-NovaBuild to run. Install Pester 5.7.1 and try again.'
236+
$thrown.FullyQualifiedErrorId | Should -Be 'Nova.Dependency.PesterDependencyMissing'
237+
$thrown.CategoryInfo.Category | Should -Be ([System.Management.Automation.ErrorCategory]::ResourceUnavailable)
238+
$thrown.TargetObject | Should -Be 'Pester'
239+
Assert-MockCalled Get-NovaProjectInfo -Times 0
240+
Assert-MockCalled New-PesterConfiguration -Times 0
241+
}
242+
}
243+
219244
It 'Invoke-NovaTestWorkflow creates the artifacts directory when it is missing' {
220245
$cfg = Get-TestNovaPesterConfig
221246

0 commit comments

Comments
 (0)