Skip to content
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Fixed

- `Invoke-NovaTest`, `Test-NovaBuild`, and `% nova test` now resolve and import a supported installed `Pester` version from `5.7.1` through `5.10.0` instead of using an unsupported `Pester 6.x` installation automatically.
- Nova test workflows now fail early with a clear dependency error when only unsupported `Pester 6.x` versions are available.
- Nova test workflows now reuse an already loaded supported `Pester 5.x` version in the current PowerShell session before selecting a different installed version.
- Repository CI no longer trips the `Pester` assembly loader by importing a gallery-installed `NovaModuleTools` module, building the local module, and then switching to another supported `Pester` version in the same session.

### Security

## [3.3.0] - 2026-06-16
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,8 +298,9 @@ Notes:
- if `project.json` sets `Pester.CodeCoverage.CoveragePercentTarget`, `Invoke-NovaTest` fails when the measured coverage percentage is lower than that configured target
- this repository currently enables coverage with a `99` percent target; the template and packaged example
`project.json` files ship the same JaCoCo configuration shape with `Enabled=false` and a `90` percent opt-in target
- make sure `Pester 5.7.1` is available before running `Invoke-NovaTest` or `Test-NovaBuild`
- the published `NovaModuleTools` manifest also declares `Pester 5.7.1`, so installed end-user workflows can still resolve that dependency automatically
- make sure a supported `Pester` version from `5.7.1` through `5.10.0` is available before running `Invoke-NovaTest` or `Test-NovaBuild`
- Nova resolves and imports a compatible installed `Pester 5.x` version for the managed test workflow and does not run tests with `Pester 6.x`
- the published `NovaModuleTools` manifest declares `Pester` with `ModuleVersion = 5.7.1` and `MaximumVersion = 5.10.0`, so installed end-user workflows can still resolve that supported dependency range automatically

### Create a package artifact

Expand Down Expand Up @@ -640,7 +641,7 @@ Responsibilities currently covered by the release pipeline include:
- publishing to PowerShell Gallery
- preparing the next prerelease version on `develop`

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.
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 stay on Nova's supported `Pester 5.x` range and do not rely on transitive manifest dependency resolution.

### Where NovaModuleTools cmdlets fit

Expand Down
2 changes: 2 additions & 0 deletions RELEASE_NOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang

### Fixed

- `Invoke-NovaTest`, `Test-NovaBuild`, and `% nova test` now stay on Nova's supported `Pester 5.x` range and stop with a clear error instead of trying to run with an installed `Pester 6.x` version.

### Security

## [3.3.0] - 2026-06-16
Expand Down
2 changes: 2 additions & 0 deletions docs/NovaModuleTools/en-US/Invoke-NovaTest.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ PS> Invoke-NovaTest [[-TagFilter] <string[]>] [[-ExcludeTagFilter] <string[]>]

`Invoke-NovaTest` reads the Pester configuration from `project.json`, discovers the repository unit-test files, and runs the managed Nova test workflow without rebuilding the project first.

Nova resolves a supported installed `Pester` version from `5.7.1` through `5.10.0` for the managed test workflow and stops with a clear dependency error when only unsupported `Pester 6.x` versions are available.

The unit-test workflow writes NUnit XML to `artifacts/UnitTestResults.xml`.

When `Pester.CodeCoverage.Enabled` is `true`, Nova also writes JaCoCo coverage to `artifacts/coverage.xml` and fails the run when the measured percentage is lower than `Pester.CodeCoverage.CoveragePercentTarget`.
Expand Down
8 changes: 5 additions & 3 deletions docs/NovaModuleTools/en-US/Test-NovaBuild.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ PS> Test-NovaBuild [[-TagFilter] <string[]>] [[-ExcludeTagFilter] <string[]>]

`Test-NovaBuild` reads the Pester configuration from `project.json`, discovers the build-validation integration tests for the current project, and runs the managed Nova test workflow against the built-module validation surface.

Nova resolves a supported installed `Pester` version from `5.7.1` through `5.10.0` for the managed test workflow and stops with a clear dependency error when only unsupported `Pester 6.x` versions are available.

This build-validation flow writes NUnit XML to `artifacts/TestResults.xml`.

Unlike `Invoke-NovaTest`, this command does not enforce source-coverage targets. Use `Invoke-NovaTest` for the unit-test and code-coverage workflow, and use `Test-NovaBuild` when you need build-validation integration coverage that reflects the built module path.

