Skip to content
Merged
20 changes: 20 additions & 0 deletions .github/skills/skills/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
name: skills
description: Index folder for vendored shared skills used by NovaModuleTools scaffolding and local guidance.
---

# Skill: skills

## When to use

Use this folder only as a container for nested shared skills that are maintained separately from the repository-local skill entry points.

## Relevant files

- `.github/skills/skills/*`

## Expected practices

- Do not reference this container skill directly from prompts or agents.
- Keep nested shared skills organized beneath this folder when they are intentionally vendored for local reuse.
- Prefer the repository-local top-level skills under `.github/skills/*/SKILL.md` for normal NovaModuleTools task routing.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- `project.json` now supports `Manifest.PrivateData` for arbitrary consumer-defined manifest metadata.
- Nova still uses `New-ModuleManifest` for the standard manifest flow, but now serializes structured `PrivateData` itself so nested objects, arrays, booleans, numbers, strings, and `null` values survive in the generated `.psd1`.
- Generated manifests now preserve both PowerShell/Nova-managed `PrivateData.PSData` metadata and custom `Manifest.PrivateData` entries, and reject reserved-key collisions instead of overwriting `PSData` silently.

### Changed

### Deprecated
Expand All @@ -15,6 +19,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Fixed

- `Update-NovaModuleTool` and `% nova update` now split self-update failure details and recovery guidance across separate terminal lines, so long dependency errors stay readable instead of wrapping mid-sentence.
- `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.1] - 2026-07-15
Expand Down Expand Up @@ -535,4 +545,3 @@ This release was yanked because it removed the implicit `Pester` dependency, bef
[0.0.6]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.5...Version_0.0.6
[0.0.5]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.4...Version_0.0.5
[0.0.4]: https://github.com/stiwicourage/NovaModuleTools/compare/Version_0.0.3...Version_0.0.4

8 changes: 7 additions & 1 deletion RELEASE_NOTE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang

### Added

- `project.json` now supports `Manifest.PrivateData` for arbitrary consumer-defined module manifest metadata.
- Nova still uses `New-ModuleManifest` for the standard manifest flow, but now serializes structured `PrivateData` itself so nested values remain valid in the generated `.psd1`.
- Generated manifests preserve both `PrivateData.PSData` and custom `Manifest.PrivateData` entries, and reject reserved-key collisions instead of overwriting manifest metadata silently.

### Changed

### Deprecated
Expand All @@ -14,6 +18,9 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang

### Fixed

- `Update-NovaModuleTool` and `% nova update` now keep self-update failure details and recovery guidance on separate terminal lines so the error is easier to scan.
- `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.1] - 2026-07-15
Expand Down Expand Up @@ -276,4 +283,3 @@ This release was yanked because it removed the implicit `Pester` dependency befo
## [0.0.4] - 2024-06-25
### Added
- First PowerShell Gallery release of NovaModuleTools with the initial module workflow support.

