Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ reload.ps1
/*plan*.md
/summary.md
/coverage.xml
/pr-descriptions/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ Keep stable `Update-NovaModuleVersion` / `% nova bump` releases on the SemVer ma
- Regular markdown elsewhere under `docs/` no longer breaks the help-generation step.
- When PlatyPS export does not create the expected help folder, Nova now raises a stable documentation error instead
of a raw rename-path failure.
- Fix CI test dependency installation so `Pester 5.7.1` is installed explicitly instead of being pulled in implicitly
through the published `NovaModuleTools` manifest.
- Published prerelease manifests no longer force `Pester` as an install-time required module.
- `Test-NovaBuild` now fails with a clear dependency error when `Pester` is not installed.
- Fix the repository `run.ps1` quality loop after the CI installer refactor introduced new ScriptAnalyzer warnings.
- The internal CI installer helper now follows ScriptAnalyzer naming and `ShouldProcess` expectations.
- `run.ps1` no longer stops in the analyzer step because of those helper warnings.

### Security

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ Notes:
- `Test-NovaBuild` validates the built module output, not just loose source files
- it writes NUnit XML to `artifacts/TestResults.xml`
- it respects `BuildRecursiveFolders` when discovering tests
- `Pester` is a test-time dependency, not a transitive install dependency of the published `NovaModuleTools` module
- install `Pester 5.7.1` explicitly in contributor or CI environments before running `Test-NovaBuild`

### Create a package artifact

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

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

### Where NovaModuleTools cmdlets fit

Expand Down
4 changes: 0 additions & 4 deletions project.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@
"PowerShellHostVersion": "7.4",
"GUID": "6b9202c8-0353-473b-b73c-afab632125a6",
"RequiredModules": [
{
"ModuleName": "Pester",
"ModuleVersion": "5.7.1"
},
{
"ModuleName": "Microsoft.PowerShell.PlatyPS",
"ModuleVersion": "1.0.1"
Expand Down
112 changes: 104 additions & 8 deletions scripts/build/ci/Install-CiPowerShellModules.ps1
Original file line number Diff line number Diff line change
@@ -1,30 +1,126 @@
param(
[string[]]$ModuleName = @(
'Pester',
'NovaModuleTools',
'KeepAChangelog'
)
)

Set-StrictMode -Version Latest

function Install-CiModule {
function Get-CiModuleInstallOption {
param(
[Parameter(Mandatory)][string]$Name
)

$repositoryName = 'PSGallery'
Write-Host "Installing PowerShell module '$Name'..."
Install-Module -Name $Name -Repository $repositoryName -AllowPrerelease -Scope CurrentUser -Force -ErrorAction Stop | Out-Null
if ($Name -eq 'Pester') {
return [pscustomobject]@{
RequiredVersion = '5.7.1'
AllowPrerelease = $false
}
}

return [pscustomobject]@{
RequiredVersion = $null
AllowPrerelease = $true
}
}

$installedModule = Get-InstalledModule -Name $Name -ErrorAction Stop |
function Get-InstalledCiModule {
param(
[Parameter(Mandatory)][string]$Name
)

return Get-InstalledModule -Name $Name -ErrorAction SilentlyContinue |
Sort-Object Version -Descending |
Select-Object -First 1
}

function Write-CiInstalledModule {
param(
[Parameter(Mandatory)][string]$Name
)

$installedModule = Get-InstalledCiModule -Name $Name
if (-not $installedModule) {
throw "Expected PowerShell module '$Name' to be installed, but no installed module record was found."
}

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

Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
function Test-CiModuleInstallSkip {
param(
[Parameter(Mandatory)][pscustomobject]$InstallOptions,
$InstalledModule
)

if (-not $InstallOptions.RequiredVersion) {
return $false
}

if (-not $InstalledModule) {
return $false
}

return $InstalledModule.Version -eq [version]$InstallOptions.RequiredVersion
}

function Invoke-CiInstallModuleCommand {
param(
[Parameter(Mandatory)][hashtable]$InstallParams
)

Install-Module @InstallParams | Out-Null
}

function Install-CiModule {
param(
[Parameter(Mandatory)][string]$Name
)

$installOptions = Get-CiModuleInstallOption -Name $Name
$installedModule = Get-InstalledCiModule -Name $Name
if (Test-CiModuleInstallSkip -InstallOptions $installOptions -InstalledModule $installedModule) {
Write-Host "Skipping PowerShell module '$Name' because required version '$( $installOptions.RequiredVersion )' is already installed."
Write-CiInstalledModule -Name $Name
return
}

$installParams = @{Name = $Name; Repository = 'PSGallery'; Scope = 'CurrentUser'; Force = $true; ErrorAction = 'Stop'}
if ($installOptions.RequiredVersion) {
$installParams.RequiredVersion = $installOptions.RequiredVersion
Write-Host "Installing PowerShell module '$Name' version '$( $installOptions.RequiredVersion )'..."
} else {
$installParams.AllowPrerelease = $installOptions.AllowPrerelease
Write-Host "Installing PowerShell module '$Name' with AllowPrerelease=$( $installOptions.AllowPrerelease )..."
}

Invoke-CiInstallModuleCommand -InstallParams $installParams
Write-CiInstalledModule -Name $Name
}

function Set-CiRepositoryTrust {
[CmdletBinding(SupportsShouldProcess)]
param()

if ( $PSCmdlet.ShouldProcess('PSGallery', 'Set repository installation policy to Trusted')) {
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
}
}

function Invoke-CiPowerShellModuleInstall {
param(
[string[]]$ModuleName = @()
)

Set-CiRepositoryTrust

foreach ($name in $ModuleName) {
Install-CiModule -Name $name
}
}

foreach ($name in $ModuleName) {
Install-CiModule -Name $name
if ($MyInvocation.InvocationName -ne '.') {
Invoke-CiPowerShellModuleInstall -ModuleName $ModuleName
}
10 changes: 10 additions & 0 deletions src/private/quality/GetNovaTestWorkflowContext.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ function Get-NovaTestWorkflowOperation {
return 'Run Pester tests and write test results'
}

function Assert-NovaPesterAvailable {
[CmdletBinding()]
param()

if (-not (Get-Module -Name Pester -ListAvailable)) {
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'
}
}

function Get-NovaTestWorkflowContext {
[CmdletBinding()]
param(
Expand All @@ -19,6 +28,7 @@ function Get-NovaTestWorkflowContext {
)

Test-ProjectSchema Pester | Out-Null
Assert-NovaPesterAvailable
$projectInfo = Get-NovaProjectInfo
$pesterConfig = New-PesterConfiguration -Hashtable $projectInfo.Pester

Expand Down
91 changes: 91 additions & 0 deletions tests/CiCoverage.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -425,3 +425,94 @@ Describe 'Coverage gaps for quality helpers' {
}
}
}

Describe 'CI PowerShell module installer' {
BeforeAll {
$script:getTestCiInstalledModuleRecord = {
param(
[Parameter(Mandatory)]$Name,
[Parameter(Mandatory)][hashtable]$InstalledVersionMap
)

$moduleName = [string]$Name

if (-not $InstalledVersionMap.ContainsKey($moduleName)) {
return $null
}

return [pscustomobject]@{
Name = $moduleName
Version = $InstalledVersionMap[$moduleName]
Repository = 'PSGallery'
}
}
$script:setTestCiInstallerMocks = {
param(
[Parameter(Mandatory)][hashtable]$InstalledVersionMap,
[System.Collections.Generic.List[string]]$InstallOrder,
[switch]$FailOnInstall
)

$script:ciInstalledVersionMap = $InstalledVersionMap
$script:ciInstallOrder = $InstallOrder
Mock Set-CiRepositoryTrust {}
Mock Get-InstalledCiModule {
& $script:getTestCiInstalledModuleRecord -Name $Name -InstalledVersionMap $script:ciInstalledVersionMap
}

if ($FailOnInstall) {
Mock Invoke-CiInstallModuleCommand {throw 'Pester should not be reinstalled when the required version is already installed.'}
return
}

Mock Invoke-CiInstallModuleCommand {
$script:ciInstallOrder.Add($InstallParams.Name) | Out-Null
$script:ciInstalledVersionMap[$InstallParams.Name] = if ( $InstallParams.ContainsKey('RequiredVersion')) {
[version]$InstallParams.RequiredVersion
} else {
[version]'9.9.9'
}
}
}
}

It 'installs Pester explicitly before prerelease gallery modules' {
$scriptPath = Join-Path $script:repoRoot 'scripts/build/ci/Install-CiPowerShellModules.ps1'
$installOrder = [System.Collections.Generic.List[string]]::new()

. $scriptPath -ModuleName @('Pester', 'NovaModuleTools', 'KeepAChangelog')
& $script:setTestCiInstallerMocks -InstalledVersionMap @{} -InstallOrder $installOrder

Invoke-CiPowerShellModuleInstall -ModuleName @('Pester', 'NovaModuleTools', 'KeepAChangelog')

$installOrder | Should -Be @('Pester', 'NovaModuleTools', 'KeepAChangelog')
Assert-MockCalled Invoke-CiInstallModuleCommand -Times 1 -ParameterFilter {
$InstallParams.Name -eq 'Pester' -and
$InstallParams.Repository -eq 'PSGallery' -and
$InstallParams.RequiredVersion -eq '5.7.1' -and
-not $InstallParams.ContainsKey('AllowPrerelease')
}
Assert-MockCalled Invoke-CiInstallModuleCommand -Times 1 -ParameterFilter {
$InstallParams.Name -eq 'NovaModuleTools' -and
$InstallParams.Repository -eq 'PSGallery' -and
$InstallParams.AllowPrerelease
}
Assert-MockCalled Invoke-CiInstallModuleCommand -Times 1 -ParameterFilter {
$InstallParams.Name -eq 'KeepAChangelog' -and
$InstallParams.Repository -eq 'PSGallery' -and
$InstallParams.AllowPrerelease
}
}

It 'skips reinstalling Pester when the required version is already installed' {
$scriptPath = Join-Path $script:repoRoot 'scripts/build/ci/Install-CiPowerShellModules.ps1'

. $scriptPath -ModuleName @('Pester')
& $script:setTestCiInstallerMocks -InstalledVersionMap @{Pester = [version]'5.7.1'} -FailOnInstall

Invoke-CiPowerShellModuleInstall -ModuleName @('Pester')

Assert-MockCalled Invoke-CiInstallModuleCommand -Times 0
}
}

16 changes: 14 additions & 2 deletions tests/CoverageGaps.BuildInternals.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,16 @@ Describe 'Coverage gaps for build and duplicate-analysis internals' {
PublicDir = '/tmp/public'
ResourcesDir = '/tmp/resources'
CopyResourcesToModuleRoot = $ManifestCase.CopyResourcesToModuleRoot
Manifest = [ordered]@{Author = 'Tester'; CompanyName = 'Nova'}
Manifest = [ordered]@{
Author = 'Tester'
CompanyName = 'Nova'
RequiredModules = @(
@{
ModuleName = 'Microsoft.PowerShell.PlatyPS'
ModuleVersion = '1.0.1'
}
)
}
Version = '1.2.3-preview'
Description = 'Example'
ProjectName = 'NovaModuleTools'
Expand All @@ -468,7 +477,10 @@ Describe 'Coverage gaps for build and duplicate-analysis internals' {
$TypesToProcess -eq @($ManifestCase.ExpectedTypePath) -and
$Prerelease -eq 'preview' -and
$Author -eq 'Tester' -and
$CompanyName -eq 'Nova'
$CompanyName -eq 'Nova' -and
@($RequiredModules).Count -eq 1 -and
$RequiredModules[0].ModuleName -eq 'Microsoft.PowerShell.PlatyPS' -and
$RequiredModules[0].ModuleVersion -eq '1.0.1'
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions tests/Module.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,19 @@ Describe 'General Module Control' {
$result | Should -Not -BeNullOrEmpty
$result.PSObject.Properties.Name | Should -Contain 'NewVersion'
}

It 'Publishes only the intended install-time required modules in the built manifest' {
$manifestPath = Join-Path $data.OutputModuleDir "$( $data.ProjectName ).psd1"
$manifest = Import-PowerShellDataFile -Path $manifestPath
$requiredModuleNames = @($manifest.RequiredModules | ForEach-Object {
if ($_ -is [string]) {
return $_
}

return $_.ModuleName
})

$requiredModuleNames | Should -Contain 'Microsoft.PowerShell.PlatyPS'
$requiredModuleNames | Should -Not -Contain 'Pester'
}
}
25 changes: 25 additions & 0 deletions tests/RemainingCommandCoverage.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ Describe 'Coverage for remaining command and filesystem branches' {
$reportWriter = [pscustomobject]@{ScriptBlock = {}}

Mock Test-ProjectSchema {}
Mock Get-Module {[pscustomobject]@{Name = 'Pester'}} -ParameterFilter {$Name -eq 'Pester' -and $ListAvailable}
Mock Get-NovaProjectInfo {
[pscustomobject]@{
Pester = @{}
Expand Down Expand Up @@ -216,6 +217,30 @@ Describe 'Coverage for remaining command and filesystem branches' {
}
}

It 'Get-NovaTestWorkflowContext fails clearly when Pester is unavailable' {
InModuleScope $script:moduleName {
Mock Test-ProjectSchema {}
Mock Get-Module {$null} -ParameterFilter {$Name -eq 'Pester' -and $ListAvailable}
Mock Get-NovaProjectInfo {throw 'should not resolve project info without Pester'}
Mock New-PesterConfiguration {throw 'should not create config without Pester'}

$thrown = $null
try {
Get-NovaTestWorkflowContext -TestOption @{} -BoundParameters @{}
}
catch {
$thrown = $_
}

$thrown.Exception.Message | Should -Be 'The module Pester must be installed for Test-NovaBuild to run. Install Pester 5.7.1 and try again.'
$thrown.FullyQualifiedErrorId | Should -Be 'Nova.Dependency.PesterDependencyMissing'
$thrown.CategoryInfo.Category | Should -Be ([System.Management.Automation.ErrorCategory]::ResourceUnavailable)
$thrown.TargetObject | Should -Be 'Pester'
Assert-MockCalled Get-NovaProjectInfo -Times 0
Assert-MockCalled New-PesterConfiguration -Times 0
}
}

It 'Invoke-NovaTestWorkflow creates the artifacts directory when it is missing' {
$cfg = Get-TestNovaPesterConfig

Expand Down
Loading