If the current project does not contain any `*.Integration.Tests.ps1` files, `Test-NovaBuild` stops with a Nova-native error that explains the expected naming and reminds you to use `Invoke-NovaTest` for unit tests.
If the current project does not contain any `*.Integration.Tests.ps1` files, `Test-NovaBuild` writes an actionable warning that explains the expected naming and reminds you to use `Invoke-NovaTest` for unit tests.

`-OverrideWarning` lets the nested build-validation flow continue even if the `src/public` layout guard reports zero or multiple top-level functions in a public file.

Expand Down Expand Up @@ -271,5 +273,5 @@ Returns the Pester result object from the managed build-validation run.

## RELATED LINKS

[Invoke-NovaTest](Invoke-NovaTest.md)
[Invoke-NovaBuild](Invoke-NovaBuild.md)
- [Invoke-NovaTest](./Invoke-NovaTest.md)
- [Invoke-NovaBuild](./Invoke-NovaBuild.md)
55 changes: 42 additions & 13 deletions scripts/build/ci/Invoke-NovaModuleToolsCI.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,58 @@ function Copy-NovaModuleToolsArtifactIfPresent {
}
}

function ConvertTo-NovaSingleQuotedPowerShellLiteral {
param(
[Parameter(Mandatory)][string]$Value
)

return "'$($Value.Replace("'", "''") )'"
}

function Get-NovaModuleToolsValidationCommand {
param(
[Parameter(Mandatory)][string]$BuiltModulePath,
[Parameter(Mandatory)][string]$CommandName,
[string[]]$ExcludeTag = @()
)

$commandLine = $CommandName
if (@($ExcludeTag).Count -gt 0) {
$excludeTagLiteral = @($ExcludeTag | ForEach-Object {ConvertTo-NovaSingleQuotedPowerShellLiteral -Value ([string]$_)}) -join ', '
$commandLine += " -ExcludeTagFilter @($excludeTagLiteral)"
}

return "Import-Module $( ConvertTo-NovaSingleQuotedPowerShellLiteral -Value $BuiltModulePath ) -Force -ErrorAction Stop; $commandLine"
}

function Invoke-NovaModuleToolsFreshValidationCommand {
param(
[Parameter(Mandatory)][string]$Command
)

& pwsh -NoLogo -NoProfile -Command $Command
if ($LASTEXITCODE -ne 0) {
throw "Validation command failed: $Command"
}
}

$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..' '..' '..')).Path
Set-Location $repoRoot
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null

Import-Module NovaModuleTools -ErrorAction Stop

Invoke-NovaBuild

$projectInfo = Get-NovaProjectInfo
$builtModulePath = $projectInfo.OutputModuleDir
Remove-Module $projectInfo.ProjectName -ErrorAction SilentlyContinue
Import-Module $builtModulePath -Force
$projectInfo = Get-NovaProjectInfo
Invoke-NovaBuild
$builtModulePath = Join-Path $projectInfo.OutputModuleDir "$( $projectInfo.ProjectName ).psd1"