39 changes: 37 additions & 2 deletions docs/project-json-reference.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ <h2>How to read this file</h2>
<strong>Schema coverage.</strong>
<p>The unified JSON schema covers all documented fields including
<code>Manifest.ReleaseNotes</code>, <code>Manifest.LicenseUri</code>,
<code>Manifest.IconUri</code>, and <code>Pester.TestResult.OutputFormat</code>. VS Code picks
<code>Manifest.IconUri</code>, <code>Manifest.PrivateData</code>, and <code>Pester.TestResult.OutputFormat</code>. VS Code picks
up the schema automatically when the <code>$schema</code> field is present in
<code>project.json</code>.</p>
</div>
Expand Down Expand Up @@ -142,7 +142,13 @@ <h2>Complete example</h2>
"Tags": ["Example", "NovaModuleTools", "PowerShell"],
"ProjectUri": "https://www.novamoduletools.com/",
"ReleaseNotes": "https://www.novamoduletools.com/release-notes.html",
"LicenseUri": "https://www.novamoduletools.com/license.html"
"LicenseUri": "https://www.novamoduletools.com/license.html",
"PrivateData": {
"ExampleProduct": {
"Enabled": true,
"ApiVersion": "1"
}
}
},
"Package": {
"Id": "NovaExampleModule",
Expand Down Expand Up @@ -372,12 +378,41 @@ <h2><code>Manifest</code> settings</h2>
publicly.
</td>
</tr>
<tr>
<td><code>PrivateData</code></td>
<td>No</td>
<td>Adds arbitrary consumer-defined metadata to the manifest <code>PrivateData</code> hashtable.</td>
<td>Nova serializes this data itself so arbitrary property names and nested objects, arrays, booleans, numbers, strings, and <code>null</code> values remain valid PowerShell data in the generated manifest.</td>
</tr>
</tbody>
</table>
</div>
<p class="tiny-note">If <code>Tags</code>, <code>ProjectUri</code>, <code>ReleaseNotes</code>, or <code>LicenseUri</code>
are omitted, packaging still succeeds. The corresponding package metadata fields are simply left
out.</p>
<p>Use <code>Manifest.PrivateData</code> for arbitrary module metadata. Nova keeps the standard PowerShell-generated
<code>PrivateData.PSData</code> metadata and merges your configured keys into the same top-level
<code>PrivateData</code> hashtable. Property names remain consumer-defined, so keys such as
<code>Example Product</code>, <code>Feature-Flag</code>, or <code>123abc</code> are serialized safely.</p>
<p class="tiny-note">Nova validates the rewritten manifest by re-importing the generated
<code>.psd1</code>. PowerShell's own <code>Test-ModuleManifest</code> cmdlet can still reject
consumer-defined top-level <code>PrivateData</code> entries even when the manifest is valid
PowerShell data, so Nova treats the data-file import round-trip as the compatibility check for
custom metadata.</p>
<pre><code>{
"Manifest": {
"PrivateData": {
"Example Product": {
"Feature-Flag": true,
"ApiVersion": "1",
"RetryCount": 3,
"Nested Data": {
"Mode": "Test"
}
}
}
}
}</code></pre>
</section>

<section class="content-section" id="package">
Expand Down
5 changes: 5 additions & 0 deletions docs/schema/v3/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@
"type": "string",
"description": "Sets the copyright string in the module manifest."
},
"PrivateData": {
"type": "object",
"additionalProperties": true,
"description": "Arbitrary consumer-defined module metadata serialized into the generated manifest PrivateData hashtable."
},
"RequiredModules": {
"type": "array",
"description": "Lists modules that must be imported before this module. Each entry is either a module name string or a module-specification object.",
Expand Down
2 changes: 1 addition & 1 deletion project.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"ProjectName": "NovaModuleTools",
"Description": "NovaModuleTools is an enterprise-focused build tool for PowerShell module development, with a strong emphasis on structure, maintainability, and build for automated CI/CD pipelines.",
"Version": "3.3.1",
"Version": "3.3.2-preview",
"Preamble": [
"Set-StrictMode -Version Latest",
"$ErrorActionPreference = 'Stop'"
Expand Down
125 changes: 85 additions & 40 deletions src/private/build/BuildManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,66 +6,111 @@ function Build-Manifest {

Write-Verbose 'Building psd1 data file Manifest'
$data = Get-NovaBuildProjectInfo -ProjectInfo $ProjectInfo

$PubFunctionFiles = @(Get-ChildItem -Path $data.PublicDir -Filter *.ps1)
$functionToExport = @()
$aliasToExport = @()
foreach ($pubFunctionFile in $PubFunctionFiles) {
$functionToExport += Get-FunctionNameFromFile -filePath $pubFunctionFile.FullName
$aliasToExport += Get-AliasInFunctionFromFile -filePath $pubFunctionFile.FullName
}

## Import Format.ps1xml (if any)
$FormatsToProcess = @()
Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Format.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object {
if ($data.CopyResourcesToModuleRoot) {
$FormatsToProcess += $_.Name
} else {
$FormatsToProcess += Join-Path -Path 'resources' -ChildPath $_.Name
}
}

## Import Types.ps1xml1 (if any)
$TypesToProcess = @()
Get-ChildItem -Path $data.ResourcesDir -File -Filter '*Types.ps1xml' -ErrorAction SilentlyContinue | ForEach-Object {
if ($data.CopyResourcesToModuleRoot) {
$TypesToProcess += $_.Name
} else {
$TypesToProcess += Join-Path -Path 'resources' -ChildPath $_.Name
}
}
$exportDefinition = Get-ManifestExportDefinition -PublicDir $data.PublicDir
$formatFiles = Get-ManifestResourceFilePath -ResourcesDir $data.ResourcesDir -CopyResourcesToModuleRoot:$data.CopyResourcesToModuleRoot -Filter '*Format.ps1xml'
$typeFiles = Get-ManifestResourceFilePath -ResourcesDir $data.ResourcesDir -CopyResourcesToModuleRoot:$data.CopyResourcesToModuleRoot -Filter '*Types.ps1xml'

$ManfiestAllowedParams = (Get-Command New-ModuleManifest).Parameters.Keys
Assert-ManifestSchema -Manifest $data.Manifest -AllowedParameter $ManfiestAllowedParams
$sv = [semver]$data.Version
$ParmsManifest = @{
Path = $data.ManifestFilePSD1
Description = $data.Description
FunctionsToExport = $functionToExport
AliasesToExport = $aliasToExport
FunctionsToExport = $exportDefinition.FunctionToExport
AliasesToExport = $exportDefinition.AliasToExport
RootModule = "$( $data.ProjectName ).psm1"
ModuleVersion = [version]$sv
FormatsToProcess = $FormatsToProcess
TypesToProcess = $TypesToProcess
FormatsToProcess = $formatFiles
TypesToProcess = $typeFiles
}

## Release lable
if ($sv.PreReleaseLabel) {
$ParmsManifest['Prerelease'] = $sv.PreReleaseLabel
}

# Accept only valid Manifest Parameters
$data.Manifest.Keys | ForEach-Object {
if ($ManfiestAllowedParams -contains $_) {
if ($data.Manifest.$_) {
$ParmsManifest.add($_, $data.Manifest.$_)
}
}
}
Add-AllowedManifestParameterEntry -ManifestParameters $ParmsManifest -Manifest $data.Manifest -AllowedParameter $ManfiestAllowedParams

try {
New-ModuleManifest @ParmsManifest
} catch {
Stop-NovaOperation -Message ('Failed to create Manifest: {0}' -f $_.Exception.Message) -ErrorId 'Nova.Dependency.ModuleManifestCreationFailed' -Category OpenError -TargetObject $data.ManifestFilePSD1
}

if ($data.Manifest.Contains('PrivateData') -and $data.Manifest['PrivateData'] -is [System.Collections.IDictionary]) {
Update-ManifestPrivateData -ManifestPath $data.ManifestFilePSD1 -PrivateData $data.Manifest['PrivateData']
Assert-GeneratedManifestPrivateDataCanBeImported -ManifestPath $data.ManifestFilePSD1
}
}

function Assert-GeneratedManifestPrivateDataCanBeImported {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$ManifestPath
)

try {
$null = Import-PowerShellDataFile -LiteralPath $ManifestPath -ErrorAction Stop
} catch {
Stop-NovaOperation -Message ('Generated manifest contains invalid PrivateData: {0}' -f $_.Exception.Message) -ErrorId 'Nova.Dependency.ModuleManifestPrivateDataValidationFailed' -Category InvalidData -TargetObject $ManifestPath
}
}

function Get-ManifestExportDefinition {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$PublicDir
)

$functionToExport = @()
$aliasToExport = @()
foreach ($publicFunctionFile in @(Get-ChildItem -Path $PublicDir -Filter *.ps1)) {
$functionToExport += Get-FunctionNameFromFile -filePath $publicFunctionFile.FullName
$aliasToExport += Get-AliasInFunctionFromFile -filePath $publicFunctionFile.FullName
}

return [pscustomobject]@{
FunctionToExport = $functionToExport
AliasToExport = $aliasToExport
}
}

function Get-ManifestResourceFilePath {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$ResourcesDir,
[Parameter(Mandatory)][string]$Filter,
[switch]$CopyResourcesToModuleRoot
)

$resourceFilePath = @()
Get-ChildItem -Path $ResourcesDir -File -Filter $Filter -ErrorAction SilentlyContinue | ForEach-Object {
if ($CopyResourcesToModuleRoot) {
$resourceFilePath += $_.Name
return
}

$resourceFilePath += Join-Path -Path 'resources' -ChildPath $_.Name
}

return $resourceFilePath
}

function Add-AllowedManifestParameterEntry {
[CmdletBinding()]
param(
[Parameter(Mandatory)][hashtable]$ManifestParameters,
[Parameter(Mandatory)][System.Collections.IDictionary]$Manifest,
[Parameter(Mandatory)][string[]]$AllowedParameter
)

foreach ($name in $Manifest.Keys) {
if ($name -eq 'PrivateData' -or $AllowedParameter -notcontains $name) {
continue
}

if ($Manifest.$name) {
$ManifestParameters.add($name, $Manifest.$name)
}
}
}
Loading
Loading