From 6fd6db31759328b6b88e0e5935f388c2eee2ac92 Mon Sep 17 00:00:00 2001 From: Yves Fridelance Date: Thu, 26 Feb 2026 16:41:42 +0100 Subject: [PATCH] feat: add prerelease support, CI/CD branching, and PSScriptAnalyzer Prerelease Support: - Uncomment Prerelease field in .psd1 manifest and fixtures - Add 'Prerelease' to Update-ManifestField ValidateSet with PSGallery validation - Add 'Prerelease' to $Script:SupportedManifestFields - Add -Prerelease parameter to build.ps1 for prerelease builds CI/CD Enhancements: - Split workflow into test (matrix) + build-and-publish (single runner) jobs - Add develop branch triggers for prerelease publishing - Add PS 5.1 matrix entry (windows-latest/powershell) - Add Pester + PSScriptAnalyzer caching (actions/cache) - Add PSScriptAnalyzer step to test job - Set Prerelease tag on develop push, strip on main push - Add DOTNET_CLI_UI_LANGUAGE=en for publish step - Add skip-duplicate (409) error handling for Publish-Module Code Quality: - Fix UTF-8 BOM encoding on 7 private function files - Fix trailing whitespace in .psd1 - Rename $Matches to $AliasMatches in Get-AliasesFromFile (automatic variable) - Add PSScriptAnalyzerSettings.psd1 with intentional suppressions Tests: - Add 5 new Prerelease tests for Update-ManifestField (306 total) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/build.yml | 124 +++++++++++++++--- PSScriptAnalyzerSettings.psd1 | 6 + .../MonolithicModule/MonolithicModule.psd1 | 2 +- Tests/Fixtures/SampleModule/SampleModule.psd1 | 2 +- .../Private/Update-ManifestField.Tests.ps1 | 52 ++++++++ .../Private/ConvertTo-SortedClassFileName.ps1 | 2 +- .../Private/Get-AliasesFromFile.ps1 | 6 +- .../Private/Get-FunctionNamesFromFile.ps1 | 2 +- .../Private/Merge-SourceFiles.ps1 | 2 +- .../Private/New-DevPsm1Content.ps1 | 2 +- .../Private/Split-PsFileContent.ps1 | 2 +- .../Private/Test-ModuleProjectStructure.ps1 | 2 +- .../Private/Update-ManifestField.ps1 | 20 ++- .../YFridelance.PS.ModuleFactory.psd1 | 4 +- .../YFridelance.PS.ModuleFactory.psm1 | 1 + build.ps1 | 12 +- 16 files changed, 206 insertions(+), 35 deletions(-) create mode 100644 PSScriptAnalyzerSettings.psd1 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1fe7fe3..02efc66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,34 +2,63 @@ name: Build & Test on: push: - branches: [ main ] + branches: [ main, develop ] pull_request: - branches: [ main ] + branches: [ main, develop ] jobs: - test-and-build: - name: Test & Build + test: + name: Test (${{ matrix.os }} / ${{ matrix.shell }}) runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] include: - os: ubuntu-latest shell: pwsh - os: windows-latest shell: pwsh + - os: windows-latest + shell: powershell steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Cache PowerShell modules + uses: actions/cache@v4 + id: ps-cache + with: + path: | + ~/.local/share/powershell/Modules + ~/Documents/PowerShell/Modules + ~/Documents/WindowsPowerShell/Modules + key: ps-modules-${{ matrix.os }}-${{ matrix.shell }}-pester5-pssa + - name: Install Pester - shell: pwsh + if: steps.ps-cache.outputs.cache-hit != 'true' + shell: ${{ matrix.shell }} run: | Install-Module -Name Pester -MinimumVersion 5.0 -Force -Scope CurrentUser -SkipPublisherCheck + - name: Install PSScriptAnalyzer + if: steps.ps-cache.outputs.cache-hit != 'true' + shell: ${{ matrix.shell }} + run: | + Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser -SkipPublisherCheck + + - name: Run PSScriptAnalyzer + shell: ${{ matrix.shell }} + run: | + $Results = Invoke-ScriptAnalyzer -Path './YFridelance.PS.ModuleFactory' -Recurse -Settings './PSScriptAnalyzerSettings.psd1' -ReportSummary + if ($Results) { + $Results | Format-Table -AutoSize + throw "PSScriptAnalyzer found $($Results.Count) issue(s)." + } + Write-Host "PSScriptAnalyzer: No issues found." -ForegroundColor Green + - name: Run Pester tests - shell: pwsh + shell: ${{ matrix.shell }} run: | $PesterConfig = New-PesterConfiguration $PesterConfig.Run.Path = './Tests' @@ -40,10 +69,64 @@ jobs: $PesterConfig.TestResult.OutputFormat = 'NUnitXml' Invoke-Pester -Configuration $PesterConfig - - name: Build module (dogfooding) + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.os }}-${{ matrix.shell }} + path: ./TestResults.xml + + build-and-publish: + name: Build & Publish + needs: test + runs-on: ubuntu-latest + if: github.event_name == 'push' + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache PowerShell modules + uses: actions/cache@v4 + id: ps-cache + with: + path: | + ~/.local/share/powershell/Modules + key: ps-modules-ubuntu-latest-pwsh-pester5 + + - name: Install Pester + if: steps.ps-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + Install-Module -Name Pester -MinimumVersion 5.0 -Force -Scope CurrentUser -SkipPublisherCheck + + - name: Build module shell: pwsh run: ./build.ps1 -Clean + - name: Set prerelease tag + if: github.ref == 'refs/heads/develop' + shell: pwsh + run: | + . ./YFridelance.PS.ModuleFactory/Private/Update-ManifestField.ps1 + $Script:DefaultEncoding = [System.Text.UTF8Encoding]::new($true) + $ManifestPath = './dist/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psd1' + $PrereleaseTag = "preview$($env:GITHUB_RUN_NUMBER)" + Update-ManifestField -ManifestPath $ManifestPath -FieldName 'Prerelease' -Value $PrereleaseTag + Write-Host "Prerelease tag set: $PrereleaseTag" -ForegroundColor Green + + - name: Ensure no prerelease tag + if: github.ref == 'refs/heads/main' + shell: pwsh + run: | + . ./YFridelance.PS.ModuleFactory/Private/Update-ManifestField.ps1 + $Script:DefaultEncoding = [System.Text.UTF8Encoding]::new($true) + $ManifestPath = './dist/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psd1' + Update-ManifestField -ManifestPath $ManifestPath -FieldName 'Prerelease' -Value '' + Write-Host "Prerelease tag cleared for stable release." -ForegroundColor Green + - name: Verify build output shell: pwsh run: | @@ -52,32 +135,37 @@ jobs: throw "Build output not found: $ManifestPath" } $Manifest = Test-ModuleManifest -Path $ManifestPath + $Prerelease = $Manifest.PrivateData.PSData.Prerelease Write-Host "Module: $($Manifest.Name)" Write-Host "Version: $($Manifest.Version)" + if ($Prerelease) { Write-Host "Prerelease: $Prerelease" } Write-Host "Functions: $($Manifest.ExportedFunctions.Keys -join ', ')" - name: Upload build artifact - if: matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v4 with: name: YFridelance.PS.ModuleFactory path: ./dist/YFridelance.PS.ModuleFactory/ - name: Publish to PowerShell Gallery - if: matrix.os == 'ubuntu-latest' && github.event_name == 'push' shell: pwsh env: PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + DOTNET_CLI_UI_LANGUAGE: en run: | $ModulePath = './dist/YFridelance.PS.ModuleFactory' if (-not (Test-Path $ModulePath)) { throw "Build output not found: $ModulePath" } - Publish-Module -Path $ModulePath -NuGetApiKey $env:PSGALLERY_API_KEY -Repository PSGallery - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results-${{ matrix.os }} - path: ./TestResults.xml + try { + Publish-Module -Path $ModulePath -NuGetApiKey $env:PSGALLERY_API_KEY -Repository PSGallery -ErrorAction Stop + Write-Host "Published successfully to PSGallery." -ForegroundColor Green + } + catch { + if ($_.Exception.Message -match '409|already exists|duplicate') { + Write-Warning "Module version already exists on PSGallery. Skipping publish." + } + else { + throw + } + } diff --git a/PSScriptAnalyzerSettings.psd1 b/PSScriptAnalyzerSettings.psd1 new file mode 100644 index 0000000..305b6cc --- /dev/null +++ b/PSScriptAnalyzerSettings.psd1 @@ -0,0 +1,6 @@ +@{ + ExcludeRules = @( + 'PSUseShouldProcessForStateChangingFunctions' + 'PSUseSingularNouns' + ) +} diff --git a/Tests/Fixtures/MonolithicModule/MonolithicModule.psd1 b/Tests/Fixtures/MonolithicModule/MonolithicModule.psd1 index 7074e79..65b92c3 100644 --- a/Tests/Fixtures/MonolithicModule/MonolithicModule.psd1 +++ b/Tests/Fixtures/MonolithicModule/MonolithicModule.psd1 @@ -110,7 +110,7 @@ PrivateData = @{ # ReleaseNotes = '' # Prerelease string of this module - # Prerelease = '' + Prerelease = '' # Flag to indicate whether the module requires explicit user acceptance for install/update/save # RequireLicenseAcceptance = $false diff --git a/Tests/Fixtures/SampleModule/SampleModule.psd1 b/Tests/Fixtures/SampleModule/SampleModule.psd1 index 49fd179..8acde32 100644 --- a/Tests/Fixtures/SampleModule/SampleModule.psd1 +++ b/Tests/Fixtures/SampleModule/SampleModule.psd1 @@ -110,7 +110,7 @@ PrivateData = @{ # ReleaseNotes = '' # Prerelease string of this module - # Prerelease = '' + Prerelease = '' # Flag to indicate whether the module requires explicit user acceptance for install/update/save # RequireLicenseAcceptance = $false diff --git a/Tests/Unit/Private/Update-ManifestField.Tests.ps1 b/Tests/Unit/Private/Update-ManifestField.Tests.ps1 index 0859d38..229b1f4 100644 --- a/Tests/Unit/Private/Update-ManifestField.Tests.ps1 +++ b/Tests/Unit/Private/Update-ManifestField.Tests.ps1 @@ -152,4 +152,56 @@ Describe 'Update-ManifestField' { $Data.AliasesToExport | Should -Contain 'al2' } } + + Context 'Scalar field - Prerelease (nested in PSData)' { + BeforeAll { + $Script:PrereleaseManifestPath = Join-Path -Path $Script:TempDir -ChildPath 'PrereleaseTest.psd1' + } + + BeforeEach { + # Create a fresh copy for each test + Copy-Item -Path $Script:RefManifestPath -Destination $Script:PrereleaseManifestPath -Force + # Uncomment the Prerelease field so Update-ManifestField can locate and replace it + $Content = [System.IO.File]::ReadAllText($Script:PrereleaseManifestPath, [System.Text.Encoding]::UTF8) + $Content = $Content -replace '# Prerelease = ''''', 'Prerelease = ''''' + [System.IO.File]::WriteAllText($Script:PrereleaseManifestPath, $Content, [System.Text.UTF8Encoding]::new($true)) + } + + It 'should set Prerelease to a preview string' { + Update-ManifestField -ManifestPath $Script:PrereleaseManifestPath -FieldName 'Prerelease' -Value 'preview1' + $Data = Import-PowerShellDataFile -Path $Script:PrereleaseManifestPath + $Data.PrivateData.PSData.Prerelease | Should -Be 'preview1' + } + + It 'should clear Prerelease by setting to empty string' { + Update-ManifestField -ManifestPath $Script:PrereleaseManifestPath -FieldName 'Prerelease' -Value 'preview1' + Update-ManifestField -ManifestPath $Script:PrereleaseManifestPath -FieldName 'Prerelease' -Value '' + $Data = Import-PowerShellDataFile -Path $Script:PrereleaseManifestPath + $Data.PrivateData.PSData.Prerelease | Should -Be '' + } + + It 'should handle hyphenated prerelease strings' { + Update-ManifestField -ManifestPath $Script:PrereleaseManifestPath -FieldName 'Prerelease' -Value 'beta-1' + $Data = Import-PowerShellDataFile -Path $Script:PrereleaseManifestPath + $Data.PrivateData.PSData.Prerelease | Should -Be 'beta-1' + } + + It 'should preserve other manifest fields when updating Prerelease' { + Update-ManifestField -ManifestPath $Script:PrereleaseManifestPath -FieldName 'Prerelease' -Value 'preview1' + $Data = Import-PowerShellDataFile -Path $Script:PrereleaseManifestPath + # Verify Prerelease was updated + $Data.PrivateData.PSData.Prerelease | Should -Be 'preview1' + # Verify top-level fields are preserved + $Data.ModuleVersion | Should -Be '1.0.0' + $Data.Author | Should -Be 'TestAuthor' + $Data.RootModule | Should -Be 'RefModule.psm1' + } + + It 'should update Prerelease when it already has a value' { + Update-ManifestField -ManifestPath $Script:PrereleaseManifestPath -FieldName 'Prerelease' -Value 'alpha1' + Update-ManifestField -ManifestPath $Script:PrereleaseManifestPath -FieldName 'Prerelease' -Value 'beta2' + $Data = Import-PowerShellDataFile -Path $Script:PrereleaseManifestPath + $Data.PrivateData.PSData.Prerelease | Should -Be 'beta2' + } + } } diff --git a/YFridelance.PS.ModuleFactory/Private/ConvertTo-SortedClassFileName.ps1 b/YFridelance.PS.ModuleFactory/Private/ConvertTo-SortedClassFileName.ps1 index 8e9646b..cb9e9af 100644 --- a/YFridelance.PS.ModuleFactory/Private/ConvertTo-SortedClassFileName.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/ConvertTo-SortedClassFileName.ps1 @@ -1,4 +1,4 @@ -function ConvertTo-SortedClassFileName { +function ConvertTo-SortedClassFileName { <# .SYNOPSIS Converts a class name and sort index into a standardised, numerically-prefixed class file name. diff --git a/YFridelance.PS.ModuleFactory/Private/Get-AliasesFromFile.ps1 b/YFridelance.PS.ModuleFactory/Private/Get-AliasesFromFile.ps1 index fdf0486..5eb9bfe 100644 --- a/YFridelance.PS.ModuleFactory/Private/Get-AliasesFromFile.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/Get-AliasesFromFile.ps1 @@ -52,16 +52,16 @@ function Get-AliasesFromFile { # Pattern: optional leading whitespace, # Alias: $AliasPattern = '(?m)^\s*#\s*Alias\s*:\s*(.+)\s*$' - $Matches = [System.Text.RegularExpressions.Regex]::Matches($FileContent, $AliasPattern) + $AliasMatches = [System.Text.RegularExpressions.Regex]::Matches($FileContent, $AliasPattern) - if ($Matches.Count -eq 0) { + if ($AliasMatches.Count -eq 0) { Write-Verbose "Get-AliasesFromFile: No alias annotations found in '$FilePath'." return [string[]]@() } $AliasNames = [System.Collections.Generic.List[string]]::new() - foreach ($Match in $Matches) { + foreach ($Match in $AliasMatches) { $RawValue = $Match.Groups[1].Value.Trim() Write-Verbose " Found alias annotation value: '$RawValue'" diff --git a/YFridelance.PS.ModuleFactory/Private/Get-FunctionNamesFromFile.ps1 b/YFridelance.PS.ModuleFactory/Private/Get-FunctionNamesFromFile.ps1 index f323794..936aee1 100644 --- a/YFridelance.PS.ModuleFactory/Private/Get-FunctionNamesFromFile.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/Get-FunctionNamesFromFile.ps1 @@ -1,4 +1,4 @@ -function Get-FunctionNamesFromFile { +function Get-FunctionNamesFromFile { <# .SYNOPSIS Extracts the names of all top-level function definitions from a PowerShell script file. diff --git a/YFridelance.PS.ModuleFactory/Private/Merge-SourceFiles.ps1 b/YFridelance.PS.ModuleFactory/Private/Merge-SourceFiles.ps1 index c345b28..322e500 100644 --- a/YFridelance.PS.ModuleFactory/Private/Merge-SourceFiles.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/Merge-SourceFiles.ps1 @@ -1,4 +1,4 @@ -function Merge-SourceFiles { +function Merge-SourceFiles { <# .SYNOPSIS Merges ordered PowerShell source files into a single monolithic script string. diff --git a/YFridelance.PS.ModuleFactory/Private/New-DevPsm1Content.ps1 b/YFridelance.PS.ModuleFactory/Private/New-DevPsm1Content.ps1 index 35eeef1..4b02576 100644 --- a/YFridelance.PS.ModuleFactory/Private/New-DevPsm1Content.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/New-DevPsm1Content.ps1 @@ -1,4 +1,4 @@ -function New-DevPsm1Content { +function New-DevPsm1Content { <# .SYNOPSIS Generates the content for a development .psm1 file that dot-sources all source files dynamically. diff --git a/YFridelance.PS.ModuleFactory/Private/Split-PsFileContent.ps1 b/YFridelance.PS.ModuleFactory/Private/Split-PsFileContent.ps1 index 1f6277f..f1ed43c 100644 --- a/YFridelance.PS.ModuleFactory/Private/Split-PsFileContent.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/Split-PsFileContent.ps1 @@ -1,4 +1,4 @@ -function Split-PsFileContent { +function Split-PsFileContent { <# .SYNOPSIS Parses a PowerShell file and splits its content into typed, named segments. diff --git a/YFridelance.PS.ModuleFactory/Private/Test-ModuleProjectStructure.ps1 b/YFridelance.PS.ModuleFactory/Private/Test-ModuleProjectStructure.ps1 index 9faab62..bd9cca0 100644 --- a/YFridelance.PS.ModuleFactory/Private/Test-ModuleProjectStructure.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/Test-ModuleProjectStructure.ps1 @@ -1,4 +1,4 @@ -function Test-ModuleProjectStructure { +function Test-ModuleProjectStructure { <# .SYNOPSIS Validates the structure of a PowerShell module project directory. diff --git a/YFridelance.PS.ModuleFactory/Private/Update-ManifestField.ps1 b/YFridelance.PS.ModuleFactory/Private/Update-ManifestField.ps1 index 2b19869..e6a26f8 100644 --- a/YFridelance.PS.ModuleFactory/Private/Update-ManifestField.ps1 +++ b/YFridelance.PS.ModuleFactory/Private/Update-ManifestField.ps1 @@ -1,4 +1,4 @@ -function Update-ManifestField { +function Update-ManifestField { <# .SYNOPSIS Updates a single field in a PowerShell module manifest (.psd1) file in place. @@ -20,6 +20,9 @@ function Update-ManifestField { Scalar fields (ModuleVersion, Description, Author): - Written as 'value' (single-quoted string). + Prerelease is handled as a scalar field within the PrivateData.PSData section. + Only alphanumeric characters and hyphens are valid for PSGallery prerelease strings. + The file is written back with UTF-8 BOM encoding and CRLF line endings. .PARAMETER ManifestPath @@ -27,7 +30,7 @@ function Update-ManifestField { .PARAMETER FieldName The manifest field to update. Must be one of: - FunctionsToExport, AliasesToExport, ModuleVersion, Description, Author. + FunctionsToExport, AliasesToExport, ModuleVersion, Description, Author, Prerelease. .PARAMETER Value The new value for the field. For array fields, pass a [string[]] or [object[]]. @@ -43,6 +46,11 @@ function Update-ManifestField { Update-ManifestField -ManifestPath 'C:\MyModule\MyModule.psd1' ` -FieldName 'FunctionsToExport' ` -Value $Functions + + .EXAMPLE + Update-ManifestField -ManifestPath 'C:\MyModule\MyModule.psd1' ` + -FieldName 'Prerelease' ` + -Value 'beta1' #> [CmdletBinding()] [OutputType([void])] @@ -52,7 +60,7 @@ function Update-ManifestField { [string]$ManifestPath, [Parameter(Mandatory = $true)] - [ValidateSet('FunctionsToExport', 'AliasesToExport', 'ModuleVersion', 'Description', 'Author')] + [ValidateSet('FunctionsToExport', 'AliasesToExport', 'ModuleVersion', 'Description', 'Author', 'Prerelease')] [string]$FieldName, [Parameter(Mandatory = $true)] @@ -115,6 +123,12 @@ function Update-ManifestField { Write-Verbose " New value string: $ValueString" + # Validate Prerelease value for PSGallery compatibility + if ($FieldName -eq 'Prerelease' -and $Value -ne '' -and $Value -match '[^a-zA-Z0-9-]') { + Write-Warning ("Update-ManifestField: Prerelease value '$Value' contains characters not supported by PSGallery. " + + "Only alphanumeric characters and hyphens are allowed.") + } + # Regex pattern to match the field and its current value in the .psd1 # Handles: # FieldName = 'value' diff --git a/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psd1 b/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psd1 index eda4a08..a8c6faf 100644 --- a/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psd1 +++ b/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psd1 @@ -69,7 +69,7 @@ PowerShellVersion = '5.1' # NestedModules = @() # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = 'Build-PSModule', 'Initialize-PSModule', 'Split-PSModule', +FunctionsToExport = 'Build-PSModule', 'Initialize-PSModule', 'Split-PSModule', 'Update-PSModuleVersion' # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. @@ -111,7 +111,7 @@ PrivateData = @{ # ReleaseNotes = '' # Prerelease string of this module - # Prerelease = '' + Prerelease = '' # Flag to indicate whether the module requires explicit user acceptance for install/update/save # RequireLicenseAcceptance = $false diff --git a/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psm1 b/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psm1 index aafd02c..fed482e 100644 --- a/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psm1 +++ b/YFridelance.PS.ModuleFactory/YFridelance.PS.ModuleFactory.psm1 @@ -14,6 +14,7 @@ $Script:SupportedManifestFields = @( 'ModuleVersion' 'Description' 'Author' + 'Prerelease' ) $Script:DefaultEncoding = [System.Text.UTF8Encoding]::new($true) $Script:LoadOrderFolders = @('Enums', 'Classes', 'Private', 'Public') diff --git a/build.ps1 b/build.ps1 index 36f8e58..0c411b9 100644 --- a/build.ps1 +++ b/build.ps1 @@ -20,7 +20,9 @@ #> [CmdletBinding()] param( - [switch]$Clean + [switch]$Clean, + + [string]$Prerelease ) $ErrorActionPreference = 'Stop' @@ -52,6 +54,14 @@ if ($Result.Success) { Write-Host " Functions: $($Result.FunctionsExported -join ', ')" -ForegroundColor Green Write-Host " Aliases: $($Result.AliasesExported -join ', ')" -ForegroundColor Green Write-Host " Files: $($Result.FilesMerged) files merged" -ForegroundColor Green + if ($Prerelease) { + # Dot-source the private helper (not exported by the module) + . (Join-Path -Path $ModulePath -ChildPath (Join-Path -Path 'Private' -ChildPath 'Update-ManifestField.ps1')) + $Script:DefaultEncoding = [System.Text.UTF8Encoding]::new($true) + $BuiltManifestPath = Join-Path -Path $OutputPath -ChildPath 'YFridelance.PS.ModuleFactory.psd1' + Update-ManifestField -ManifestPath $BuiltManifestPath -FieldName 'Prerelease' -Value $Prerelease + Write-Host " Prerelease: $Prerelease" -ForegroundColor Green + } } else { Write-Host "`nBuild failed!" -ForegroundColor Red