$novaModuleToolsTestFailed = $false
try {
if (@($ExcludeTag).Count -gt 0) {
Invoke-NovaTest -ExcludeTagFilter $ExcludeTag
Test-NovaBuild -ExcludeTagFilter $ExcludeTag
} else {
Invoke-NovaTest
Test-NovaBuild
}
$unitTestCommand = Get-NovaModuleToolsValidationCommand -BuiltModulePath $builtModulePath -CommandName 'Invoke-NovaTest' -ExcludeTag $ExcludeTag
Invoke-NovaModuleToolsFreshValidationCommand -Command $unitTestCommand

$buildValidationCommand = Get-NovaModuleToolsValidationCommand -BuiltModulePath $builtModulePath -CommandName 'Test-NovaBuild' -ExcludeTag $ExcludeTag
Invoke-NovaModuleToolsFreshValidationCommand -Command $buildValidationCommand
} catch {
$novaModuleToolsTestFailed = $true
Write-Warning "Nova test workflow failed: $( $_.Exception.Message )"
Expand Down
188 changes: 184 additions & 4 deletions src/private/quality/GetNovaTestWorkflowContext.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,205 @@ function Get-NovaTestWorkflowOperation {
}

function Assert-NovaPesterAvailable {
[CmdletBinding()]
param(
[AllowNull()][pscustomobject]$ProjectInfo
)

$moduleSpecification = Get-NovaSupportedPesterModuleSpecification -ProjectInfo $ProjectInfo
Import-NovaSupportedPesterModule -ModuleSpecification $moduleSpecification
return $moduleSpecification
}

function Get-NovaSupportedPesterModuleSpecification {
[CmdletBinding()]
param(
[AllowNull()][pscustomobject]$ProjectInfo
)

$moduleRequirement = Get-NovaPesterModuleRequirement -ProjectInfo $ProjectInfo
$availableModule = @(Get-LoadedNovaPesterModule -ModuleRequirement $moduleRequirement)
if ($availableModule.Count -eq 0) {
$availableModule = @(Get-AvailableNovaPesterModule -ModuleRequirement $moduleRequirement)
}

if ($availableModule.Count -eq 0) {
Stop-NovaOperation -Message (Get-NovaPesterDependencyMessage -ModuleRequirement $moduleRequirement) -ErrorId 'Nova.Dependency.PesterDependencyMissing' -Category ResourceUnavailable -TargetObject 'Pester'
}

$selectedVersion = [version]$availableModule[0].Version
return [pscustomobject]@{
Name = 'Pester'
MinimumVersion = $moduleRequirement.MinimumVersion
MaximumVersion = $moduleRequirement.MaximumVersion
SelectedVersion = $selectedVersion
FullyQualifiedName = @{
ModuleName = 'Pester'
RequiredVersion = [string]$selectedVersion
}
}
}

function Get-NovaSupportedPesterRange {
[CmdletBinding()]
param()

if (-not (Get-Module -Name Pester -ListAvailable)) {
Stop-NovaOperation -Message 'The module Pester must be installed to run Nova tests. Install Pester 5.7.1 and try again.' -ErrorId 'Nova.Dependency.PesterDependencyMissing' -Category ResourceUnavailable -TargetObject 'Pester'
return [pscustomobject]@{
Name = 'Pester'
MinimumVersion = [version]'5.7.1'
MaximumVersion = [version]'5.10.0'
}
}

function Get-NovaPesterModuleRequirement {
[CmdletBinding()]
param(
[AllowNull()][pscustomobject]$ProjectInfo
)

$supportedRequirement = Get-NovaSupportedPesterRange
$manifestSettings = Get-NovaPesterSettingValue -InputObject $ProjectInfo -Name 'Manifest'
$requiredModules = @(Get-NovaPesterSettingValue -InputObject $manifestSettings -Name 'RequiredModules')
$pesterModule = $requiredModules |
Where-Object {(Get-NovaPesterSettingValue -InputObject $_ -Name 'ModuleName') -eq 'Pester'} |
Select-Object -First 1

if ($null -ne $pesterModule) {
$manifestRequirement = [pscustomobject]@{
Name = 'Pester'
MinimumVersion = [version](Get-NovaPesterVersionText -InputObject $pesterModule -Name 'ModuleVersion' -DefaultValue '5.7.1')
MaximumVersion = [version](Get-NovaPesterVersionText -InputObject $pesterModule -Name 'MaximumVersion' -DefaultValue '5.10.0')
}
Assert-NovaPesterModuleRequirementSupported -ModuleRequirement $manifestRequirement -SupportedRequirement $supportedRequirement
}

return $supportedRequirement
}

function Assert-NovaPesterModuleRequirementSupported {
[CmdletBinding()]
param(
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement,
[Parameter(Mandatory)][pscustomobject]$SupportedRequirement
)

if ($ModuleRequirement.MinimumVersion -eq $SupportedRequirement.MinimumVersion -and $ModuleRequirement.MaximumVersion -eq $SupportedRequirement.MaximumVersion) {
return
}

$supportedVersionText = Get-NovaPesterVersionRangeText -ModuleRequirement $SupportedRequirement
$declaredVersionText = Get-NovaPesterVersionRangeText -ModuleRequirement $ModuleRequirement
Stop-NovaOperation -Message "Nova tests support Pester only from $supportedVersionText. project.json declares Pester from $declaredVersionText. Update project.json to match Nova's supported range and try again." -ErrorId 'Nova.Dependency.UnsupportedPesterVersionRequirement' -Category InvalidData -TargetObject 'project.json'
}

function Get-NovaPesterVersionText {
[CmdletBinding()]
param(
[AllowNull()][object]$InputObject,
[Parameter(Mandatory)][string]$Name,
[Parameter(Mandatory)][string]$DefaultValue
)

$value = [string](Get-NovaPesterSettingValue -InputObject $InputObject -Name $Name)
if ( [string]::IsNullOrWhiteSpace($value)) {
return $DefaultValue
}

return $value
}

function Get-NovaPesterVersionRangeText {
[CmdletBinding()]
param(
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
)

return "$( $ModuleRequirement.MinimumVersion ) through $( $ModuleRequirement.MaximumVersion )"
}

function Get-AvailableNovaPesterModule {
[CmdletBinding()]
param(
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
)

return @(
Get-Module -Name Pester -ListAvailable |
Where-Object {Test-NovaPesterModuleVersionSupported -Version $_.Version -ModuleRequirement $ModuleRequirement} |
Sort-Object Version -Descending
)
}

function Get-LoadedNovaPesterModule {
[CmdletBinding()]
param(
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
)

return @(
Get-Module -Name Pester |
Where-Object {Test-NovaPesterModuleVersionSupported -Version $_.Version -ModuleRequirement $ModuleRequirement} |
Sort-Object Version -Descending
)
}

function Test-NovaPesterModuleVersionSupported {
[CmdletBinding()]
param(
[AllowNull()][object]$Version,
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
)

if ($null -eq $Version) {
return $false
}

$resolvedVersion = [version]$Version
return $resolvedVersion -ge $ModuleRequirement.MinimumVersion -and $resolvedVersion -le $ModuleRequirement.MaximumVersion
}

function Get-NovaPesterDependencyMessage {
[CmdletBinding()]
param(
[Parameter(Mandatory)][pscustomobject]$ModuleRequirement
)

$supportedVersionText = Get-NovaPesterVersionRangeText -ModuleRequirement $ModuleRequirement
$installedVersion = @(
Get-Module -Name Pester -ListAvailable |
Sort-Object Version -Descending |
ForEach-Object {[string]$_.Version} |
Select-Object -Unique
)
if ($installedVersion.Count -eq 0) {
return "The module Pester must be installed to run Nova tests. Install a supported Pester version from $supportedVersionText and try again."
}

return "Nova tests require Pester from $supportedVersionText. Installed versions: $( $installedVersion -join ', ' ). Install a supported Pester 5.x version and try again."
}

function Import-NovaSupportedPesterModule {
[CmdletBinding()]
param(
[Parameter(Mandatory)][pscustomobject]$ModuleSpecification
)

Import-Module -FullyQualifiedName $ModuleSpecification.FullyQualifiedName -Force -ErrorAction Stop | Out-Null
}

function Get-NovaTestWorkflowContext {
[CmdletBinding()]
param(
[Parameter(Mandatory)][hashtable]$TestOption,
[Parameter(Mandatory)][hashtable]$BoundParameters
)

Test-ProjectSchema | Out-Null
Assert-NovaPesterAvailable
$projectInfo = Get-NovaProjectInfo
Test-ProjectSchema | Out-Null
$pesterModuleSpecification = Assert-NovaPesterAvailable -ProjectInfo $projectInfo
$workflowProfile = Get-NovaTestWorkflowProfile -TestOption $TestOption
$pesterConfig = New-PesterConfiguration -Hashtable $projectInfo.Pester
Add-Member -InputObject $pesterConfig -MemberType NoteProperty -Name PesterModuleSpecification -Value $pesterModuleSpecification -Force
$coverageConfiguration = Get-NovaPesterCoverageConfigurationState -ProjectInfo $projectInfo -CoverageEnabled:$workflowProfile.CoverageEnabled
$pesterConfig.CodeCoverage.Enabled = $coverageConfiguration.Enabled
$pesterConfig.CodeCoverage.Path = $coverageConfiguration.Path
Expand Down Expand Up @@ -62,6 +241,7 @@ function Get-NovaTestWorkflowContext {
CommandName = $workflowProfile.CommandName
OverrideWarningRequested = $BoundParameters.ContainsKey('OverrideWarning') -and [bool]$BoundParameters.OverrideWarning
ProjectInfo = $projectInfo
PesterModuleSpecification = $pesterModuleSpecification
PesterSettings = Get-NovaTestWorkflowPesterConfiguration -ProjectPesterSettings $projectInfo.Pester -CoverageEnabled:$workflowProfile.CoverageEnabled
PesterConfig = $pesterConfig
TestResultPath = $testResultPath
Expand Down
Loading
Loading