From 444e3c0d01fbe291ccd28f74d99463246191c69d Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 11:19:10 +0200 Subject: [PATCH 01/16] fix(#215): restore deploy help related links --- CHANGELOG.md | 2 + RELEASE_NOTE.md | 2 + .../en-US/Deploy-NovaPackage.md | 273 ++++++++++-------- .../InvokeNovaPackageArtifactUpload.ps1 | 4 +- .../InvokeNovaPackageUploadWorkflow.ps1 | 56 +++- .../WriteNovaPackageUploadResultOutput.ps1 | 50 ++++ .../WriteNovaPackageUploadWorkflowContext.ps1 | 33 +++ src/public/DeployNovaPackage.ps1 | 7 +- tests/ArchitectureGuardrails.Tests.ps1 | 2 +- .../InvokeNovaPackageArtifactUpload.Tests.ps1 | 4 +- .../InvokeNovaPackageUploadWorkflow.Tests.ps1 | 21 +- ...iteNovaPackageUploadResultOutput.Tests.ps1 | 31 ++ ...NovaPackageUploadWorkflowContext.Tests.ps1 | 37 +++ .../public/DeployNovaPackage.TestSupport.ps1 | 2 + tests/public/DeployNovaPackage.Tests.ps1 | 14 +- 15 files changed, 394 insertions(+), 144 deletions(-) create mode 100644 src/private/package/WriteNovaPackageUploadResultOutput.ps1 create mode 100644 src/private/package/WriteNovaPackageUploadWorkflowContext.ps1 create mode 100644 tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 create mode 100644 tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7955eb64..75101485 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Changed +- `Deploy-NovaPackage` now shows a concise resolved-upload summary before execution, reports progress while multiple artifacts are uploading, and prints a short completion summary with a suggested verification step after successful raw uploads. + ### Deprecated ### Removed diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index d318a843..4e20f222 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -13,6 +13,8 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang ### Changed +- `Deploy-NovaPackage` now shows clearer terminal feedback during raw package uploads, including a concise pre-flight summary, progress across multiple artifacts, and a short verification hint after success. + ### Deprecated ### Removed diff --git a/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md b/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md index 4a6fc558..d86b97eb 100644 --- a/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md +++ b/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md @@ -1,10 +1,10 @@ ---- +--- document type: cmdlet external help file: NovaModuleTools-Help.xml -HelpUri: 'https://www.novamoduletools.com/packaging-and-delivery.html#upload' +HelpUri: https://www.novamoduletools.com/packaging-and-delivery.html#upload Locale: en-US Module Name: NovaModuleTools -ms.date: 04/25/2026 +ms.date: 05.24.2026 PlatyPS schema version: 2024-05-01 title: Deploy-NovaPackage --- @@ -19,10 +19,15 @@ Uploads one or more generated package artifacts to a raw HTTP endpoint. ### __AllParameterSets -```powershell -PS> Deploy-NovaPackage [[-PackagePath] ] [[-PackageType] ] [[-Url] ] [[-Repository] ] [[-UploadPath] ] [[-Headers] ] [[-Token] ] [[-TokenEnvironmentVariable] ] [[-AuthenticationScheme] ] [-WhatIf] [-Confirm] [] +``` +Deploy-NovaPackage [[-PackagePath] ] [[-PackageType] ] [[-Url] ] + [[-Repository] ] [-WhatIf] [-Confirm] [-UploadPath ] [-Headers ] + [-Token ] [-TokenEnvironmentVariable ] [-AuthenticationScheme ] + [] ``` +## ALIASES + ## DESCRIPTION `Deploy-NovaPackage` uploads existing package artifacts that were generated by `New-NovaModulePackage`. @@ -42,6 +47,8 @@ This command is intentionally separate from `Publish-NovaModule`. `Deploy-NovaPa When you run `Deploy-NovaPackage -Confirm`, PowerShell uses its native confirmation prompt against the full resolved upload set instead of prompting once per artifact. +Before uploads start, `Deploy-NovaPackage` prints a concise summary of the resolved upload set. During multi-artifact uploads it shows progress, and after success it prints a short completion summary plus a verification hint while still returning result objects for automation. + ## EXAMPLES ### EXAMPLE 1 @@ -87,10 +94,10 @@ Uploads the explicitly selected package files instead of discovering them from t ### EXAMPLE 6 ```powershell -PS> Deploy-NovaPackage -Repository LocalNexus -WhatIf +PS> Deploy-NovaPackage -PackageType @('NuGet', 'Zip') -Repository LocalNexus ``` -Previews which package artifacts would be uploaded and which destination URLs would be used. +Uploads both matching `.nupkg` and `.zip` artifacts, shows progress as each artifact is sent, and prints a short completion summary with a suggested verification step. ### EXAMPLE 7 @@ -102,130 +109,138 @@ Uses PowerShell's native confirmation prompt for the full resolved upload set be ## PARAMETERS -### -PackagePath +### -AuthenticationScheme -Optional explicit package file path list. When omitted, the command resolves matching artifacts from the configured package output directory. +Optional authentication scheme prefix used when formatting the resolved authentication header value. ```yaml -Type: System.String[] +Type: System.String DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -PackageType +### -Confirm -Optional package type filter. Supported values follow the same normalization as `Package.Types`, including `NuGet`, -`Zip`, `.nupkg`, and `.zip`. +Prompts for confirmation before uploading package artifacts. ```yaml -Type: System.String[] -DefaultValue: '' +Type: System.Management.Automation.SwitchParameter +DefaultValue: False SupportsWildcards: false -Aliases: [ ] +Aliases: +- cf ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -Url +### -Headers -Explicit raw upload base URL. This takes precedence over repository or package configuration. +Optional additional HTTP headers. These are merged with `Package.Headers` and repository-specific headers. +Optional additional HTTP headers. +These are merged with `Package.Headers` and repository-specific headers. ```yaml -Type: System.String +Type: System.Collections.Hashtable DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -Repository +### -PackagePath -Repository name to resolve from `Package.Repositories`. +Optional explicit package file path list. When omitted, the command resolves matching artifacts from the configured package output directory. +Optional explicit package file path list. +When omitted, the command resolves matching artifacts from the configured package output directory. ```yaml -Type: System.String +Type: System.String[] DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: 0 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -UploadPath +### -PackageType -Optional extra path segment appended between the resolved base URL and the uploaded package file name. +Optional package type filter. Supported values follow the same normalization as `Package.Types`, including `NuGet`, +`Zip`, `.nupkg`, and `.zip`. +Optional package type filter. +Supported values follow the same normalization as `Package.Types`, including `NuGet`, +`Zip`, `.nupkg`, and `.zip`. ```yaml -Type: System.String +Type: System.String[] DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: 1 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -Headers +### -Repository -Optional additional HTTP headers. These are merged with `Package.Headers` and repository-specific headers. +Repository name to resolve from `Package.Repositories`. ```yaml -Type: System.Collections.Hashtable +Type: System.String DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: 3 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` @@ -237,16 +252,16 @@ Optional explicit token value used to populate the resolved authentication heade Type: System.String DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` @@ -258,89 +273,93 @@ Optional environment variable name that holds the upload token. Type: System.String DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -AuthenticationScheme +### -UploadPath -Optional authentication scheme prefix used when formatting the resolved authentication header value. +Optional extra path segment appended between the resolved base URL and the uploaded package file name. ```yaml Type: System.String DefaultValue: '' SupportsWildcards: false -Aliases: [ ] +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -WhatIf +### -Url -Shows what would happen if the cmdlet runs. The cmdlet is not run. +Explicit raw upload base URL. This takes precedence over repository or package configuration. +Explicit raw upload base URL. +This takes precedence over repository or package configuration. ```yaml -Type: System.Management.Automation.SwitchParameter -DefaultValue: False +Type: System.String +DefaultValue: '' SupportsWildcards: false -Aliases: - - wi +Aliases: [] ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: 2 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` -### -Confirm +### -WhatIf -Prompts for confirmation before uploading package artifacts. +Shows what would happen if the cmdlet runs. The cmdlet is not run. +Shows what would happen if the cmdlet runs. +The cmdlet is not run. ```yaml Type: System.Management.Automation.SwitchParameter DefaultValue: False SupportsWildcards: false Aliases: - - cf +- wi ParameterSets: - - Name: (All) - Position: Named - IsRequired: false - ValueFromPipeline: false - ValueFromPipelineByPropertyName: false - ValueFromRemainingArguments: false +- Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false DontShow: false -AcceptedValues: [ ] +AcceptedValues: [] HelpMessage: '' ``` ### CommonParameters -This cmdlet supports the common parameters: `-Debug`, `-ErrorAction`, `-ErrorVariable`, `-InformationAction`, -`-InformationVariable`, `-OutBuffer`, `-OutVariable`, `-PipelineVariable`, `-ProgressAction`, `-Verbose`, -`-WarningAction`, `-WarningVariable`, `-WhatIf`, and `-Confirm`. +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, +-InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, +-ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see +[about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). ## INPUTS diff --git a/src/private/package/InvokeNovaPackageArtifactUpload.ps1 b/src/private/package/InvokeNovaPackageArtifactUpload.ps1 index 09a0f209..c11a3c16 100644 --- a/src/private/package/InvokeNovaPackageArtifactUpload.ps1 +++ b/src/private/package/InvokeNovaPackageArtifactUpload.ps1 @@ -6,13 +6,13 @@ function Invoke-NovaPackageArtifactUpload { ) if (-not (Test-Path -LiteralPath $UploadArtifact.PackagePath -PathType Leaf)) { - Stop-NovaOperation -Message "Package file not found: $( $UploadArtifact.PackagePath )" -ErrorId 'Nova.Environment.PackageUploadFileNotFound' -Category ObjectNotFound -TargetObject $UploadArtifact.PackagePath + Stop-NovaOperation -Message "Package file not found: $( $UploadArtifact.PackagePath ). Run New-NovaModulePackage first or provide a valid -PackagePath." -ErrorId 'Nova.Environment.PackageUploadFileNotFound' -Category ObjectNotFound -TargetObject $UploadArtifact.PackagePath } try { $response = Invoke-NovaPackageUploadRequest -UploadArtifact $UploadArtifact } catch { - Stop-NovaOperation -Message "Package upload failed for $( $UploadArtifact.PackagePath ) -> $( $UploadArtifact.UploadUrl ). $( $_.Exception.Message )" -ErrorId 'Nova.Dependency.PackageUploadRequestFailed' -Category ConnectionError -TargetObject $UploadArtifact.UploadUrl + Stop-NovaOperation -Message "Package upload failed for $( $UploadArtifact.PackagePath ) -> $( $UploadArtifact.UploadUrl ). Check the upload URL, authentication token, and network access, then try again. Details: $( $_.Exception.Message )" -ErrorId 'Nova.Dependency.PackageUploadRequestFailed' -Category ConnectionError -TargetObject $UploadArtifact.UploadUrl } return [pscustomobject]@{ diff --git a/src/private/package/InvokeNovaPackageUploadWorkflow.ps1 b/src/private/package/InvokeNovaPackageUploadWorkflow.ps1 index 079d8a46..4e3ef262 100644 --- a/src/private/package/InvokeNovaPackageUploadWorkflow.ps1 +++ b/src/private/package/InvokeNovaPackageUploadWorkflow.ps1 @@ -5,14 +5,60 @@ function Invoke-NovaPackageUploadWorkflow { [object[]]$UploadArtifactList = @() ) - $resolvedUploadArtifactList = @($UploadArtifactList) - if ($resolvedUploadArtifactList.Count -eq 0 -and @($WorkflowContext.UploadArtifactList).Count -eq 0) { + $resolvedUploadArtifactList = @(Get-NovaResolvedPackageUploadArtifactList -WorkflowContext $WorkflowContext -UploadArtifactList $UploadArtifactList) + if ($resolvedUploadArtifactList.Count -eq 0) { return @() } - return @( - $resolvedUploadArtifactList | ForEach-Object { - Invoke-NovaPackageArtifactUpload -UploadArtifact $_ + $uploadResult = @() + $artifactCount = $resolvedUploadArtifactList.Count + try { + for ($index = 0; $index -lt $artifactCount; $index++) { + $uploadArtifact = $resolvedUploadArtifactList[$index] + Write-NovaPackageUploadProgress -UploadArtifact $uploadArtifact -CurrentIndex ($index + 1) -TotalCount $artifactCount + $uploadResult += Invoke-NovaPackageArtifactUpload -UploadArtifact $uploadArtifact + } + } finally { + Complete-NovaPackageUploadProgress } + + return $uploadResult +} + +function Get-NovaResolvedPackageUploadArtifactList { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [object[]]$UploadArtifactList = @() + ) + + if (@($UploadArtifactList).Count -gt 0) { + return @($UploadArtifactList) + } + + return @($WorkflowContext.UploadArtifactList) +} + +function Write-NovaPackageUploadProgress { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$UploadArtifact, + [Parameter(Mandatory)][int]$CurrentIndex, + [Parameter(Mandatory)][int]$TotalCount ) + + $percentComplete = [int](($CurrentIndex - 1) * 100 / $TotalCount) + $status = "Uploading $( $UploadArtifact.PackageFileName ) ($CurrentIndex of $TotalCount)" + if ([string]::IsNullOrWhiteSpace([string]$UploadArtifact.PackageFileName)) { + $status = "Uploading artifact $CurrentIndex of $TotalCount" + } + + Write-Progress -Activity 'Uploading package artifacts' -Status $status -PercentComplete $percentComplete +} + +function Complete-NovaPackageUploadProgress { + [CmdletBinding()] + param() + + Write-Progress -Activity 'Uploading package artifacts' -Completed } diff --git a/src/private/package/WriteNovaPackageUploadResultOutput.ps1 b/src/private/package/WriteNovaPackageUploadResultOutput.ps1 new file mode 100644 index 00000000..4be1b487 --- /dev/null +++ b/src/private/package/WriteNovaPackageUploadResultOutput.ps1 @@ -0,0 +1,50 @@ +function Write-NovaPackageUploadResultOutput { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object[]]$Result + ) + + $resolvedResult = @($Result) + if ($resolvedResult.Count -eq 0) { + return + } + + Write-Host (Get-NovaPackageUploadResultSummaryMessage -Result $resolvedResult) + + $nextStepMessage = Get-NovaPackageUploadResultNextStepMessage -Result $resolvedResult + if ($null -ne $nextStepMessage) { + Write-Host $nextStepMessage + } +} + +function Get-NovaPackageUploadResultSummaryMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object[]]$Result + ) + + $artifactCount = @($Result).Count + if ($artifactCount -eq 1) { + return 'Uploaded 1 package artifact.' + } + + return "Uploaded $artifactCount package artifacts." +} + +function Get-NovaPackageUploadResultNextStepMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object[]]$Result + ) + + $resolvedResult = @($Result) + if ($resolvedResult.Count -eq 0) { + return $null + } + + if ($resolvedResult.Count -eq 1 -and -not [string]::IsNullOrWhiteSpace($resolvedResult[0].UploadUrl)) { + return "Next step: verify the uploaded artifact at $( $resolvedResult[0].UploadUrl )." + } + + return 'Next step: verify the uploaded artifacts at the target repository.' +} diff --git a/src/private/package/WriteNovaPackageUploadWorkflowContext.ps1 b/src/private/package/WriteNovaPackageUploadWorkflowContext.ps1 new file mode 100644 index 00000000..e441f8b8 --- /dev/null +++ b/src/private/package/WriteNovaPackageUploadWorkflowContext.ps1 @@ -0,0 +1,33 @@ +function Write-NovaPackageUploadWorkflowContext { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + $message = Get-NovaPackageUploadWorkflowContextMessage -WorkflowContext $WorkflowContext + if ($null -ne $message) { + Write-Host $message + } + + if (-not [string]::IsNullOrWhiteSpace($WorkflowContext.Target)) { + Write-Verbose "Target: $( $WorkflowContext.Target )" + } +} + +function Get-NovaPackageUploadWorkflowContextMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + $artifactCount = @($WorkflowContext.UploadArtifactList).Count + if ($artifactCount -eq 0) { + return $null + } + + if ($artifactCount -eq 1) { + return "$( $WorkflowContext.Operation )." + } + + return "Ready to upload $artifactCount package artifacts." +} diff --git a/src/public/DeployNovaPackage.ps1 b/src/public/DeployNovaPackage.ps1 index f81e6771..10af1ec4 100644 --- a/src/public/DeployNovaPackage.ps1 +++ b/src/public/DeployNovaPackage.ps1 @@ -16,12 +16,17 @@ function Deploy-NovaPackage { $uploadOption = New-NovaPackageUploadOption -BoundParameters $PSBoundParameters $workflowContext = Get-NovaPackageUploadWorkflowContext -BoundParameters $PSBoundParameters -ProjectInfo $projectInfo -UploadOption $uploadOption + Write-NovaPackageUploadWorkflowContext -WorkflowContext $workflowContext + $shouldRun = $PSCmdlet.ShouldProcess($workflowContext.Target, $workflowContext.Operation) if (-not $shouldRun) { return @() } - return @(Invoke-NovaPackageUploadWorkflow -WorkflowContext $workflowContext -UploadArtifactList $workflowContext.UploadArtifactList) + $result = @(Invoke-NovaPackageUploadWorkflow -WorkflowContext $workflowContext -UploadArtifactList $workflowContext.UploadArtifactList) + Write-NovaPackageUploadResultOutput -Result $result + + return $result } } diff --git a/tests/ArchitectureGuardrails.Tests.ps1 b/tests/ArchitectureGuardrails.Tests.ps1 index a8b3eec0..dc07e3df 100644 --- a/tests/ArchitectureGuardrails.Tests.ps1 +++ b/tests/ArchitectureGuardrails.Tests.ps1 @@ -82,7 +82,7 @@ Describe 'Architecture guardrails' { It 'public command files use only their approved Nova helper surface' { $testCases = @( - [pscustomobject]@{Path = 'src/public/DeployNovaPackage.ps1'; ExpectedHelpers = @('Get-NovaPackageUploadWorkflowContext', 'Get-NovaProjectInfo', 'Invoke-NovaPackageUploadWorkflow', 'New-NovaPackageUploadDynamicParameterDictionary', 'New-NovaPackageUploadOption')} + [pscustomobject]@{Path = 'src/public/DeployNovaPackage.ps1'; ExpectedHelpers = @('Get-NovaPackageUploadWorkflowContext', 'Get-NovaProjectInfo', 'Invoke-NovaPackageUploadWorkflow', 'New-NovaPackageUploadDynamicParameterDictionary', 'New-NovaPackageUploadOption', 'Write-NovaPackageUploadResultOutput', 'Write-NovaPackageUploadWorkflowContext')} [pscustomobject]@{Path = 'src/public/GetNovaProjectInfo.ps1'; ExpectedHelpers = @('Format-NovaCliVersionString', 'Get-NovaCliInstalledVersion', 'Get-NovaProjectInfoContext', 'Get-NovaProjectInfoResult')} [pscustomobject]@{Path = 'src/public/GetNovaUpdateNotificationPreference.ps1'; ExpectedHelpers = @('Get-NovaUpdateNotificationPreferenceStatus')} [pscustomobject]@{Path = 'src/public/InitializeNovaModule.ps1'; ExpectedHelpers = @('Get-NovaModuleInitializationWorkflowContext', 'Invoke-NovaModuleInitializationWorkflow')} diff --git a/tests/private/package/InvokeNovaPackageArtifactUpload.Tests.ps1 b/tests/private/package/InvokeNovaPackageArtifactUpload.Tests.ps1 index 9a72b05d..7f4974ce 100644 --- a/tests/private/package/InvokeNovaPackageArtifactUpload.Tests.ps1 +++ b/tests/private/package/InvokeNovaPackageArtifactUpload.Tests.ps1 @@ -17,7 +17,7 @@ Describe 'Invoke-NovaPackageArtifactUpload' { It 'throws when the package file does not exist' { Remove-Item -LiteralPath $script:file -Force - {Invoke-NovaPackageArtifactUpload -UploadArtifact $script:artifact} | Should -Throw '*Package file not found*' + {Invoke-NovaPackageArtifactUpload -UploadArtifact $script:artifact} | Should -Throw '*Run New-NovaModulePackage first or provide a valid -PackagePath*' } It 'returns a result object including the status code on success' { @@ -28,6 +28,6 @@ Describe 'Invoke-NovaPackageArtifactUpload' { It 'wraps upload errors into Stop-NovaOperation' { Mock Invoke-NovaPackageUploadRequest {throw 'boom'} - {Invoke-NovaPackageArtifactUpload -UploadArtifact $script:artifact} | Should -Throw '*Package upload failed*' + {Invoke-NovaPackageArtifactUpload -UploadArtifact $script:artifact} | Should -Throw '*Check the upload URL, authentication token, and network access, then try again*' } } diff --git a/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 b/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 index 916efa5e..af180677 100644 --- a/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 +++ b/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 @@ -5,17 +5,32 @@ BeforeAll { } Describe 'Invoke-NovaPackageUploadWorkflow' { + BeforeEach { + Mock Write-Progress {} + } + It 'returns empty when neither supplied list nor context list has artifacts' { $ctx = [pscustomobject]@{UploadArtifactList=@()} $r = Invoke-NovaPackageUploadWorkflow -WorkflowContext $ctx @($r).Count | Should -Be 0 + Should -Invoke Write-Progress -Times 0 } - It 'uploads each supplied artifact and returns the responses' { + It 'uploads each supplied artifact, reports progress, and returns the responses' { Mock Invoke-NovaPackageArtifactUpload {[pscustomobject]@{StatusCode=200; Artifact=$UploadArtifact}} - $ctx = [pscustomobject]@{UploadArtifactList=@([pscustomobject]@{Name='a'})} - $r = @(Invoke-NovaPackageUploadWorkflow -WorkflowContext $ctx -UploadArtifactList @([pscustomobject]@{Name='a'},[pscustomobject]@{Name='b'})) + $ctx = [pscustomobject]@{UploadArtifactList=@([pscustomobject]@{PackageFileName='a.nupkg'})} + $r = @(Invoke-NovaPackageUploadWorkflow -WorkflowContext $ctx -UploadArtifactList @([pscustomobject]@{PackageFileName='a.nupkg'},[pscustomobject]@{PackageFileName='b.nupkg'})) $r.Count | Should -Be 2 Should -Invoke Invoke-NovaPackageArtifactUpload -Times 2 + Should -Invoke Write-Progress -Times 2 -ParameterFilter {-not $Completed} + Should -Invoke Write-Progress -Times 1 -ParameterFilter {$Completed} + } + + It 'falls back to the workflow context artifact list when no explicit upload list is supplied' { + Mock Invoke-NovaPackageArtifactUpload {[pscustomobject]@{StatusCode=200; Artifact=$UploadArtifact}} + $ctx = [pscustomobject]@{UploadArtifactList=@([pscustomobject]@{PackageFileName='a.nupkg'})} + $r = @(Invoke-NovaPackageUploadWorkflow -WorkflowContext $ctx) + $r.Count | Should -Be 1 + Should -Invoke Invoke-NovaPackageArtifactUpload -Times 1 } } diff --git a/tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 b/tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 new file mode 100644 index 00000000..4ae965d3 --- /dev/null +++ b/tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 @@ -0,0 +1,31 @@ +BeforeAll { + $projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) + . (Join-Path $projectRoot 'src/private/package/WriteNovaPackageUploadResultOutput.ps1') +} + +Describe 'Write-NovaPackageUploadResultOutput' { + BeforeEach { + Mock Write-Host {} + } + + It 'writes a single-artifact summary and URL-specific next step' { + $result = @([pscustomobject]@{UploadUrl = 'https://packages.example/raw/NovaModuleTools.1.0.0.nupkg'}) + + Write-NovaPackageUploadResultOutput -Result $result + + Should -Invoke Write-Host -Times 1 -ParameterFilter {$Object -eq 'Uploaded 1 package artifact.'} + Should -Invoke Write-Host -Times 1 -ParameterFilter {$Object -eq 'Next step: verify the uploaded artifact at https://packages.example/raw/NovaModuleTools.1.0.0.nupkg.'} + } + + It 'writes a multi-artifact summary and repository-level next step' { + $result = @( + [pscustomobject]@{UploadUrl = 'https://packages.example/raw/a.nupkg'} + [pscustomobject]@{UploadUrl = 'https://packages.example/raw/b.nupkg'} + ) + + Write-NovaPackageUploadResultOutput -Result $result + + Should -Invoke Write-Host -Times 1 -ParameterFilter {$Object -eq 'Uploaded 2 package artifacts.'} + Should -Invoke Write-Host -Times 1 -ParameterFilter {$Object -eq 'Next step: verify the uploaded artifacts at the target repository.'} + } +} diff --git a/tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 b/tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 new file mode 100644 index 00000000..ea0926f9 --- /dev/null +++ b/tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 @@ -0,0 +1,37 @@ +BeforeAll { + $projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) + . (Join-Path $projectRoot 'src/private/package/WriteNovaPackageUploadWorkflowContext.ps1') +} + +Describe 'Write-NovaPackageUploadWorkflowContext' { + BeforeEach { + Mock Write-Host {} + Mock Write-Verbose {} + } + + It 'writes a ready message and verbose target for multiple artifacts' { + $workflowContext = [pscustomobject]@{ + UploadArtifactList = @([pscustomobject]@{}, [pscustomobject]@{}) + Target = 'https://packages.example/raw/' + Operation = 'Upload 2 package artifacts' + } + + Write-NovaPackageUploadWorkflowContext -WorkflowContext $workflowContext + + Should -Invoke Write-Host -Times 1 -ParameterFilter {$Object -eq 'Ready to upload 2 package artifacts.'} + Should -Invoke Write-Verbose -Times 1 -ParameterFilter {$Message -eq 'Target: https://packages.example/raw/'} + } + + It 'writes the operation message for a single artifact' { + $workflowContext = [pscustomobject]@{ + UploadArtifactList = @([pscustomobject]@{}) + Target = '' + Operation = 'Upload NuGet package artifact NovaModuleTools.1.0.0.nupkg' + } + + Write-NovaPackageUploadWorkflowContext -WorkflowContext $workflowContext + + Should -Invoke Write-Host -Times 1 -ParameterFilter {$Object -eq 'Upload NuGet package artifact NovaModuleTools.1.0.0.nupkg.'} + Should -Invoke Write-Verbose -Times 0 + } +} diff --git a/tests/public/DeployNovaPackage.TestSupport.ps1 b/tests/public/DeployNovaPackage.TestSupport.ps1 index 66a2370d..b9da4bde 100644 --- a/tests/public/DeployNovaPackage.TestSupport.ps1 +++ b/tests/public/DeployNovaPackage.TestSupport.ps1 @@ -4,7 +4,9 @@ function New-NovaPackageUploadOption {param($BoundParameters) return [pscustomob function Get-NovaPackageUploadWorkflowContext {param($BoundParameters, $ProjectInfo, $UploadOption) return [pscustomobject]@{Target='https://x/repo'; Operation='Upload'; UploadArtifactList=@([pscustomobject]@{Name='a.nupkg'})} } +function Write-NovaPackageUploadWorkflowContext {param($WorkflowContext) $script:wroteContext = $true} function Invoke-NovaPackageUploadWorkflow {param($WorkflowContext, $UploadArtifactList) $script:invoked = $true return @([pscustomobject]@{StatusCode=201}) } +function Write-NovaPackageUploadResultOutput {param($Result) $script:wroteResult = $true} diff --git a/tests/public/DeployNovaPackage.Tests.ps1 b/tests/public/DeployNovaPackage.Tests.ps1 index 3b03e437..9ab9bccc 100644 --- a/tests/public/DeployNovaPackage.Tests.ps1 +++ b/tests/public/DeployNovaPackage.Tests.ps1 @@ -6,18 +6,26 @@ BeforeAll { } Describe 'Deploy-NovaPackage' { - BeforeEach {$script:invoked = $false} + BeforeEach { + $script:invoked = $false + $script:wroteContext = $false + $script:wroteResult = $false + } - It 'invokes the upload workflow and returns its results' { + It 'writes context, invokes the upload workflow, and returns its results' { $result = Deploy-NovaPackage -PackagePath '/o/a.nupkg' -Url 'https://x' -Repository 'Nexus' + $script:wroteContext | Should -BeTrue $script:invoked | Should -BeTrue + $script:wroteResult | Should -BeTrue @($result).Count | Should -Be 1 $result[0].StatusCode | Should -Be 201 } - It 'returns an empty array and does not invoke the workflow when -WhatIf is set' { + It 'writes context and returns an empty array without invoking the workflow when -WhatIf is set' { $result = Deploy-NovaPackage -PackagePath '/o/a.nupkg' -WhatIf + $script:wroteContext | Should -BeTrue $script:invoked | Should -BeFalse + $script:wroteResult | Should -BeFalse @($result).Count | Should -Be 0 } } From ecf9dac7c13af88e3e9e72b0e225b2243a9cc63c Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 11:51:14 +0200 Subject: [PATCH 02/16] fix(#215): close raw upload coverage gaps --- .../package/WriteNovaPackageUploadResultOutput.ps1 | 2 +- .../InvokeNovaPackageUploadWorkflow.Tests.ps1 | 13 +++++++++++++ .../WriteNovaPackageUploadResultOutput.Tests.ps1 | 6 ++++++ .../WriteNovaPackageUploadWorkflowContext.Tests.ps1 | 13 +++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/private/package/WriteNovaPackageUploadResultOutput.ps1 b/src/private/package/WriteNovaPackageUploadResultOutput.ps1 index 4be1b487..1a422b9f 100644 --- a/src/private/package/WriteNovaPackageUploadResultOutput.ps1 +++ b/src/private/package/WriteNovaPackageUploadResultOutput.ps1 @@ -34,7 +34,7 @@ function Get-NovaPackageUploadResultSummaryMessage { function Get-NovaPackageUploadResultNextStepMessage { [CmdletBinding()] param( - [Parameter(Mandatory)][object[]]$Result + [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Result ) $resolvedResult = @($Result) diff --git a/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 b/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 index af180677..e15e66a1 100644 --- a/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 +++ b/tests/private/package/InvokeNovaPackageUploadWorkflow.Tests.ps1 @@ -26,6 +26,19 @@ Describe 'Invoke-NovaPackageUploadWorkflow' { Should -Invoke Write-Progress -Times 1 -ParameterFilter {$Completed} } + It 'uses a generic progress status when the artifact file name is blank' { + Mock Invoke-NovaPackageArtifactUpload {[pscustomobject]@{StatusCode=200; Artifact=$UploadArtifact}} + $ctx = [pscustomobject]@{UploadArtifactList=@()} + + $null = Invoke-NovaPackageUploadWorkflow -WorkflowContext $ctx -UploadArtifactList @([pscustomobject]@{PackageFileName=''}) + + Should -Invoke Write-Progress -Times 1 -ParameterFilter { + (-not $Completed) -and + $Status -eq 'Uploading artifact 1 of 1' + } + Should -Invoke Write-Progress -Times 1 -ParameterFilter {$Completed} + } + It 'falls back to the workflow context artifact list when no explicit upload list is supplied' { Mock Invoke-NovaPackageArtifactUpload {[pscustomobject]@{StatusCode=200; Artifact=$UploadArtifact}} $ctx = [pscustomobject]@{UploadArtifactList=@([pscustomobject]@{PackageFileName='a.nupkg'})} diff --git a/tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 b/tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 index 4ae965d3..671831c0 100644 --- a/tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 +++ b/tests/private/package/WriteNovaPackageUploadResultOutput.Tests.ps1 @@ -8,6 +8,12 @@ Describe 'Write-NovaPackageUploadResultOutput' { Mock Write-Host {} } + It 'returns no repository next-step message for an empty result list' { + $message = Get-NovaPackageUploadResultNextStepMessage -Result @() + + $message | Should -BeNullOrEmpty + } + It 'writes a single-artifact summary and URL-specific next step' { $result = @([pscustomobject]@{UploadUrl = 'https://packages.example/raw/NovaModuleTools.1.0.0.nupkg'}) diff --git a/tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 b/tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 index ea0926f9..9e3f51f5 100644 --- a/tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 +++ b/tests/private/package/WriteNovaPackageUploadWorkflowContext.Tests.ps1 @@ -9,6 +9,19 @@ Describe 'Write-NovaPackageUploadWorkflowContext' { Mock Write-Verbose {} } + It 'writes nothing when no artifacts are resolved' { + $workflowContext = [pscustomobject]@{ + UploadArtifactList = @() + Target = '' + Operation = 'Upload package artifacts' + } + + Write-NovaPackageUploadWorkflowContext -WorkflowContext $workflowContext + + Should -Invoke Write-Host -Times 0 + Should -Invoke Write-Verbose -Times 0 + } + It 'writes a ready message and verbose target for multiple artifacts' { $workflowContext = [pscustomobject]@{ UploadArtifactList = @([pscustomobject]@{}, [pscustomobject]@{}) From 70aef5cff985896c32d8761096b677030dbbbda7 Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 12:32:48 +0200 Subject: [PATCH 03/16] #215 fix: nomalize command help related links --- .../instructions/platyps-help.instructions.md | 5 ++ CHANGELOG.md | 1 + RELEASE_NOTE.md | 2 + .../en-US/Deploy-NovaPackage.md | 4 +- .../en-US/Get-NovaProjectInfo.md | 6 +- .../Get-NovaUpdateNotificationPreference.md | 6 +- .../en-US/Initialize-NovaModule.md | 4 +- docs/NovaModuleTools/en-US/Install-NovaCli.md | 2 +- .../Invoke-NovaAgenticCopilotScaffold.md | 4 +- .../NovaModuleTools/en-US/Invoke-NovaBuild.md | 8 +-- .../en-US/Invoke-NovaRelease.md | 8 +-- .../en-US/New-NovaModulePackage.md | 4 +- .../en-US/Publish-NovaModule.md | 6 +- .../Set-NovaUpdateNotificationPreference.md | 4 +- docs/NovaModuleTools/en-US/Test-NovaBuild.md | 6 +- .../en-US/Update-NovaModuleTools.md | 6 +- .../en-US/Update-NovaModuleVersion.md | 6 +- .../instructions/platyps-help.instructions.md | 5 ++ tests/CommandHelpRelatedLinks.Tests.ps1 | 58 +++++++++++++++++++ 19 files changed, 108 insertions(+), 37 deletions(-) create mode 100644 tests/CommandHelpRelatedLinks.Tests.ps1 diff --git a/.github/instructions/platyps-help.instructions.md b/.github/instructions/platyps-help.instructions.md index dcb320ce..b2714daf 100644 --- a/.github/instructions/platyps-help.instructions.md +++ b/.github/instructions/platyps-help.instructions.md @@ -66,6 +66,9 @@ Import-MarkdownCommandHelp -Path ./docs/NovaModuleTools/en-US/.md | - The `external help file` field must always use the module name, not the command name: `NovaModuleTools-Help.xml`. The `Module Name` field must match the project name. When both fields use the module name, Nova build produces a single `-Help.xml` under `dist//en-US/`. If either field contains a command name instead, the build produces per-command XML files and the module manifest cannot find its help. - Keep the H1 title equal to the exact command name. - Preserve the standard PlatyPS section order with uppercase H2 headers: `SYNOPSIS`, `SYNTAX`, optional `ALIASES`, `DESCRIPTION`, `EXAMPLES`, `PARAMETERS`, `INPUTS`, `OUTPUTS`, `NOTES`, and `RELATED LINKS`. +- In `## RELATED LINKS`, use only a bulleted list of Markdown links. Do not use bare URLs or backticked command names as list items. +- Prefer relative links to sibling command-help files when the related topic has a matching file in `docs/NovaModuleTools/en-US/`, for example `[Invoke-NovaBuild](./Invoke-NovaBuild.md)`. +- Do not use GitHub blob URLs in shipped command help. If a relative command-help link is not suitable after PlatyPS validation, use the related topic's `novamoduletools.com` `HelpUri` instead. - Keep at least one example under `## EXAMPLES`. - Keep parameter sections as `### -ParameterName` blocks with the PlatyPS-generated YAML metadata code block. - Only hand-edit parameter metadata when PlatyPS cannot infer it correctly, especially `DefaultValue` and `SupportsWildcards`. @@ -82,6 +85,7 @@ Import-MarkdownCommandHelp -Path ./docs/NovaModuleTools/en-US/.md | - Use `Update-MarkdownCommandHelp` after command or parameter changes so syntax, aliases, and parameter metadata stay synchronized with the implementation. - Use `Test-MarkdownCommandHelp` as the quick structural gate before handoff; use `Import-MarkdownCommandHelp` diagnostics when you need more detail. - Use existing valid files under `docs/NovaModuleTools/en-US/` as the structural template before inventing a new layout. +- Validate any new `RELATED LINKS` target style through the real PlatyPS import/export path before applying it repo-wide. - Keep command help separate from contributor docs and project/site docs. ## Review expectations @@ -90,4 +94,5 @@ Import-MarkdownCommandHelp -Path ./docs/NovaModuleTools/en-US/.md | - Reviewers should flag help files where `external help file` contains a command name (e.g., `Get-Something-Help.xml`) instead of the module name (`NovaModuleTools-Help.xml`). This produces per-command XML files that the module manifest cannot locate at runtime. - Reviewers should flag help files that would fail `Test-MarkdownCommandHelp` or produce diagnostics/errors when imported with `Import-MarkdownCommandHelp`. - Reviewers should flag any new public entry point that does not add its matching command-help file in the same change. +- Reviewers should flag `RELATED LINKS` entries that use GitHub blob URLs, plain URLs, or bare/backticked command names instead of Markdown links. - Treat build errors from `Import-MarkdownCommandHelp` as a sign that the file is not valid PlatyPS help yet. diff --git a/CHANGELOG.md b/CHANGELOG.md index 75101485..1bb90375 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Fixed - `Test-NovaBuild` now expands configured Pester coverage globs into concrete source files before invoking Pester, so nested helpers remain measurable in `artifacts/coverage.xml` while repository and scaffolded `project.json` defaults can stay on the simpler `src/private/**/*.ps1` entry. +- PowerShell command help `RELATED LINKS` now use valid PlatyPS Markdown links and point to shipped help topics instead of GitHub blob pages. ### Security diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 4e20f222..811fba62 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -21,6 +21,8 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang ### Fixed +- PowerShell command help `RELATED LINKS` now use valid help-topic links instead of GitHub blob pages. + ### Security ## [3.0.1] - 2026-05-18 diff --git a/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md b/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md index d86b97eb..7494f2a8 100644 --- a/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md +++ b/docs/NovaModuleTools/en-US/Deploy-NovaPackage.md @@ -382,5 +382,5 @@ If no upload target can be resolved, `Deploy-NovaPackage` fails fast with a clea ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/New-NovaModulePackage.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Publish-NovaModule.md +- [New-NovaModulePackage](./New-NovaModulePackage.md) +- [Publish-NovaModule](./Publish-NovaModule.md) diff --git a/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md b/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md index 67168fca..e32f931d 100644 --- a/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md +++ b/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md @@ -189,6 +189,6 @@ This command throws a clear error when `project.json` is missing or empty. ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Test-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Test-NovaBuild](./Test-NovaBuild.md) +- [Update-NovaModuleVersion](./Update-NovaModuleVersion.md) diff --git a/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md b/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md index 047c8da1..f1c5941a 100644 --- a/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md +++ b/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md @@ -81,6 +81,6 @@ When prerelease notifications are enabled again, `Update-NovaModuleTool` / `Upda ## RELATED LINKS -- `Invoke-NovaBuild` -- `Set-NovaUpdateNotificationPreference` -- `Update-NovaModuleTool` +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Set-NovaUpdateNotificationPreference](./Set-NovaUpdateNotificationPreference.md) +- [Update-NovaModuleTool](./Update-NovaModuleTools.md) diff --git a/docs/NovaModuleTools/en-US/Initialize-NovaModule.md b/docs/NovaModuleTools/en-US/Initialize-NovaModule.md index 88532b2c..2036cd44 100644 --- a/docs/NovaModuleTools/en-US/Initialize-NovaModule.md +++ b/docs/NovaModuleTools/en-US/Initialize-NovaModule.md @@ -152,5 +152,5 @@ Generated projects start with NovaModuleTools defaults for recursive discovery, ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) diff --git a/docs/NovaModuleTools/en-US/Install-NovaCli.md b/docs/NovaModuleTools/en-US/Install-NovaCli.md index 897354ec..03dc02de 100644 --- a/docs/NovaModuleTools/en-US/Install-NovaCli.md +++ b/docs/NovaModuleTools/en-US/Install-NovaCli.md @@ -172,4 +172,4 @@ After running `Install-NovaCli`, add the destination directory to your shell `PA ## RELATED LINKS -- `Publish-NovaModule` +- [Publish-NovaModule](./Publish-NovaModule.md) diff --git a/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md b/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md index 649c3690..3d2bbc90 100644 --- a/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md +++ b/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md @@ -203,5 +203,5 @@ This workflow does not persist `ShortName` to `project.json` and does not infer ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Initialize-NovaModule.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md +- [Initialize-NovaModule](./Initialize-NovaModule.md) +- [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) diff --git a/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md b/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md index f6c8f375..073ad7ac 100644 --- a/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md +++ b/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md @@ -174,7 +174,7 @@ Files under `src/public` are expected to contain exactly one top-level function ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Test-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Publish-NovaModule.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md +- [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) +- [Test-NovaBuild](./Test-NovaBuild.md) +- [Publish-NovaModule](./Publish-NovaModule.md) +- [Update-NovaModuleTool](./Update-NovaModuleTools.md) diff --git a/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md b/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md index 9b0683f9..c8e3efed 100644 --- a/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md +++ b/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md @@ -325,7 +325,7 @@ and `-Confirm` support. ## RELATED LINKS -- `Invoke-NovaBuild` -- `Test-NovaBuild` -- `Update-NovaModuleVersion` -- `Publish-NovaModule` +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Test-NovaBuild](./Test-NovaBuild.md) +- [Update-NovaModuleVersion](./Update-NovaModuleVersion.md) +- [Publish-NovaModule](./Publish-NovaModule.md) diff --git a/docs/NovaModuleTools/en-US/New-NovaModulePackage.md b/docs/NovaModuleTools/en-US/New-NovaModulePackage.md index 751e72e7..ae124936 100644 --- a/docs/NovaModuleTools/en-US/New-NovaModulePackage.md +++ b/docs/NovaModuleTools/en-US/New-NovaModulePackage.md @@ -277,5 +277,5 @@ Files under `src/public` are expected to contain exactly one top-level function ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Test-NovaBuild.md +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Test-NovaBuild](./Test-NovaBuild.md) diff --git a/docs/NovaModuleTools/en-US/Publish-NovaModule.md b/docs/NovaModuleTools/en-US/Publish-NovaModule.md index 886cca78..a1ae7834 100644 --- a/docs/NovaModuleTools/en-US/Publish-NovaModule.md +++ b/docs/NovaModuleTools/en-US/Publish-NovaModule.md @@ -302,6 +302,6 @@ When `-ContinuousIntegration` is used, Nova restores the built `dist/` module af ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Test-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Test-NovaBuild](./Test-NovaBuild.md) +- [Invoke-NovaRelease](./Invoke-NovaRelease.md) diff --git a/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md b/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md index 07a1d0f9..6c09762f 100644 --- a/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md +++ b/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md @@ -197,5 +197,5 @@ Use this command together with `Get-NovaUpdateNotificationPreference` when you w ## RELATED LINKS -- `Get-NovaUpdateNotificationPreference` -- `Update-NovaModuleTool` +- [Get-NovaUpdateNotificationPreference](./Get-NovaUpdateNotificationPreference.md) +- [Update-NovaModuleTool](./Update-NovaModuleTools.md) diff --git a/docs/NovaModuleTools/en-US/Test-NovaBuild.md b/docs/NovaModuleTools/en-US/Test-NovaBuild.md index 0c3ddd20..26a5b9db 100644 --- a/docs/NovaModuleTools/en-US/Test-NovaBuild.md +++ b/docs/NovaModuleTools/en-US/Test-NovaBuild.md @@ -260,6 +260,6 @@ If `project.json` configures `Pester.CodeCoverage.CoveragePercentTarget`, `Test- ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Publish-NovaModule.md +- [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Publish-NovaModule](./Publish-NovaModule.md) diff --git a/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md b/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md index de3b66d4..2bc054a2 100644 --- a/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md +++ b/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md @@ -149,6 +149,6 @@ Use `Get-NovaUpdateNotificationPreference` and `Set-NovaUpdateNotificationPrefer ## RELATED LINKS -- `Get-NovaUpdateNotificationPreference` -- `Set-NovaUpdateNotificationPreference` -- `Invoke-NovaBuild` +- [Get-NovaUpdateNotificationPreference](./Get-NovaUpdateNotificationPreference.md) +- [Set-NovaUpdateNotificationPreference](./Set-NovaUpdateNotificationPreference.md) +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) diff --git a/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md b/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md index 7c87e57d..37ad8594 100644 --- a/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md +++ b/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md @@ -303,6 +303,6 @@ When Git-based inference is unavailable, `Update-NovaModuleVersion` now requires ## RELATED LINKS -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Get-NovaProjectInfo.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md -- https://github.com/stiwicourage/NovaModuleTools/blob/main/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md +- [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Invoke-NovaRelease](./Invoke-NovaRelease.md) diff --git a/src/resources/agentic-copilot/.github/instructions/platyps-help.instructions.md b/src/resources/agentic-copilot/.github/instructions/platyps-help.instructions.md index 1b97779d..15fab571 100644 --- a/src/resources/agentic-copilot/.github/instructions/platyps-help.instructions.md +++ b/src/resources/agentic-copilot/.github/instructions/platyps-help.instructions.md @@ -66,6 +66,9 @@ Import-MarkdownCommandHelp -Path ./docs/{{ProjectName}}/en-US/.md | - The `external help file` field must always use the module name, not the command name: `{{ProjectName}}-Help.xml`. The `Module Name` field must match the project name. When both fields use the module name, Nova build produces a single `-Help.xml` under `dist//en-US/`. If either field contains a command name instead, the build produces per-command XML files and the module manifest cannot find its help. - Keep the H1 title equal to the exact command name. - Preserve the standard PlatyPS section order with uppercase H2 headers: `SYNOPSIS`, `SYNTAX`, optional `ALIASES`, `DESCRIPTION`, `EXAMPLES`, `PARAMETERS`, `INPUTS`, `OUTPUTS`, `NOTES`, and `RELATED LINKS`. +- In `## RELATED LINKS`, use only a bulleted list of Markdown links. Do not use bare URLs or backticked command names as list items. +- Prefer relative links to sibling command-help files when the related topic has a matching file in `docs/{{ProjectName}}/en-US/`, for example `[Invoke-NovaBuild](./Invoke-NovaBuild.md)`. +- Do not use GitHub blob URLs in shipped command help. If a relative command-help link is not suitable after PlatyPS validation, use the related topic's `novamoduletools.com` `HelpUri` instead. - Keep at least one example under `## EXAMPLES`. - Keep parameter sections as `### -ParameterName` blocks with the PlatyPS-generated YAML metadata code block. - Only hand-edit parameter metadata when PlatyPS cannot infer it correctly, especially `DefaultValue` and `SupportsWildcards`. @@ -82,6 +85,7 @@ Import-MarkdownCommandHelp -Path ./docs/{{ProjectName}}/en-US/.md | - Use `Update-MarkdownCommandHelp` after command or parameter changes so syntax, aliases, and parameter metadata stay synchronized with the implementation. - Use `Test-MarkdownCommandHelp` as the quick structural gate before handoff; use `Import-MarkdownCommandHelp` diagnostics when you need more detail. - Use existing valid files under `docs/{{ProjectName}}/en-US/` as the structural template before inventing a new layout. +- Validate any new `RELATED LINKS` target style through the real PlatyPS import/export path before applying it repo-wide. - Keep command help separate from contributor docs and project/site docs. ## Review expectations @@ -90,4 +94,5 @@ Import-MarkdownCommandHelp -Path ./docs/{{ProjectName}}/en-US/.md | - Reviewers should flag help files where `external help file` contains a command name (e.g., `Get-Something-Help.xml`) instead of the module name (`{{ProjectName}}-Help.xml`). This produces per-command XML files that the module manifest cannot locate at runtime. - Reviewers should flag help files that would fail `Test-MarkdownCommandHelp` or produce diagnostics/errors when imported with `Import-MarkdownCommandHelp`. - Reviewers should flag any new public entry point that does not add its matching command-help file in the same change. +- Reviewers should flag `RELATED LINKS` entries that use GitHub blob URLs, plain URLs, or bare/backticked command names instead of Markdown links. - Treat build errors from `Import-MarkdownCommandHelp` as a sign that the file is not valid PlatyPS help yet. diff --git a/tests/CommandHelpRelatedLinks.Tests.ps1 b/tests/CommandHelpRelatedLinks.Tests.ps1 new file mode 100644 index 00000000..405a8daf --- /dev/null +++ b/tests/CommandHelpRelatedLinks.Tests.ps1 @@ -0,0 +1,58 @@ +BeforeAll { + $projectRoot = Split-Path -Parent $PSScriptRoot + $script:docsDirectory = Join-Path $projectRoot 'docs/NovaModuleTools/en-US' + $script:helpFiles = Get-ChildItem -LiteralPath $script:docsDirectory -Filter '*.md' -File | + Where-Object Name -ne 'NovaModuleTools.md' + + $script:relatedLinks = foreach ($file in $script:helpFiles) { + $content = Get-Content -LiteralPath $file.FullName -Raw + $sectionMatch = [regex]::Match($content, '(?ms)^## RELATED LINKS\r?\n\r?\n(?.*)$') + if (-not $sectionMatch.Success) { + continue + } + + $linkLineList = @( + $sectionMatch.Groups['body'].Value -split '\r?\n' | + Where-Object { $_ -match '^- ' } + ) + + foreach ($line in $linkLineList) { + $lineMatch = [regex]::Match($line, '^- \[(?[^\]]+)\]\((?[^)]+)\)$') + [pscustomobject]@{ + FileName = $file.Name + Line = $line + Text = $lineMatch.Groups['text'].Value + Uri = $lineMatch.Groups['uri'].Value + IsMarkdownLink = $lineMatch.Success + } + } + } +} + +Describe 'command help related links' { + It 'uses Markdown link syntax for every related-links entry' { + foreach ($entry in $script:relatedLinks) { + $entry.Line | Should -Match '^- \[[^\]]+\]\([^)]+\)$' -Because $entry.FileName + } + } + + It 'does not use GitHub blob links in related-links entries' { + foreach ($entry in $script:relatedLinks) { + $entry.Line | Should -Not -Match 'github\.com/.*/blob/' -Because $entry.FileName + } + } + + It 'uses resolvable internal help links or Nova website links in related-links entries' { + foreach ($entry in $script:relatedLinks) { + $entry.IsMarkdownLink | Should -BeTrue -Because $entry.FileName + + if ($entry.Uri -match '^\./(?[^/]+\.md)$') { + $targetPath = Join-Path $script:docsDirectory $Matches['target'] + (Test-Path -LiteralPath $targetPath -PathType Leaf) | Should -BeTrue -Because "$($entry.FileName): $($entry.Uri)" + continue + } + + $entry.Uri | Should -Match '^https://www\.novamoduletools\.com/' -Because "$($entry.FileName): $($entry.Uri)" + } + } +} From 14d15bddff698e41e27238e90a2cdc8c7b0bd172 Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 13:59:58 +0200 Subject: [PATCH 04/16] #215 fix(#217): clarify update notification perference status --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../Get-NovaUpdateNotificationPreference.md | 18 +++++++++ ...NovaUpdateNotificationPreferenceStatus.ps1 | 38 ++++++++++++++++++- ...dateNotificationPreferenceStatus.Tests.ps1 | 20 ++++++++++ 5 files changed, 76 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38008509..2322c591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Deploy-NovaPackage` now shows a concise resolved-upload summary before execution, reports progress while multiple artifacts are uploading, and prints a short completion summary with a suggested verification step after successful raw uploads. - `Get-NovaProjectInfo` now fails with clearer recovery guidance when `-Path` does not exist, points to a file, or the target folder is missing `project.json`. +- `Get-NovaUpdateNotificationPreference -Verbose` now explains whether Nova is reading a stored preference or the built-in default, and the command help now points read-only PowerShell users to the matching `% nova notification` workflow. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 049257a5..56c28506 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -15,6 +15,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Deploy-NovaPackage` now shows clearer terminal feedback during raw package uploads, including a concise pre-flight summary, progress across multiple artifacts, and a short verification hint after success. - `Get-NovaProjectInfo` now explains how to recover when `-Path` is invalid or the target folder is not a Nova project root. +- `Get-NovaUpdateNotificationPreference -Verbose` now tells you whether Nova is using a stored update-notification preference or the built-in default, and the help now points to the matching `% nova notification` workflow. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md b/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md index f1c5941a..65b0d666 100644 --- a/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md +++ b/docs/NovaModuleTools/en-US/Get-NovaUpdateNotificationPreference.md @@ -32,6 +32,11 @@ The same stored preference is also used by `Update-NovaModuleTool` (alias: `Upda Stable self-updates remain available and do not require prerelease eligibility. +If no settings file exists yet, the command reports the default behavior: prerelease self-updates are enabled and +stable self-updates remain available. + +Use `% nova notification` when you want the CLI-oriented view of the same preference. + ## EXAMPLES ### EXAMPLE 1 @@ -44,6 +49,15 @@ Shows whether prerelease self-updates are currently enabled, whether stable self ### EXAMPLE 2 +```text +PS> Get-NovaUpdateNotificationPreference -Verbose +``` + +Shows the current status and writes a short explanation that tells you whether Nova is using a stored settings file +or the built-in default. + +### EXAMPLE 3 + ```text PS> Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications PS> Get-NovaUpdateNotificationPreference @@ -77,10 +91,14 @@ Use `Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications` to st Use `Set-NovaUpdateNotificationPreference -EnablePrereleaseNotifications` to allow prerelease self-updates again. +Use `-Verbose` when you want a short explanation of whether Nova is reading a stored preference or falling back to +the default. + When prerelease notifications are enabled again, `Update-NovaModuleTool` / `Update-NovaModuleTools` may again select a prerelease target. Prerelease self-updates still require explicit confirmation before the update proceeds, and that confirmation defaults to `No` so pressing Enter cancels the update. ## RELATED LINKS - [Invoke-NovaBuild](./Invoke-NovaBuild.md) - [Set-NovaUpdateNotificationPreference](./Set-NovaUpdateNotificationPreference.md) +- [Install-NovaCli](./Install-NovaCli.md) - [Update-NovaModuleTool](./Update-NovaModuleTools.md) diff --git a/src/private/update/GetNovaUpdateNotificationPreferenceStatus.ps1 b/src/private/update/GetNovaUpdateNotificationPreferenceStatus.ps1 index 7eae2909..f0115e7e 100644 --- a/src/private/update/GetNovaUpdateNotificationPreferenceStatus.ps1 +++ b/src/private/update/GetNovaUpdateNotificationPreferenceStatus.ps1 @@ -3,10 +3,44 @@ function Get-NovaUpdateNotificationPreferenceStatus { param() $preference = Read-NovaUpdateNotificationPreference + $settingsPath = Get-NovaUpdateSettingsFilePath + $settingsFileExists = Test-Path -LiteralPath $settingsPath -PathType Leaf - return [pscustomobject]@{ + $status = [pscustomobject]@{ PrereleaseNotificationsEnabled = $preference.PrereleaseNotificationsEnabled StableReleaseNotificationsEnabled = $true - SettingsPath = Get-NovaUpdateSettingsFilePath + SettingsPath = $settingsPath } + + Write-NovaUpdateNotificationPreferenceStatusVerbose -Status $status -SettingsFileExists:$settingsFileExists + return $status +} + +function Write-NovaUpdateNotificationPreferenceStatusVerbose { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Status, + [Parameter(Mandatory)][bool]$SettingsFileExists + ) + + $prereleaseState = Get-NovaUpdateNotificationPreferenceStateText -Enabled:$Status.PrereleaseNotificationsEnabled + if ($SettingsFileExists) { + Write-Verbose "Prerelease self-updates are $prereleaseState. Stable self-updates remain available. Settings file: $( $Status.SettingsPath )" + return + } + + Write-Verbose "No settings file was found at $( $Status.SettingsPath ). Using the default setting: prerelease self-updates are $prereleaseState. Stable self-updates remain available. Use Set-NovaUpdateNotificationPreference to store a different preference." +} + +function Get-NovaUpdateNotificationPreferenceStateText { + [CmdletBinding()] + param( + [Parameter(Mandatory)][bool]$Enabled + ) + + if ($Enabled) { + return 'enabled' + } + + return 'disabled' } diff --git a/tests/private/update/GetNovaUpdateNotificationPreferenceStatus.Tests.ps1 b/tests/private/update/GetNovaUpdateNotificationPreferenceStatus.Tests.ps1 index db553443..0e87b6c8 100644 --- a/tests/private/update/GetNovaUpdateNotificationPreferenceStatus.Tests.ps1 +++ b/tests/private/update/GetNovaUpdateNotificationPreferenceStatus.Tests.ps1 @@ -10,11 +10,31 @@ Describe 'Get-NovaUpdateNotificationPreferenceStatus' { It 'returns the stored preference along with the settings file path' { Mock Read-NovaUpdateNotificationPreference {return [pscustomobject]@{PrereleaseNotificationsEnabled = $false}} Mock Get-NovaUpdateSettingsFilePath {return '/some/path/settings.json'} + Mock Test-Path {return $true} + Mock Write-Verbose {} $status = Get-NovaUpdateNotificationPreferenceStatus $status.PrereleaseNotificationsEnabled | Should -BeFalse $status.StableReleaseNotificationsEnabled | Should -BeTrue $status.SettingsPath | Should -Be '/some/path/settings.json' + Assert-MockCalled Write-Verbose -Times 1 -ParameterFilter { + $Message -eq 'Prerelease self-updates are disabled. Stable self-updates remain available. Settings file: /some/path/settings.json' + } + } + + It 'explains when the default preference is being used because the settings file is missing' { + Mock Read-NovaUpdateNotificationPreference {return [pscustomobject]@{PrereleaseNotificationsEnabled = $true}} + Mock Get-NovaUpdateSettingsFilePath {return '/some/path/settings.json'} + Mock Test-Path {return $false} + Mock Write-Verbose {} + + $status = Get-NovaUpdateNotificationPreferenceStatus + + $status.PrereleaseNotificationsEnabled | Should -BeTrue + $status.SettingsPath | Should -Be '/some/path/settings.json' + Assert-MockCalled Write-Verbose -Times 1 -ParameterFilter { + $Message -eq 'No settings file was found at /some/path/settings.json. Using the default setting: prerelease self-updates are enabled. Stable self-updates remain available. Use Set-NovaUpdateNotificationPreference to store a different preference.' + } } } From 2c06074627594a06fcf628828c8224402a723285 Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 14:18:37 +0200 Subject: [PATCH 05/16] #215 feat: enhance scaffold feedback and error handling in Initialize-NovaModule --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../en-US/Initialize-NovaModule.md | 4 ++ .../scaffold/InitializeNovaModuleScaffold.ps1 | 14 ++-- ...InvokeNovaModuleInitializationWorkflow.ps1 | 71 +++++++++++++++++-- .../InitializeNovaModuleScaffold.Tests.ps1 | 12 ++++ ...NovaModuleInitializationWorkflow.Tests.ps1 | 32 ++++++++- 7 files changed, 122 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2322c591..88e3886c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Deploy-NovaPackage` now shows a concise resolved-upload summary before execution, reports progress while multiple artifacts are uploading, and prints a short completion summary with a suggested verification step after successful raw uploads. - `Get-NovaProjectInfo` now fails with clearer recovery guidance when `-Path` does not exist, points to a file, or the target folder is missing `project.json`. - `Get-NovaUpdateNotificationPreference -Verbose` now explains whether Nova is reading a stored preference or the built-in default, and the command help now points read-only PowerShell users to the matching `% nova notification` workflow. +- `Initialize-NovaModule` now shows scaffold progress, ends with the created project root plus the next suggested cmdlet, and fails with clearer guidance when the target project folder already exists. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 56c28506..8d6b3cee 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -16,6 +16,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Deploy-NovaPackage` now shows clearer terminal feedback during raw package uploads, including a concise pre-flight summary, progress across multiple artifacts, and a short verification hint after success. - `Get-NovaProjectInfo` now explains how to recover when `-Path` is invalid or the target folder is not a Nova project root. - `Get-NovaUpdateNotificationPreference -Verbose` now tells you whether Nova is using a stored update-notification preference or the built-in default, and the help now points to the matching `% nova notification` workflow. +- `Initialize-NovaModule` now shows scaffold progress, ends with the project root and the next suggested cmdlet, and gives clearer recovery guidance when the target project folder already exists. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Initialize-NovaModule.md b/docs/NovaModuleTools/en-US/Initialize-NovaModule.md index 2036cd44..7c396754 100644 --- a/docs/NovaModuleTools/en-US/Initialize-NovaModule.md +++ b/docs/NovaModuleTools/en-US/Initialize-NovaModule.md @@ -45,6 +45,8 @@ Use `-Example` when you want the scaffold to start from the packaged example pro This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` to preview the scaffold target after the interactive answers have been collected, without creating folders, writing `project.json`, or initializing Git. +During scaffold creation, Nova shows progress for the main setup phases and finishes with the created project root plus the next cmdlet to run. + ## EXAMPLES ### EXAMPLE 1 @@ -150,6 +152,8 @@ Generated projects start with NovaModuleTools defaults for recursive discovery, `-WhatIf` and `-Confirm` support. +Press `Ctrl+C` during the interactive prompt flow if you want to cancel before Nova creates the scaffold. + ## RELATED LINKS - [Invoke-NovaBuild](./Invoke-NovaBuild.md) diff --git a/src/private/scaffold/InitializeNovaModuleScaffold.ps1 b/src/private/scaffold/InitializeNovaModuleScaffold.ps1 index 6189fc70..3f8bbaac 100644 --- a/src/private/scaffold/InitializeNovaModuleScaffold.ps1 +++ b/src/private/scaffold/InitializeNovaModuleScaffold.ps1 @@ -6,12 +6,12 @@ function Initialize-NovaModuleScaffold { [switch]$Example ) - if (Test-Path $Paths.Project) { - Stop-NovaOperation -Message 'Project already exists, aborting' -ErrorId 'Nova.Workflow.ScaffoldProjectAlreadyExists' -Category ResourceExists -TargetObject $Paths.Project + if (Test-Path -LiteralPath $Paths.Project) { + Stop-NovaOperation -Message "Project folder already exists: $( $Paths.Project ). Choose a different project name or remove or move the existing folder before running Initialize-NovaModule again." -ErrorId 'Nova.Workflow.ScaffoldProjectAlreadyExists' -Category ResourceExists -TargetObject $Paths.Project } - Write-Message "`nStarted Module Scaffolding" -color Green - Write-Message 'Setting up Directories' + Write-Message 'Starting Nova module scaffold' -color Green + Write-Message 'Creating project directories' if ($Example) { Initialize-NovaExampleModuleScaffold -Paths $Paths @@ -21,7 +21,7 @@ function Initialize-NovaModuleScaffold { if ($Answer.EnableGit -eq 'Yes') { Update-NovaGitIgnore -ProjectRoot $Paths.Project -Confirm:$false - Write-Message 'Initialize Git Repo' + Write-Message 'Initializing Git repository' New-InitiateGitRepo -DirectoryPath $Paths.Project } } @@ -39,7 +39,7 @@ function Initialize-NovaDefaultModuleScaffold { } if ($Answer.EnablePester -eq 'Yes') { - Write-Message 'Include Pester Configs' + Write-Message 'Creating tests folder' New-Item -ItemType Directory -Path $Paths.Tests | Out-Null } } @@ -50,7 +50,7 @@ function Initialize-NovaExampleModuleScaffold { [Parameter(Mandatory)][pscustomobject]$Paths ) - Write-Message 'Copy example project template' + Write-Message 'Copying packaged example project' New-Item -ItemType Directory -Path $Paths.Project | Out-Null Copy-NovaExampleProjectTemplate -DestinationPath $Paths.Project } diff --git a/src/private/scaffold/InvokeNovaModuleInitializationWorkflow.ps1 b/src/private/scaffold/InvokeNovaModuleInitializationWorkflow.ps1 index f1c6bc9e..0b483c4c 100644 --- a/src/private/scaffold/InvokeNovaModuleInitializationWorkflow.ps1 +++ b/src/private/scaffold/InvokeNovaModuleInitializationWorkflow.ps1 @@ -4,11 +4,72 @@ function Invoke-NovaModuleInitializationWorkflow { [Parameter(Mandatory)][pscustomobject]$WorkflowContext ) - Initialize-NovaModuleScaffold -Answer $WorkflowContext.AnswerSet -Paths $WorkflowContext.Layout -Example:$WorkflowContext.Example - Write-NovaModuleProjectJson -Answer $WorkflowContext.AnswerSet -ProjectJsonFile $WorkflowContext.Layout.ProjectJsonFile -Example:$WorkflowContext.Example - if ($WorkflowContext.AnswerSet.EnableAgenticCopilot -eq 'Yes') { - Initialize-NovaModuleAgenticCopilotScaffold -Answer $WorkflowContext.AnswerSet -ProjectRoot $WorkflowContext.Layout.Project -Example:$WorkflowContext.Example + $progressActivity = 'Creating Nova module scaffold' + + try { + Invoke-NovaModuleInitializationStep -Activity $progressActivity -Status 'Creating scaffold files' -PercentComplete 25 -Action { + Initialize-NovaModuleScaffold -Answer $WorkflowContext.AnswerSet -Paths $WorkflowContext.Layout -Example:$WorkflowContext.Example + } + + Invoke-NovaModuleInitializationStep -Activity $progressActivity -Status 'Writing project.json' -PercentComplete 60 -Action { + Write-NovaModuleProjectJson -Answer $WorkflowContext.AnswerSet -ProjectJsonFile $WorkflowContext.Layout.ProjectJsonFile -Example:$WorkflowContext.Example + } + + if ($WorkflowContext.AnswerSet.EnableAgenticCopilot -eq 'Yes') { + Invoke-NovaModuleInitializationStep -Activity $progressActivity -Status 'Applying Agentic Copilot starter' -PercentComplete 85 -Action { + Initialize-NovaModuleAgenticCopilotScaffold -Answer $WorkflowContext.AnswerSet -ProjectRoot $WorkflowContext.Layout.Project -Example:$WorkflowContext.Example + } + } + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + Write-NovaModuleInitializationResult -WorkflowContext $WorkflowContext +} + +function Invoke-NovaModuleInitializationStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + +function Write-NovaModuleInitializationResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + Write-Message ("Created Nova module scaffold: $( $WorkflowContext.AnswerSet.ProjectName )") -color Green + Write-Message ("Project root: $( $WorkflowContext.Layout.Project )") + + foreach ($line in (Get-NovaModuleInitializationNextStepLine -WorkflowContext $WorkflowContext)) { + Write-Message $line + } +} + +function Get-NovaModuleInitializationNextStepLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + $nextSteps = @( + 'Next steps:' + "Set-Location $( $WorkflowContext.Layout.Project )" + ) + + if ($WorkflowContext.Example) { + $nextSteps += 'Test-NovaBuild' + return $nextSteps } - 'Module {0} scaffolding complete' -f $WorkflowContext.AnswerSet.ProjectName | Write-Message -color Green + $nextSteps += 'Invoke-NovaBuild' + return $nextSteps } diff --git a/tests/private/scaffold/InitializeNovaModuleScaffold.Tests.ps1 b/tests/private/scaffold/InitializeNovaModuleScaffold.Tests.ps1 index 7fe7db0e..78264858 100644 --- a/tests/private/scaffold/InitializeNovaModuleScaffold.Tests.ps1 +++ b/tests/private/scaffold/InitializeNovaModuleScaffold.Tests.ps1 @@ -8,6 +8,18 @@ BeforeAll { } Describe 'Initialize-NovaModuleScaffold' { + It 'fails with recovery guidance when the project folder already exists' { + $paths = [pscustomobject]@{Project = (Join-Path $TestDrive 'ExistingProject')} + $null = New-Item -ItemType Directory -Path $paths.Project -Force + + { + Initialize-NovaModuleScaffold -Answer @{ + EnableGit = 'No' + EnableAgenticCopilot = 'No' + } -Paths $paths + } | Should -Throw '*Project folder already exists:*Choose a different project name or remove or move the existing folder before running Initialize-NovaModule again.*' + } + It 'creates the default .gitignore for the standard scaffold when Git is enabled' { $paths = Get-NovaModuleScaffoldLayout -Path $TestDrive -ProjectName 'StandardWithGit' Mock Write-Message {} diff --git a/tests/private/scaffold/InvokeNovaModuleInitializationWorkflow.Tests.ps1 b/tests/private/scaffold/InvokeNovaModuleInitializationWorkflow.Tests.ps1 index 8a34ea13..8b76241d 100644 --- a/tests/private/scaffold/InvokeNovaModuleInitializationWorkflow.Tests.ps1 +++ b/tests/private/scaffold/InvokeNovaModuleInitializationWorkflow.Tests.ps1 @@ -19,13 +19,21 @@ Describe 'Invoke-NovaModuleInitializationWorkflow' { Mock Write-NovaModuleProjectJson {} Mock Initialize-NovaModuleAgenticCopilotScaffold {} Mock Write-Message {} + Mock Write-Progress {} Invoke-NovaModuleInitializationWorkflow -WorkflowContext $script:context Assert-MockCalled Initialize-NovaModuleScaffold -Times 1 Assert-MockCalled Write-NovaModuleProjectJson -Times 1 Assert-MockCalled Initialize-NovaModuleAgenticCopilotScaffold -Times 0 - Assert-MockCalled Write-Message -Times 1 + Assert-MockCalled Write-Progress -Times 3 + Assert-MockCalled Write-Message -Times 4 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $InputObject -eq 'Created Nova module scaffold: DemoModule' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $InputObject -eq 'Invoke-NovaBuild' + } } It 'invokes the Agentic Copilot scaffold step when the answer set requests it' { @@ -33,6 +41,7 @@ Describe 'Invoke-NovaModuleInitializationWorkflow' { Mock Write-NovaModuleProjectJson {} Mock Initialize-NovaModuleAgenticCopilotScaffold {} Mock Write-Message {} + Mock Write-Progress {} $contextWithAgentic = [pscustomobject]@{ AnswerSet = @{ProjectName = 'DemoModule'; EnableAgenticCopilot = 'Yes'} @@ -42,5 +51,26 @@ Describe 'Invoke-NovaModuleInitializationWorkflow' { Invoke-NovaModuleInitializationWorkflow -WorkflowContext $contextWithAgentic Assert-MockCalled Initialize-NovaModuleAgenticCopilotScaffold -Times 1 + Assert-MockCalled Write-Progress -Times 4 + } + + It 'suggests Test-NovaBuild as the next step for the example scaffold' { + Mock Initialize-NovaModuleScaffold {} + Mock Write-NovaModuleProjectJson {} + Mock Initialize-NovaModuleAgenticCopilotScaffold {} + Mock Write-Message {} + Mock Write-Progress {} + + $exampleContext = [pscustomobject]@{ + AnswerSet = @{ProjectName = 'DemoModule'; EnableAgenticCopilot = 'No'} + Layout = [pscustomobject]@{Project = '/tmp/DemoModule'; ProjectJsonFile = '/tmp/DemoModule/project.json'} + Example = $true + } + + Invoke-NovaModuleInitializationWorkflow -WorkflowContext $exampleContext + + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $InputObject -eq 'Test-NovaBuild' + } } } From 241795789139de3857ddd1be77ff03e52a72bf6f Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 14:51:41 +0200 Subject: [PATCH 06/16] #215 feat: enhance Invoke-NovaAgenticCopilotScaffold with progress feedback and clearer cancellation guidance --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../Invoke-NovaAgenticCopilotScaffold.md | 4 ++ ...vokeNovaAgenticCopilotScaffoldWorkflow.ps1 | 53 +++++++++++++++++-- ...ticCopilotScaffoldWorkflow.TestSupport.ps1 | 4 ++ ...vaAgenticCopilotScaffoldWorkflow.Tests.ps1 | 12 ++++- 6 files changed, 70 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c42389ba..e0c43f79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Get-NovaUpdateNotificationPreference -Verbose` now explains whether Nova is reading a stored preference or the built-in default, and the command help now points read-only PowerShell users to the matching `% nova notification` workflow. - `Initialize-NovaModule` now shows scaffold progress, ends with the created project root plus the next suggested cmdlet, and fails with clearer guidance when the target project folder already exists. - `Install-NovaCli` now prints the installed launcher path, suggests the next command to run, and gives a clearer `PATH` warning when the destination directory is not yet available from the shell. +- `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and fails with clearer cancellation guidance when the overwrite warning is declined. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index bab591f6..a8cf52ed 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -18,6 +18,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Get-NovaUpdateNotificationPreference -Verbose` now tells you whether Nova is using a stored update-notification preference or the built-in default, and the help now points to the matching `% nova notification` workflow. - `Initialize-NovaModule` now shows scaffold progress, ends with the project root and the next suggested cmdlet, and gives clearer recovery guidance when the target project folder already exists. - `Install-NovaCli` now prints the installed launcher path, suggests the next command to run, and gives a clearer `PATH` warning when the launcher directory is not yet available from the shell. +- `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and gives clearer cancellation guidance when the overwrite warning is declined. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md b/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md index 3d2bbc90..351f34f1 100644 --- a/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md +++ b/docs/NovaModuleTools/en-US/Invoke-NovaAgenticCopilotScaffold.md @@ -34,6 +34,8 @@ By default the workflow shows an overwrite warning before it updates the managed The target path must contain a valid `project.json`. Invalid project metadata or an invalid short name stops the command with a clear validation error. +During apply or refresh, Nova shows progress for the main scaffold phases and ends with the project root plus the next suggested review and validation steps. + ## EXAMPLES ### EXAMPLE 1 @@ -201,6 +203,8 @@ This workflow does not persist `ShortName` to `project.json` and does not infer `README.md`, `CHANGELOG.md`, and `RELEASE_NOTE.md` are created only when they are missing. +Press `Ctrl+C` before confirming the overwrite warning if you want to cancel the apply without changing files. + ## RELATED LINKS - [Initialize-NovaModule](./Initialize-NovaModule.md) diff --git a/src/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.ps1 b/src/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.ps1 index 5f736999..e479578d 100644 --- a/src/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.ps1 +++ b/src/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.ps1 @@ -49,7 +49,7 @@ function Confirm-NovaAgenticCopilotScaffoldWarning { return } - Stop-NovaOperation -Message 'Operation cancelled.' -ErrorId 'Nova.Workflow.AgenticCopilotScaffoldCancelled' -Category OperationStopped -TargetObject $WorkflowContext.ProjectRoot + Stop-NovaOperation -Message "Agentic Copilot scaffold apply cancelled for $( $WorkflowContext.ProjectRoot ). Re-run the command and choose Yes when you are ready to overwrite the managed scaffold paths." -ErrorId 'Nova.Workflow.AgenticCopilotScaffoldCancelled' -Category OperationStopped -TargetObject $WorkflowContext.ProjectRoot } function Invoke-NovaAgenticCopilotScaffoldWorkflow { @@ -63,7 +63,54 @@ function Invoke-NovaAgenticCopilotScaffoldWorkflow { return } - Confirm-NovaAgenticCopilotScaffoldWarning -WorkflowContext $WorkflowContext - Initialize-NovaModuleAgenticCopilotScaffold -Answer $WorkflowContext.AnswerSet -ProjectRoot $WorkflowContext.ProjectRoot -ScaffoldPolicy $WorkflowContext.ScaffoldPolicy + $progressActivity = 'Applying Agentic Copilot scaffold' + + try { + Invoke-NovaAgenticCopilotScaffoldStep -Activity $progressActivity -Status 'Confirming overwrite warning' -PercentComplete 20 -Action { + Confirm-NovaAgenticCopilotScaffoldWarning -WorkflowContext $WorkflowContext + } + + Invoke-NovaAgenticCopilotScaffoldStep -Activity $progressActivity -Status 'Refreshing managed scaffold files' -PercentComplete 75 -Action { + Initialize-NovaModuleAgenticCopilotScaffold -Answer $WorkflowContext.AnswerSet -ProjectRoot $WorkflowContext.ProjectRoot -ScaffoldPolicy $WorkflowContext.ScaffoldPolicy + } + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + Write-NovaAgenticCopilotScaffoldResult -WorkflowContext $WorkflowContext +} + +function Invoke-NovaAgenticCopilotScaffoldStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + +function Write-NovaAgenticCopilotScaffoldResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + Write-Message "Agentic Copilot scaffold applied to $( $WorkflowContext.ProjectInfo.ProjectName )" -color Green + Write-Message "Project root: $( $WorkflowContext.ProjectRoot )" + + foreach ($line in (Get-NovaAgenticCopilotScaffoldNextStepLine)) { + Write-Message $line + } +} + +function Get-NovaAgenticCopilotScaffoldNextStepLine { + return @( + 'Next steps:' + 'Review AGENTS.md and CONTRIBUTING.md' + 'Test-NovaBuild' + ) } diff --git a/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.TestSupport.ps1 b/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.TestSupport.ps1 index 88c5efc0..f2f4364b 100644 --- a/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.TestSupport.ps1 +++ b/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.TestSupport.ps1 @@ -7,6 +7,10 @@ function Initialize-NovaModuleAgenticCopilotScaffold { ) } +function Write-Message { + param([string]$Message, [string]$color) +} + function Stop-NovaOperation { param([string]$Message, [string]$ErrorId, [System.Management.Automation.ErrorCategory]$Category, $TargetObject) $exception = [System.Exception]::new($Message) diff --git a/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.Tests.ps1 b/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.Tests.ps1 index d1759022..8b83b75c 100644 --- a/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.Tests.ps1 +++ b/tests/private/scaffold/InvokeNovaAgenticCopilotScaffoldWorkflow.Tests.ps1 @@ -71,7 +71,7 @@ Describe 'Confirm-NovaAgenticCopilotScaffoldWarning' { ManagedOverwritePathList = @('.github/agents/') AddOnlyPathList = @('README.md') }) - } | Should -Throw -ErrorId 'Nova.Workflow.AgenticCopilotScaffoldCancelled' + } | Should -Throw '*Agentic Copilot scaffold apply cancelled for /repo.*choose Yes when you are ready to overwrite the managed scaffold paths.*' } } @@ -80,6 +80,7 @@ Describe 'Invoke-NovaAgenticCopilotScaffoldWorkflow' { Mock Confirm-NovaAgenticCopilotScaffoldWarning {} Mock Initialize-NovaModuleAgenticCopilotScaffold {} Mock Write-Message {} + Mock Write-Progress {} } It 'returns without applying when ShouldRun is false' { @@ -108,6 +109,13 @@ Describe 'Invoke-NovaAgenticCopilotScaffoldWorkflow' { Assert-MockCalled Confirm-NovaAgenticCopilotScaffoldWarning -Times 1 Assert-MockCalled Initialize-NovaModuleAgenticCopilotScaffold -Times 1 - Assert-MockCalled Write-Message -Times 1 + Assert-MockCalled Write-Progress -Times 3 + Assert-MockCalled Write-Message -Times 5 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Message -eq 'Agentic Copilot scaffold applied to Demo' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Message -eq 'Test-NovaBuild' + } } } From 2b13456fcaa1016c2288fdeeee911540c67f6aeb Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 15:02:10 +0200 Subject: [PATCH 07/16] #215 feat: enhance Invoke-NovaBuild with progress reporting and next steps guidance Closes refactor(Install-NovaCli): align with terminal-ux-design principles Fixes #219 Closes refactor(Invoke-NovaAgenticCopilotScaffold): align with terminal-ux-design principles Fixes #220 Closes refactor(Invoke-NovaBuild): align with terminal-ux-design principles Fixes #221 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../NovaModuleTools/en-US/Invoke-NovaBuild.md | 8 +- src/private/build/InvokeNovaBuildWorkflow.ps1 | 85 ++++++++++++++++--- .../InvokeNovaBuildWorkflow.TestSupport.ps1 | 1 + .../build/InvokeNovaBuildWorkflow.Tests.ps1 | 52 +++++++++++- 6 files changed, 132 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0c43f79..dbc37c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Initialize-NovaModule` now shows scaffold progress, ends with the created project root plus the next suggested cmdlet, and fails with clearer guidance when the target project folder already exists. - `Install-NovaCli` now prints the installed launcher path, suggests the next command to run, and gives a clearer `PATH` warning when the destination directory is not yet available from the shell. - `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and fails with clearer cancellation guidance when the overwrite warning is declined. +- `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index a8cf52ed..7f037cab 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -19,6 +19,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Initialize-NovaModule` now shows scaffold progress, ends with the project root and the next suggested cmdlet, and gives clearer recovery guidance when the target project folder already exists. - `Install-NovaCli` now prints the installed launcher path, suggests the next command to run, and gives a clearer `PATH` warning when the launcher directory is not yet available from the shell. - `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and gives clearer cancellation guidance when the overwrite warning is declined. +- `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md b/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md index 073ad7ac..1dffe62c 100644 --- a/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md +++ b/docs/NovaModuleTools/en-US/Invoke-NovaBuild.md @@ -29,6 +29,8 @@ PS> Invoke-NovaBuild [-ContinuousIntegration] [-OverrideWarning] [-WhatIf] [-Con This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` to preview the build target without clearing `dist/` or generating new build output. +During a real build, Nova shows progress for the main build phases and ends with the output module directory plus the next suggested validation step. + Use `-ContinuousIntegration` when the same PowerShell session needs to keep using the freshly built `dist/` module after the build completes. In CI/self-hosting flows, that re-activates the built module before the command returns. Use `-OverrideWarning` only when you intentionally want to continue a build even though a file under `src/public` @@ -62,7 +64,7 @@ If `Preamble` is configured, those lines are written at the very top of the gene PS> Invoke-NovaBuild ``` -Builds the current project into `dist//`. +Builds the current project into `dist//`, prints the output module directory, and suggests `Test-NovaBuild` as the next validation step. ### EXAMPLE 2 @@ -70,7 +72,7 @@ Builds the current project into `dist//`. PS> Invoke-NovaBuild -Verbose ``` -Builds the current project and writes verbose progress for the build workflow. +Builds the current project, shows phase progress for the build workflow, and writes verbose details from the underlying build helpers. ### EXAMPLE 3 @@ -172,6 +174,8 @@ When `-ContinuousIntegration` is used together with a real build, the command re Files under `src/public` are expected to contain exactly one top-level function each. Use `-OverrideWarning` only when you intentionally want to bypass that guard for the current build. +Use `Ctrl+C` if you need to stop a running build before all phases complete. + ## RELATED LINKS - [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) diff --git a/src/private/build/InvokeNovaBuildWorkflow.ps1 b/src/private/build/InvokeNovaBuildWorkflow.ps1 index 79063e4f..b59ba8f1 100644 --- a/src/private/build/InvokeNovaBuildWorkflow.ps1 +++ b/src/private/build/InvokeNovaBuildWorkflow.ps1 @@ -7,19 +7,81 @@ function Invoke-NovaBuildWorkflow { $projectInfo = $WorkflowContext.ProjectInfo $continuousIntegrationRequested = ($WorkflowContext.PSObject.Properties.Name -contains 'ContinuousIntegrationRequested') -and $WorkflowContext.ContinuousIntegrationRequested $overrideWarningRequested = ($WorkflowContext.PSObject.Properties.Name -contains 'OverrideWarningRequested') -and $WorkflowContext.OverrideWarningRequested + $progressActivity = 'Building Nova module' - Assert-NovaPublicFunctionFileLayout -ProjectInfo $projectInfo -OverrideWarningRequested:$overrideWarningRequested - Reset-ProjectDist -ProjectInfo $projectInfo -Confirm:$false - Build-Module -ProjectInfo $projectInfo - Invoke-NovaBuildDuplicateValidation -ProjectInfo $projectInfo - Build-Manifest -ProjectInfo $projectInfo - Build-Help -ProjectInfo $projectInfo - Copy-ProjectResource -ProjectInfo $projectInfo - Invoke-NovaModuleUpdateNotificationSafely - - if ($continuousIntegrationRequested) { - $null = Import-NovaBuiltModuleForCi -ProjectInfo $projectInfo + try { + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Validating public command layout' -PercentComplete 10 -Action { + Assert-NovaPublicFunctionFileLayout -ProjectInfo $projectInfo -OverrideWarningRequested:$overrideWarningRequested + } + + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Resetting dist output' -PercentComplete 20 -Action { + Reset-ProjectDist -ProjectInfo $projectInfo -Confirm:$false + } + + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Building module output' -PercentComplete 35 -Action { + Build-Module -ProjectInfo $projectInfo + } + + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Validating duplicate function names' -PercentComplete 50 -Action { + Invoke-NovaBuildDuplicateValidation -ProjectInfo $projectInfo + } + + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Writing module manifest' -PercentComplete 65 -Action { + Build-Manifest -ProjectInfo $projectInfo + } + + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Building command help' -PercentComplete 78 -Action { + Build-Help -ProjectInfo $projectInfo + } + + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Copying project resources' -PercentComplete 88 -Action { + Copy-ProjectResource -ProjectInfo $projectInfo + } + + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Checking update notifications' -PercentComplete 94 -Action { + Invoke-NovaModuleUpdateNotificationSafely + } + + if ($continuousIntegrationRequested) { + Invoke-NovaBuildWorkflowStep -Activity $progressActivity -Status 'Refreshing the current session with the built module' -PercentComplete 98 -Action { + $null = Import-NovaBuiltModuleForCi -ProjectInfo $projectInfo + } + } + } finally { + Write-Progress -Activity $progressActivity -Completed } + + Write-NovaBuildWorkflowResult -ProjectInfo $projectInfo -ContinuousIntegrationRequested:$continuousIntegrationRequested +} + +function Invoke-NovaBuildWorkflowStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + +function Write-NovaBuildWorkflowResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$ProjectInfo, + [switch]$ContinuousIntegrationRequested + ) + + Write-Message "Built Nova module: $( $ProjectInfo.ProjectName )" -color Green + Write-Message "Output module: $( $ProjectInfo.OutputModuleDir )" + + if ($ContinuousIntegrationRequested) { + Write-Message 'The freshly built dist module is loaded for later commands in this session.' + } + + Write-Message 'Next step: Test-NovaBuild' } function Invoke-NovaBuildDuplicateValidation { @@ -34,4 +96,3 @@ function Invoke-NovaBuildDuplicateValidation { Assert-BuiltModuleHasNoDuplicateFunctionName -ProjectInfo $ProjectInfo } - diff --git a/tests/private/build/InvokeNovaBuildWorkflow.TestSupport.ps1 b/tests/private/build/InvokeNovaBuildWorkflow.TestSupport.ps1 index c8fc5386..732b91a0 100644 --- a/tests/private/build/InvokeNovaBuildWorkflow.TestSupport.ps1 +++ b/tests/private/build/InvokeNovaBuildWorkflow.TestSupport.ps1 @@ -7,3 +7,4 @@ function Build-Help {param($ProjectInfo)} function Copy-ProjectResource {param($ProjectInfo)} function Invoke-NovaModuleUpdateNotificationSafely {} function Import-NovaBuiltModuleForCi {param($ProjectInfo)} +function Write-Message {param([string]$Text, [string]$color)} diff --git a/tests/private/build/InvokeNovaBuildWorkflow.Tests.ps1 b/tests/private/build/InvokeNovaBuildWorkflow.Tests.ps1 index 30cf48ed..6949da59 100644 --- a/tests/private/build/InvokeNovaBuildWorkflow.Tests.ps1 +++ b/tests/private/build/InvokeNovaBuildWorkflow.Tests.ps1 @@ -6,7 +6,49 @@ BeforeAll { } Describe 'Invoke-NovaBuildWorkflow' { - It 'uses the shared module update notification before the optional CI import step' { + It 'reports progress, runs the build steps in order, and prints the next suggested validation step' { + $global:steps = @() + try { + $workflowContext = [pscustomobject]@{ + ProjectInfo = [pscustomobject]@{ + ProjectName = 'NovaModuleTools' + OutputModuleDir = '/tmp/dist/NovaModuleTools' + FailOnDuplicateFunctionNames = $true + } + ContinuousIntegrationRequested = $false + } + + Mock Assert-NovaPublicFunctionFileLayout {$global:steps += 'public-layout'} + Mock Reset-ProjectDist {$global:steps += 'reset'} + Mock Build-Module {$global:steps += 'module'} + Mock Assert-BuiltModuleHasNoDuplicateFunctionName {$global:steps += 'duplicates'} + Mock Build-Manifest {$global:steps += 'manifest'} + Mock Build-Help {$global:steps += 'help'} + Mock Copy-ProjectResource {$global:steps += 'resources'} + Mock Invoke-NovaModuleUpdateNotificationSafely {$global:steps += 'notification'} + Mock Import-NovaBuiltModuleForCi {$global:steps += 'ci'} + Mock Write-Message {} + Mock Write-Progress {} + + Invoke-NovaBuildWorkflow -WorkflowContext $workflowContext + + $global:steps -join ',' | Should -Be 'public-layout,reset,module,duplicates,manifest,help,resources,notification' + Assert-MockCalled Invoke-NovaModuleUpdateNotificationSafely -Times 1 + Assert-MockCalled Import-NovaBuiltModuleForCi -Times 0 + Assert-MockCalled Write-Progress -Times 9 + Assert-MockCalled Write-Message -Times 3 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Built Nova module: NovaModuleTools' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Next step: Test-NovaBuild' + } + } finally { + Remove-Variable -Name steps -Scope Global -ErrorAction SilentlyContinue + } + } + + It 'uses the shared module update notification before the optional CI import step and reports the refreshed session' { $global:steps = @() try { $workflowContext = [pscustomobject]@{ @@ -27,12 +69,18 @@ Describe 'Invoke-NovaBuildWorkflow' { Mock Copy-ProjectResource {$global:steps += 'resources'} Mock Invoke-NovaModuleUpdateNotificationSafely {$global:steps += 'notification'} Mock Import-NovaBuiltModuleForCi {$global:steps += 'ci'} + Mock Write-Message {} + Mock Write-Progress {} Invoke-NovaBuildWorkflow -WorkflowContext $workflowContext $global:steps -join ',' | Should -Be 'public-layout,reset,module,duplicates,manifest,help,resources,notification,ci' - Assert-MockCalled Invoke-NovaModuleUpdateNotificationSafely -Times 1 Assert-MockCalled Import-NovaBuiltModuleForCi -Times 1 + Assert-MockCalled Write-Progress -Times 10 + Assert-MockCalled Write-Message -Times 4 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'The freshly built dist module is loaded for later commands in this session.' + } } finally { Remove-Variable -Name steps -Scope Global -ErrorAction SilentlyContinue } From 5320789a1cd5611f93d4eda04839cf79117e6ec2 Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 15:12:54 +0200 Subject: [PATCH 08/16] #215 feat: enhance Invoke-NovaCli command handling with improved help and error messages refactor(Invoke-NovaCli): align with terminal-ux-design principles Fixes #222 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + docs/NovaModuleTools/en-US/Invoke-NovaCli.md | 191 ++++++++++++++++++ docs/NovaModuleTools/en-US/NovaModuleTools.md | 4 + src/private/cli/InvokeNovaCliCommandRoute.ps1 | 2 +- src/public/InvokeNovaCli.ps1 | 8 +- .../cli/InvokeNovaCliCommandRoute.Tests.ps1 | 12 +- tests/public/InvokeNovaCli.Tests.ps1 | 4 + 8 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 docs/NovaModuleTools/en-US/Invoke-NovaCli.md diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc37c7c..7b0fc78c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Install-NovaCli` now prints the installed launcher path, suggests the next command to run, and gives a clearer `PATH` warning when the destination directory is not yet available from the shell. - `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and fails with clearer cancellation guidance when the overwrite warning is declined. - `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. +- `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated PlatyPS help topic for the PowerShell wrapper surface. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 7f037cab..4d8bdfb1 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -20,6 +20,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Install-NovaCli` now prints the installed launcher path, suggests the next command to run, and gives a clearer `PATH` warning when the launcher directory is not yet available from the shell. - `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and gives clearer cancellation guidance when the overwrite warning is declined. - `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. +- `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated help topic for the PowerShell wrapper surface. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Invoke-NovaCli.md b/docs/NovaModuleTools/en-US/Invoke-NovaCli.md new file mode 100644 index 00000000..5ebe9a78 --- /dev/null +++ b/docs/NovaModuleTools/en-US/Invoke-NovaCli.md @@ -0,0 +1,191 @@ +--- +document type: cmdlet +external help file: NovaModuleTools-Help.xml +HelpUri: '' +Locale: en-US +Module Name: NovaModuleTools +ms.date: 05/24/2026 +PlatyPS schema version: 2024-05-01 +title: Invoke-NovaCli +--- + +# Invoke-NovaCli + +## SYNOPSIS + +Runs Nova's launcher-style command routing from PowerShell. + +## SYNTAX + +### __AllParameterSets + +```text +PS> Invoke-NovaCli [[-Command] ] [[-Arguments] ] [-WhatIf] [-Confirm] [] +``` + +## DESCRIPTION + +`Invoke-NovaCli` is the PowerShell entrypoint behind the launcher-style `nova ` workflow. + +Use this cmdlet when you want Nova's routed CLI experience from inside PowerShell, including root help, per-command help, and the routed command surfaces such as `build`, `test`, `package`, `publish`, `release`, `update`, and `notification`. + +When `-Command` is omitted or blank, Nova falls back to the root CLI help. + +This cmdlet forwards `-WhatIf` and `-Confirm` semantics to the routed command surface instead of treating `Invoke-NovaCli` itself as the mutating operation owner. + +Use `Get-Help Invoke-NovaCli -Full` when you want the PowerShell wrapper help. Use `nova --help`, `nova --help`, or `nova --help ` when you want launcher-native CLI help. + +## EXAMPLES + +### EXAMPLE 1 + +```text +PS> Invoke-NovaCli +``` + +Shows the root Nova CLI help. + +### EXAMPLE 2 + +```text +PS> Invoke-NovaCli build --help +``` + +Shows the short help for the routed `build` command by using launcher-style arguments from PowerShell. + +### EXAMPLE 3 + +```text +PS> Invoke-NovaCli --help build +``` + +Shows the long help for the routed `build` command. + +### EXAMPLE 4 + +```text +PS> Invoke-NovaCli publish --repository PSGallery --api-key -WhatIf +``` + +Previews the routed publish workflow without changing files or publishing artifacts. + +## PARAMETERS + +### -Arguments + +Pass the remaining launcher-style arguments for the routed command. + +Use CLI syntax here, for example `--help`, `--verbose`, `--what-if`, or route-specific flags such as `--repository`. + +```yaml +Type: System.String[] +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: + - Name: (All) + Position: 1 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: true +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Command + +Choose the routed Nova command to run, for example `build`, `test`, `package`, `publish`, `release`, `notification`, `update`, or the root help/version forms such as `--help` and `--version`. + +When this parameter is omitted or blank, Nova shows the root CLI help. + +```yaml +Type: System.String +DefaultValue: '' +SupportsWildcards: false +Aliases: [] +ParameterSets: + - Name: (All) + Position: 0 + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -Confirm + +Forward confirmation behavior to routed mutating commands that support Nova's CLI confirmation flow. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: + - cf +ParameterSets: + - Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### -WhatIf + +Forward preview behavior to routed commands that support Nova's non-destructive preview flow. + +```yaml +Type: System.Management.Automation.SwitchParameter +DefaultValue: '' +SupportsWildcards: false +Aliases: + - wi +ParameterSets: + - Name: (All) + Position: Named + IsRequired: false + ValueFromPipeline: false + ValueFromPipelineByPropertyName: false + ValueFromRemainingArguments: false +DontShow: false +AcceptedValues: [] +HelpMessage: '' +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutBuffer, -OutVariable, -PipelineVariable, -ProgressAction, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](https://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### None + +You can't pipe objects to this cmdlet. + +## OUTPUTS + +### System.Object + +This cmdlet returns the routed command result or CLI help text for help and version requests. + +## NOTES + +`Invoke-NovaCli` is a PowerShell wrapper around Nova's launcher-style command routing. The routed command surfaces keep CLI syntax such as `--help`, `--verbose`, and `--what-if`. + +Use `Ctrl+C` if you need to cancel a running routed workflow. + +## RELATED LINKS + +- [Invoke-NovaBuild](./Invoke-NovaBuild.md) +- [Test-NovaBuild](./Test-NovaBuild.md) +- [Install-NovaCli](./Install-NovaCli.md) +- [NovaModuleTools Module](./NovaModuleTools.md) diff --git a/docs/NovaModuleTools/en-US/NovaModuleTools.md b/docs/NovaModuleTools/en-US/NovaModuleTools.md index 840c2a44..0dd02f5d 100644 --- a/docs/NovaModuleTools/en-US/NovaModuleTools.md +++ b/docs/NovaModuleTools/en-US/NovaModuleTools.md @@ -32,6 +32,10 @@ Shows whether prerelease update notifications are enabled. Stable release notifi Installs the bundled command-line launcher into a user command directory on macOS or Linux. +### `PS> Invoke-NovaCli` + +Runs Nova's launcher-style command routing from inside PowerShell. + ### `PS> Invoke-NovaBuild` Builds the current NovaModuleTools project into a ready-to-import PowerShell module. diff --git a/src/private/cli/InvokeNovaCliCommandRoute.ps1 b/src/private/cli/InvokeNovaCliCommandRoute.ps1 index 3e4c570b..d63f4994 100644 --- a/src/private/cli/InvokeNovaCliCommandRoute.ps1 +++ b/src/private/cli/InvokeNovaCliCommandRoute.ps1 @@ -216,7 +216,7 @@ function Invoke-NovaCliCommandRoute { return Get-NovaCliHelp } default { - Stop-NovaOperation -Message "Unknown command: <$command> | Use 'nova --help' to see available commands." -ErrorId 'Nova.Validation.UnknownCliCommand' -Category InvalidArgument -TargetObject $command + Stop-NovaOperation -Message "Unknown nova command: $command. Run 'nova --help' to list available commands, or 'nova --help ' for command-specific help." -ErrorId 'Nova.Validation.UnknownCliCommand' -Category InvalidArgument -TargetObject $command } } } diff --git a/src/public/InvokeNovaCli.ps1 b/src/public/InvokeNovaCli.ps1 index 486cf08b..64e5f835 100644 --- a/src/public/InvokeNovaCli.ps1 +++ b/src/public/InvokeNovaCli.ps1 @@ -8,8 +8,14 @@ function Invoke-NovaCli { [string[]]$Arguments ) + $commandName = if ([string]::IsNullOrWhiteSpace($Command)) { + '--help' + } else { + $Command + } + $invocationRequest = [pscustomobject]@{ - Command = $Command + Command = $commandName BoundParameters = $PSBoundParameters Arguments = $Arguments } diff --git a/tests/private/cli/InvokeNovaCliCommandRoute.Tests.ps1 b/tests/private/cli/InvokeNovaCliCommandRoute.Tests.ps1 index 20d7986f..c3c260a8 100644 --- a/tests/private/cli/InvokeNovaCliCommandRoute.Tests.ps1 +++ b/tests/private/cli/InvokeNovaCliCommandRoute.Tests.ps1 @@ -231,6 +231,16 @@ Describe 'Invoke-NovaCliCommandRoute' { It 'throws on unknown command' { Mock Confirm-NovaCliRoutedCommand {} - {Invoke-NovaCliCommandRoute -InvocationContext (New-TestContext -Command 'bogus')} | Should -Throw -ErrorId 'Nova.Validation.UnknownCliCommand' + $record = $null + + try { + Invoke-NovaCliCommandRoute -InvocationContext (New-TestContext -Command 'bogus') + } catch { + $record = $_ + } + + $record | Should -Not -BeNullOrEmpty + $record.FullyQualifiedErrorId | Should -Be 'Nova.Validation.UnknownCliCommand' + $record.Exception.Message | Should -Be "Unknown nova command: bogus. Run 'nova --help' to list available commands, or 'nova --help ' for command-specific help." } } diff --git a/tests/public/InvokeNovaCli.Tests.ps1 b/tests/public/InvokeNovaCli.Tests.ps1 index 3f1258ae..fa46895d 100644 --- a/tests/public/InvokeNovaCli.Tests.ps1 +++ b/tests/public/InvokeNovaCli.Tests.ps1 @@ -14,6 +14,10 @@ Describe 'Invoke-NovaCli' { (Invoke-NovaCli).Command | Should -Be '--help' } + It 'defaults blank command input to --help' { + (Invoke-NovaCli '').Command | Should -Be '--help' + } + It 'forwards command and remaining arguments to the invocation context' { Invoke-NovaCli 'build' '--ci' '--override-warning' | Out-Null $script:request.Command | Should -Be 'build' From 491fa54cf6a216b75303fe2e02094f577084d40c Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 15:22:33 +0200 Subject: [PATCH 09/16] #215 feat: enhance Invoke-NovaRelease with progress reporting and next-step guidance refactor(Invoke-NovaRelease): align with terminal-ux-design principles Fixes #223 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../en-US/Invoke-NovaRelease.md | 4 + .../release/InvokeNovaReleaseWorkflow.ps1 | 193 +++++++++++++++++- .../InvokeNovaReleaseWorkflow.TestSupport.ps1 | 54 +++++ .../InvokeNovaReleaseWorkflow.Tests.ps1 | 53 ++++- 6 files changed, 287 insertions(+), 19 deletions(-) create mode 100644 tests/private/release/InvokeNovaReleaseWorkflow.TestSupport.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b0fc78c..65e6cee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and fails with clearer cancellation guidance when the overwrite warning is declined. - `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. - `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated PlatyPS help topic for the PowerShell wrapper surface. +- `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 4d8bdfb1..3349577c 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -21,6 +21,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Invoke-NovaAgenticCopilotScaffold` now shows apply progress, ends with the project root plus suggested review and validation steps, and gives clearer cancellation guidance when the overwrite warning is declined. - `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. - `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated help topic for the PowerShell wrapper surface. +- `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md b/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md index c8e3efed..30f32bb3 100644 --- a/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md +++ b/docs/NovaModuleTools/en-US/Invoke-NovaRelease.md @@ -57,6 +57,8 @@ When local release mode is selected, the resolved local publish target is previe This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` to preview the entire release workflow and resolved publish target without building, testing, versioning, or publishing. +During the workflow, Nova shows progress for the main release phases. When the workflow completes, Nova prints the publish target and a short next-step hint. In `-WhatIf` mode, Nova ends with a release-plan summary instead of a release-complete message. + ## EXAMPLES ### EXAMPLE 1 @@ -323,6 +325,8 @@ When `-ContinuousIntegration` is used, the release workflow restores the built ` `Invoke-NovaRelease` uses `SupportsShouldProcess`, so `Get-Help Invoke-NovaRelease -Full` surfaces native `-WhatIf` and `-Confirm` support. +Use `Ctrl+C` if you need to stop a running release before all phases complete. + ## RELATED LINKS - [Invoke-NovaBuild](./Invoke-NovaBuild.md) diff --git a/src/private/release/InvokeNovaReleaseWorkflow.ps1 b/src/private/release/InvokeNovaReleaseWorkflow.ps1 index dc54fa89..9661b7ba 100644 --- a/src/private/release/InvokeNovaReleaseWorkflow.ps1 +++ b/src/private/release/InvokeNovaReleaseWorkflow.ps1 @@ -52,20 +52,195 @@ function Invoke-NovaReleaseWorkflow { $testWorkflowParams = Get-NovaReleaseBuildWorkflowParameterMap -WorkflowParams $workflowParams -OverrideWarningRequested:$overrideWarningRequested $shouldRestoreBuiltModule = Test-NovaReleaseWorkflowShouldRestoreBuiltModule -WorkflowParams $workflowParams -ContinuousIntegrationRequested:$continuousIntegrationRequested $publishParams = $WorkflowContext.PublishParams + $whatIfEnabled = Test-NovaReleaseWorkflowWhatIfEnabled -WorkflowParams $workflowParams + $progressActivity = 'Running Nova release workflow' + $versionResult = $null - Invoke-NovaBuild @buildWorkflowParams - if (-not $skipTestsRequested) { - Test-NovaBuild @testWorkflowParams - } + try { + Invoke-NovaReleaseWorkflowStep -Activity $progressActivity -Status 'Building the current project state' -PercentComplete 15 -Action { + Invoke-NovaBuild @buildWorkflowParams + } + + if (-not $skipTestsRequested) { + Invoke-NovaReleaseWorkflowStep -Activity $progressActivity -Status 'Running pre-release tests' -PercentComplete 35 -Action { + Test-NovaBuild @testWorkflowParams + } + } + + $versionResult = Invoke-NovaReleaseWorkflowStep -Activity $progressActivity -Status (Get-NovaReleaseVersionStepStatus -WhatIfEnabled:$whatIfEnabled) -PercentComplete 55 -Action { + Update-NovaModuleVersion @ciWorkflowParams + } - $versionResult = Update-NovaModuleVersion @ciWorkflowParams - Invoke-NovaBuild @buildWorkflowParams + Invoke-NovaReleaseWorkflowStep -Activity $progressActivity -Status 'Rebuilding release output' -PercentComplete 75 -Action { + Invoke-NovaBuild @buildWorkflowParams + } - & $WorkflowContext.PublishInvocation.Action @publishParams + Invoke-NovaReleaseWorkflowStep -Activity $progressActivity -Status (Get-NovaReleasePublishStepStatus -WorkflowContext $WorkflowContext -WhatIfEnabled:$whatIfEnabled) -PercentComplete 90 -Action { + & $WorkflowContext.PublishInvocation.Action @publishParams + } - if ($shouldRestoreBuiltModule) { - $null = Import-NovaBuiltModuleForCi -ProjectInfo $WorkflowContext.ProjectInfo + if ($shouldRestoreBuiltModule) { + Invoke-NovaReleaseWorkflowStep -Activity $progressActivity -Status 'Refreshing the current session with the built module' -PercentComplete 98 -Action { + $null = Import-NovaBuiltModuleForCi -ProjectInfo $WorkflowContext.ProjectInfo + } + } + } finally { + Write-Progress -Activity $progressActivity -Completed } + Write-NovaReleaseWorkflowResult -WorkflowContext $WorkflowContext -VersionResult $versionResult -WhatIfEnabled:$whatIfEnabled -ShouldRestoreBuiltModule:$shouldRestoreBuiltModule return $versionResult } + +function Invoke-NovaReleaseWorkflowStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + +function Test-NovaReleaseWorkflowWhatIfEnabled { + [CmdletBinding()] + param( + [hashtable]$WorkflowParams = @{} + ) + + return $WorkflowParams.ContainsKey('WhatIf') -and $WorkflowParams.WhatIf +} + +function Get-NovaReleaseVersionStepStatus { + [CmdletBinding()] + param( + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return 'Planning the next release version' + } + + return 'Updating the project version' +} + +function Get-NovaReleasePublishStepStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + $targetDescription = if ($WorkflowContext.PublishInvocation.IsLocal) { + 'the local module path' + } else { + "repository $( $WorkflowContext.PublishInvocation.Target )" + } + + if ($WhatIfEnabled) { + return "Previewing publish to $targetDescription" + } + + return "Publishing release to $targetDescription" +} + +function Write-NovaReleaseWorkflowResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [AllowNull()][object]$VersionResult, + [switch]$WhatIfEnabled, + [switch]$ShouldRestoreBuiltModule + ) + + $resolvedVersion = Get-NovaReleaseWorkflowResultVersion -VersionResult $VersionResult + $statusMessage = Get-NovaReleaseWorkflowStatusMessage -ProjectInfo $WorkflowContext.ProjectInfo -Version $resolvedVersion -WhatIfEnabled:$WhatIfEnabled + Write-Message $statusMessage -color Green + Write-Message "Publish target: $( $WorkflowContext.PublishInvocation.Target )" + + if ($WorkflowContext.SkipTestsRequested) { + Write-Message 'Pre-release tests were skipped for this run.' + } + + if ($ShouldRestoreBuiltModule) { + Write-Message 'The freshly built dist module is loaded again for later commands in this session.' + } + + foreach ($line in (Get-NovaReleaseWorkflowNextStepLine -WorkflowContext $WorkflowContext -WhatIfEnabled:$WhatIfEnabled)) { + Write-Message $line + } +} + +function Get-NovaReleaseWorkflowResultVersion { + [CmdletBinding()] + param( + [AllowNull()][object]$VersionResult + ) + + if ($null -eq $VersionResult) { + return $null + } + + if ($VersionResult.PSObject.Properties.Name -contains 'NewVersion') { + return $VersionResult.NewVersion + } + + if ($VersionResult.PSObject.Properties.Name -contains 'Version') { + return $VersionResult.Version + } + + return $null +} + +function Get-NovaReleaseWorkflowStatusMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$ProjectInfo, + [AllowNull()][string]$Version, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + if ([string]::IsNullOrWhiteSpace($Version)) { + return "Release plan ready for $( $ProjectInfo.ProjectName )" + } + + return "Release plan ready for $( $ProjectInfo.ProjectName ) -> $Version" + } + + if ([string]::IsNullOrWhiteSpace($Version)) { + return "Released Nova module: $( $ProjectInfo.ProjectName )" + } + + return "Released Nova module: $( $ProjectInfo.ProjectName ) $Version" +} + +function Get-NovaReleaseWorkflowNextStepLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return @( + 'Next step:' + 'Run Invoke-NovaRelease without -WhatIf when you are ready to apply the release.' + ) + } + + if ($WorkflowContext.PublishInvocation.IsLocal) { + return @( + 'Next step:' + 'Get-NovaProjectInfo -Installed' + ) + } + + return @( + 'Next step:' + 'Get-NovaProjectInfo -Version' + ) +} diff --git a/tests/private/release/InvokeNovaReleaseWorkflow.TestSupport.ps1 b/tests/private/release/InvokeNovaReleaseWorkflow.TestSupport.ps1 new file mode 100644 index 00000000..c9b2a695 --- /dev/null +++ b/tests/private/release/InvokeNovaReleaseWorkflow.TestSupport.ps1 @@ -0,0 +1,54 @@ +function Get-NovaBuildCommandParameterMap { + param( + [hashtable]$WorkflowParams, + [switch]$OverrideWarningRequested + ) + + $map = @{} + $WorkflowParams + if ($OverrideWarningRequested) { + $map.OverrideWarning = $true + } + + return $map +} + +function Invoke-NovaBuild { + param() + + $script:buildCalls += 1 +} + +function Test-NovaBuild { + param() + + $script:testCalls += 1 +} + +function Update-NovaModuleVersion { + param() + + $script:versionCalls += 1 + return [pscustomobject]@{Version = '1.0.0'} +} + +function Import-NovaBuiltModuleForCi { + param($ProjectInfo) + + $script:restoreCalls += 1 +} + +function Write-Message { + param( + [string]$Text, + [string]$color + ) +} + +function Write-Progress { + param( + $Activity, + $Status, + $PercentComplete, + [switch]$Completed + ) +} diff --git a/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 b/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 index ad1060d9..072fcec0 100644 --- a/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 +++ b/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 @@ -1,11 +1,7 @@ BeforeAll { $projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) . (Join-Path $projectRoot 'src/private/release/InvokeNovaReleaseWorkflow.ps1') - function Get-NovaBuildCommandParameterMap {param([hashtable]$WorkflowParams,[switch]$OverrideWarningRequested) $map=@{}+$WorkflowParams; if($OverrideWarningRequested){$map.OverrideWarning=$true}; return $map} - function Invoke-NovaBuild {param() $script:buildCalls += 1} - function Test-NovaBuild {param() $script:testCalls += 1} - function Update-NovaModuleVersion {param() $script:versionCalls += 1; return [pscustomobject]@{Version='1.0.0'}} - function Import-NovaBuiltModuleForCi {param($ProjectInfo) $script:restoreCalls += 1} + . (Join-Path $PSScriptRoot 'InvokeNovaReleaseWorkflow.TestSupport.ps1') } Describe 'Get-NovaReleaseNestedWorkflowParameterMap' { @@ -46,14 +42,16 @@ Describe 'Invoke-NovaReleaseWorkflow' { $script:versionCalls = 0 $script:restoreCalls = 0 $script:publishCalls = 0 + Mock Write-Message {} + Mock Write-Progress {} } - It 'builds, tests, updates version, builds again, publishes, and restores in CI' { + It 'builds, tests, updates version, builds again, publishes, restores in CI, and reports the result' { $ctx = [pscustomobject]@{ WorkflowParams = @{} PublishParams = @{} - PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}} - ProjectInfo = [pscustomobject]@{} + PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}; Target = 'PSGallery'; IsLocal = $false} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'} ContinuousIntegrationRequested = $true SkipTestsRequested = $false OverrideWarningRequested = $false @@ -65,18 +63,53 @@ Describe 'Invoke-NovaReleaseWorkflow' { $script:versionCalls | Should -Be 1 $script:publishCalls | Should -Be 1 $script:restoreCalls | Should -Be 1 + Assert-MockCalled Write-Progress -Times 6 + Assert-MockCalled Write-Message -Times 5 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Released Nova module: NovaModuleTools 1.0.0' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Get-NovaProjectInfo -Version' + } } It 'skips tests when SkipTestsRequested and skips restore when not CI' { $ctx = [pscustomobject]@{ WorkflowParams = @{} PublishParams = @{} - PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}} - ProjectInfo = [pscustomobject]@{} + PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}; Target = '/modules'; IsLocal = $true} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'} SkipTestsRequested = $true } $null = Invoke-NovaReleaseWorkflow -WorkflowContext $ctx $script:testCalls | Should -Be 0 $script:restoreCalls | Should -Be 0 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Pre-release tests were skipped for this run.' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Get-NovaProjectInfo -Installed' + } + } + + It 'writes a release plan summary in WhatIf mode without restoring the built module' { + $ctx = [pscustomobject]@{ + WorkflowParams = @{WhatIf = $true} + PublishParams = @{} + PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}; Target = 'PSGallery'; IsLocal = $false} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'} + ContinuousIntegrationRequested = $true + SkipTestsRequested = $false + } + + $null = Invoke-NovaReleaseWorkflow -WorkflowContext $ctx + + $script:restoreCalls | Should -Be 0 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Release plan ready for NovaModuleTools -> 1.0.0' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Run Invoke-NovaRelease without -WhatIf when you are ready to apply the release.' + } } } From 849d3bc0a15541822df3102a6884ba112550fbad Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 15:30:45 +0200 Subject: [PATCH 10/16] #215 feat: enhance New-NovaModulePackage with progress reporting and next-step guidance refactor(New-NovaModulePackage): align with terminal-ux-design principles Fixes #224 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../en-US/New-NovaModulePackage.md | 4 + .../package/InvokeNovaPackageWorkflow.ps1 | 137 +++++++++++++++++- .../InvokeNovaPackageWorkflow.TestSupport.ps1 | 18 +++ .../InvokeNovaPackageWorkflow.Tests.ps1 | 52 ++++++- 6 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 tests/private/package/InvokeNovaPackageWorkflow.TestSupport.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 65e6cee2..a04b3108 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. - `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated PlatyPS help topic for the PowerShell wrapper surface. - `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. +- `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 3349577c..446783f8 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -22,6 +22,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Invoke-NovaBuild` now shows progress for the main build phases, ends with the output module directory plus the next suggested validation step, and explains the refreshed session when `-ContinuousIntegration` reloads the built module. - `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated help topic for the PowerShell wrapper surface. - `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. +- `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/New-NovaModulePackage.md b/docs/NovaModuleTools/en-US/New-NovaModulePackage.md index ae124936..772b4baf 100644 --- a/docs/NovaModuleTools/en-US/New-NovaModulePackage.md +++ b/docs/NovaModuleTools/en-US/New-NovaModulePackage.md @@ -35,6 +35,8 @@ Use `-OverrideWarning` only when you intentionally want the nested build to cont The package is written to `artifacts/packages/` by default. You can override generic package metadata through the optional `Package` section in `project.json`. +During a packaging run, Nova shows progress for the build-validation phase and the artifact-creation phase. When packaging completes, Nova prints the package target together with the next suggested step. In `-WhatIf` mode, Nova ends with a package-plan summary instead of a package-created summary. + Use this `project.json` shape when you want to control package types and the package output directory: ```json @@ -275,6 +277,8 @@ When `-SkipTests` is used, only `Test-NovaBuild` is skipped. Build still runs. Files under `src/public` are expected to contain exactly one top-level function each. `-OverrideWarning` bypasses that guard only for the current packaging run. +Use `Ctrl+C` if you need to stop a running packaging workflow before the package artifacts are created. + ## RELATED LINKS - [Invoke-NovaBuild](./Invoke-NovaBuild.md) diff --git a/src/private/package/InvokeNovaPackageWorkflow.ps1 b/src/private/package/InvokeNovaPackageWorkflow.ps1 index 8df20457..c3b29785 100644 --- a/src/private/package/InvokeNovaPackageWorkflow.ps1 +++ b/src/private/package/InvokeNovaPackageWorkflow.ps1 @@ -5,11 +5,140 @@ function Invoke-NovaPackageWorkflow { [switch]$ShouldRun ) - Invoke-NovaBuildValidation -WorkflowContext $WorkflowContext + $whatIfEnabled = Test-NovaPackageWorkflowWhatIfEnabled -WorkflowContext $WorkflowContext + $progressActivity = 'Creating Nova package artifacts' - if (-not $ShouldRun) { - return + try { + Invoke-NovaPackageWorkflowStep -Activity $progressActivity -Status (Get-NovaPackageValidationStatus -WorkflowContext $WorkflowContext) -PercentComplete 30 -Action { + Invoke-NovaBuildValidation -WorkflowContext $WorkflowContext + } + + if (-not $ShouldRun) { + if ($whatIfEnabled) { + Write-NovaPackageWorkflowResult -WorkflowContext $WorkflowContext -WhatIfEnabled + } + + return + } + + $artifacts = Invoke-NovaPackageWorkflowStep -Activity $progressActivity -Status 'Creating package artifacts' -PercentComplete 85 -Action { + Invoke-NovaPackageArtifactCreation -WorkflowContext $WorkflowContext + } + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + Write-NovaPackageWorkflowResult -WorkflowContext $WorkflowContext -Artifacts $artifacts + return $artifacts +} + +function Invoke-NovaPackageWorkflowStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + +function Test-NovaPackageWorkflowWhatIfEnabled { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + return ($WorkflowContext.WorkflowParams.ContainsKey('WhatIf') -and $WorkflowContext.WorkflowParams.WhatIf) +} + +function Get-NovaPackageValidationStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + if ($WorkflowContext.SkipTestsRequested) { + return 'Building package input with tests skipped' } - return Invoke-NovaPackageArtifactCreation -WorkflowContext $WorkflowContext + return 'Building and testing package input' +} + +function Write-NovaPackageWorkflowResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [AllowNull()][AllowEmptyCollection()][object[]]$Artifacts = @(), + [switch]$WhatIfEnabled + ) + + $resolvedArtifacts = @($Artifacts) + $statusMessage = Get-NovaPackageWorkflowStatusMessage -WorkflowContext $WorkflowContext -ArtifactCount $resolvedArtifacts.Count -WhatIfEnabled:$WhatIfEnabled + Write-Message $statusMessage -color Green + Write-Message "Package target: $( Get-NovaPackageWorkflowResultTarget -WorkflowContext $WorkflowContext -Artifacts $resolvedArtifacts -WhatIfEnabled:$WhatIfEnabled )" + + foreach ($line in (Get-NovaPackageWorkflowNextStepLine -WhatIfEnabled:$WhatIfEnabled)) { + Write-Message $line + } +} + +function Get-NovaPackageWorkflowStatusMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [Parameter(Mandatory)][int]$ArtifactCount, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return "Package plan ready for $( $WorkflowContext.ProjectInfo.ProjectName )" + } + + if ($ArtifactCount -eq 1) { + return "Created 1 package artifact for $( $WorkflowContext.ProjectInfo.ProjectName )" + } + + return "Created $ArtifactCount package artifacts for $( $WorkflowContext.ProjectInfo.ProjectName )" +} + +function Get-NovaPackageWorkflowResultTarget { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [Parameter(Mandatory)][AllowEmptyCollection()][object[]]$Artifacts, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return $WorkflowContext.Target + } + + $resolvedOutputDirectories = @($Artifacts | ForEach-Object OutputDirectory | Sort-Object -Unique) + if ($resolvedOutputDirectories.Count -eq 0) { + return $WorkflowContext.Target + } + + return ($resolvedOutputDirectories -join ', ') +} + +function Get-NovaPackageWorkflowNextStepLine { + [CmdletBinding()] + param( + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return @( + 'Next step:' + 'Run New-NovaModulePackage without -WhatIf when you are ready to create the package artifacts.' + ) + } + + return @( + 'Next step:' + 'Deploy-NovaPackage' + ) } diff --git a/tests/private/package/InvokeNovaPackageWorkflow.TestSupport.ps1 b/tests/private/package/InvokeNovaPackageWorkflow.TestSupport.ps1 new file mode 100644 index 00000000..4c9549a0 --- /dev/null +++ b/tests/private/package/InvokeNovaPackageWorkflow.TestSupport.ps1 @@ -0,0 +1,18 @@ +function Invoke-NovaBuildValidation { + param($WorkflowContext) + + $script:validated = $true +} + +function Invoke-NovaPackageArtifactCreation { + param($WorkflowContext) + + return @([pscustomobject]@{PackagePath = '/p'}) +} + +function Write-Message { + param( + [string]$Text, + [string]$color + ) +} diff --git a/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 b/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 index acbdfa89..a1f078fe 100644 --- a/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 +++ b/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 @@ -1,29 +1,67 @@ BeforeAll { $projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) . (Join-Path $projectRoot 'src/private/package/InvokeNovaPackageWorkflow.ps1') - - function Invoke-NovaBuildValidation {param($WorkflowContext) $script:validated = $true} - function Invoke-NovaPackageArtifactCreation {param($WorkflowContext) return @([pscustomobject]@{PackagePath='/p'})} + . (Join-Path $PSScriptRoot 'InvokeNovaPackageWorkflow.TestSupport.ps1') } Describe 'Invoke-NovaPackageWorkflow' { BeforeEach { $script:validated = $false + Mock Write-Message {} + Mock Write-Progress {} } It 'validates the build and returns without creating artifacts when ShouldRun is false' { Mock Invoke-NovaPackageArtifactCreation {} - $result = Invoke-NovaPackageWorkflow -WorkflowContext ([pscustomobject]@{}) + $result = Invoke-NovaPackageWorkflow -WorkflowContext ([pscustomobject]@{ + WorkflowParams = @{} + SkipTestsRequested = $false + ProjectInfo = [pscustomobject]@{ProjectName = 'Demo'} + Target = '/p/Demo.1.0.0.nupkg' + }) $script:validated | Should -BeTrue Should -Invoke Invoke-NovaPackageArtifactCreation -Times 0 + Assert-MockCalled Write-Progress -Times 2 $result | Should -BeNullOrEmpty } - It 'creates artifacts when ShouldRun is set' { - $artifacts = @([pscustomobject]@{PackagePath='/p'}) + It 'creates artifacts, reports progress, and prints the next suggested step when ShouldRun is set' { + $artifacts = @([pscustomobject]@{PackagePath='/p/Demo.1.0.0.nupkg'; OutputDirectory='/p'}) Mock Invoke-NovaPackageArtifactCreation {return $artifacts} - $result = Invoke-NovaPackageWorkflow -WorkflowContext ([pscustomobject]@{}) -ShouldRun + $result = Invoke-NovaPackageWorkflow -WorkflowContext ([pscustomobject]@{ + WorkflowParams = @{} + SkipTestsRequested = $false + ProjectInfo = [pscustomobject]@{ProjectName = 'Demo'} + Target = '/p/Demo.1.0.0.nupkg' + }) -ShouldRun Should -Invoke Invoke-NovaPackageArtifactCreation -Times 1 + Assert-MockCalled Write-Progress -Times 3 + Assert-MockCalled Write-Message -Times 3 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Created 1 package artifact for Demo' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Deploy-NovaPackage' + } @($result).Count | Should -Be 1 } + + It 'writes a package plan summary in WhatIf mode' { + $result = Invoke-NovaPackageWorkflow -WorkflowContext ([pscustomobject]@{ + WorkflowParams = @{WhatIf = $true} + SkipTestsRequested = $true + ProjectInfo = [pscustomobject]@{ProjectName = 'Demo'} + Target = '/p/Demo.1.0.0.nupkg' + }) + + $script:validated | Should -BeTrue + Assert-MockCalled Write-Message -Times 3 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Package plan ready for Demo' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Run New-NovaModulePackage without -WhatIf when you are ready to create the package artifacts.' + } + $result | Should -BeNullOrEmpty + } } From ea1763cd9de22072fd9f984d248606a926506621 Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 15:38:31 +0200 Subject: [PATCH 11/16] #215 feat: enhance Publish-NovaModule with progress reporting and next-step guidance refactor(Publish-NovaModule): align with terminal-ux-design principles Fixes #225 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../en-US/Publish-NovaModule.md | 8 +- .../release/InvokeNovaPublishWorkflow.ps1 | 162 ++++++++++++++++-- .../InvokeNovaPublishWorkflow.TestSupport.ps1 | 27 +++ .../InvokeNovaPublishWorkflow.Tests.ps1 | 87 +++++++++- 6 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 tests/private/release/InvokeNovaPublishWorkflow.TestSupport.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a04b3108..5d50dd2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated PlatyPS help topic for the PowerShell wrapper surface. - `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. - `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. +- `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 446783f8..9db7729c 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -23,6 +23,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Invoke-NovaCli` now treats blank command input as root help, reports unknown commands with clearer recovery guidance, and ships a dedicated help topic for the PowerShell wrapper surface. - `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. - `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. +- `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Publish-NovaModule.md b/docs/NovaModuleTools/en-US/Publish-NovaModule.md index a1ae7834..2fd8e6c9 100644 --- a/docs/NovaModuleTools/en-US/Publish-NovaModule.md +++ b/docs/NovaModuleTools/en-US/Publish-NovaModule.md @@ -45,10 +45,12 @@ Use `-ContinuousIntegration` when the same CI/self-hosting session should switch Use `-OverrideWarning` only when you intentionally want the nested build to continue even though a file under `src/public` contains zero or multiple top-level functions. -This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` to preview the resolved publish target and workflow without building, testing, or publishing. +This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` when you want the normal build-validation flow to run but the nested publish step to stay in preview mode. Use `-Confirm` when you want PowerShell to prompt before the publish workflow starts. +During a publish run, Nova shows progress for the build-validation phase, the publish phase, the local import phase when applicable, and the CI restore phase when `-ContinuousIntegration` is used. When the run completes, Nova prints the publish target together with the next suggested verification step. In `-WhatIf` mode, Nova ends with a publish-plan summary instead of a publish-completed summary. + ## EXAMPLES ### EXAMPLE 1 @@ -89,7 +91,7 @@ Prompts before the repository publish workflow starts. PS> Publish-NovaModule -Local -WhatIf ``` -Previews the local publish workflow and target directory without making changes. No module copy or import happens when `-WhatIf` is used. +Runs the normal build-validation flow, then previews the local publish target directory without copying or importing the module. ### EXAMPLE 6 @@ -300,6 +302,8 @@ When `-ContinuousIntegration` is used, Nova restores the built `dist/` module af `Publish-NovaModule` uses `SupportsShouldProcess`, so `Get-Help Publish-NovaModule -Full` should surface native `-WhatIf` and `-Confirm` support. +Use `Ctrl+C` if you need to stop a running publish workflow before the publish step finishes. + ## RELATED LINKS - [Invoke-NovaBuild](./Invoke-NovaBuild.md) diff --git a/src/private/release/InvokeNovaPublishWorkflow.ps1 b/src/private/release/InvokeNovaPublishWorkflow.ps1 index c92343b3..97f34dc1 100644 --- a/src/private/release/InvokeNovaPublishWorkflow.ps1 +++ b/src/private/release/InvokeNovaPublishWorkflow.ps1 @@ -8,6 +8,15 @@ function Test-NovaPublishWorkflowShouldImportLocalModule { return $ShouldRun -and $null -ne $WorkflowContext.LocalPublishActivation } +function Test-NovaPublishWorkflowWhatIfEnabled { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + return $WorkflowContext.WorkflowParams.ContainsKey('WhatIf') -and $WorkflowContext.WorkflowParams.WhatIf +} + function Invoke-NovaPublishWorkflowCiRestore { [CmdletBinding()] param( @@ -22,6 +31,19 @@ function Invoke-NovaPublishWorkflowCiRestore { } } +function Invoke-NovaPublishWorkflowStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + function Invoke-NovaPublishWorkflow { [CmdletBinding()] param( @@ -32,20 +54,140 @@ function Invoke-NovaPublishWorkflow { $publishParams = $WorkflowContext.PublishParams $continuousIntegrationRequested = ($WorkflowContext.PSObject.Properties.Name -contains 'ContinuousIntegrationRequested') -and $WorkflowContext.ContinuousIntegrationRequested $shouldImportPublishedLocalModule = Test-NovaPublishWorkflowShouldImportLocalModule -WorkflowContext $WorkflowContext -ShouldRun:$ShouldRun - $ciRestoreAction = ${function:Invoke-NovaPublishWorkflowCiRestore} - $importBuiltModuleForCiAction = ${function:Import-NovaBuiltModuleForCi} + $whatIfEnabled = Test-NovaPublishWorkflowWhatIfEnabled -WorkflowContext $WorkflowContext + $progressActivity = 'Running Nova publish workflow' + $importedLocalModule = $false + $restoredBuiltModule = $false + + try { + Invoke-NovaPublishWorkflowStep -Activity $progressActivity -Status (Get-NovaPublishWorkflowValidationStatus -WorkflowContext $WorkflowContext) -PercentComplete 35 -Action { + Invoke-NovaBuildValidation -WorkflowContext $WorkflowContext + } + + Invoke-NovaPublishWorkflowStep -Activity $progressActivity -Status (Get-NovaPublishWorkflowPublishStatus -WorkflowContext $WorkflowContext -WhatIfEnabled:$whatIfEnabled) -PercentComplete 75 -Action { + & $WorkflowContext.PublishInvocation.Action @publishParams + } + + if ($shouldImportPublishedLocalModule) { + Invoke-NovaPublishWorkflowStep -Activity $progressActivity -Status 'Importing the published local module' -PercentComplete 90 -Action { + $null = & $WorkflowContext.LocalPublishActivation.ImportAction -ProjectName $WorkflowContext.PublishInvocation.Parameters.ProjectInfo.ProjectName -ManifestPath $WorkflowContext.LocalPublishActivation.ManifestPath + } + $importedLocalModule = $true + Write-Verbose "Module copy to local path complete and imported from $( $WorkflowContext.LocalPublishActivation.ManifestPath )" + } + + if ($ShouldRun -and $continuousIntegrationRequested) { + Invoke-NovaPublishWorkflowStep -Activity $progressActivity -Status 'Refreshing the current session with the built module' -PercentComplete 98 -Action { + $null = Import-NovaBuiltModuleForCi -ProjectInfo $WorkflowContext.ProjectInfo + } + $restoredBuiltModule = $true + } + } finally { + Write-Progress -Activity $progressActivity -Completed + } - Invoke-NovaBuildValidation -WorkflowContext $WorkflowContext + Write-NovaPublishWorkflowResult -WorkflowContext $WorkflowContext -WhatIfEnabled:$whatIfEnabled -ImportedLocalModule:$importedLocalModule -RestoredBuiltModule:$restoredBuiltModule +} - & $WorkflowContext.PublishInvocation.Action @publishParams +function Get-NovaPublishWorkflowValidationStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) - if (-not $shouldImportPublishedLocalModule) { - & $ciRestoreAction -WorkflowContext $WorkflowContext -ShouldRun:$ShouldRun -ContinuousIntegrationRequested:$continuousIntegrationRequested -ImportBuiltModuleForCiAction $importBuiltModuleForCiAction - return + if ($WorkflowContext.SkipTestsRequested) { + return 'Building publish output with tests skipped' } - $null = & $WorkflowContext.LocalPublishActivation.ImportAction -ProjectName $WorkflowContext.PublishInvocation.Parameters.ProjectInfo.ProjectName -ManifestPath $WorkflowContext.LocalPublishActivation.ManifestPath - Write-Verbose "Module copy to local path complete and imported from $( $WorkflowContext.LocalPublishActivation.ManifestPath )" + return 'Building and testing publish output' +} - & $ciRestoreAction -WorkflowContext $WorkflowContext -ShouldRun:$ShouldRun -ContinuousIntegrationRequested:$continuousIntegrationRequested -ImportBuiltModuleForCiAction $importBuiltModuleForCiAction +function Get-NovaPublishWorkflowPublishStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + $targetDescription = if ($WorkflowContext.PublishInvocation.IsLocal) { + 'the local module path' + } else { + "repository $( $WorkflowContext.PublishInvocation.Target )" + } + + if ($WhatIfEnabled) { + return "Previewing publish to $targetDescription" + } + + return "Publishing to $targetDescription" +} + +function Write-NovaPublishWorkflowResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled, + [switch]$ImportedLocalModule, + [switch]$RestoredBuiltModule + ) + + Write-Message (Get-NovaPublishWorkflowStatusMessage -WorkflowContext $WorkflowContext -WhatIfEnabled:$WhatIfEnabled) -color Green + Write-Message "Publish target: $( $WorkflowContext.PublishInvocation.Target )" + + if ($WorkflowContext.SkipTestsRequested) { + Write-Message 'Pre-publish tests were skipped for this run.' + } + + if ($ImportedLocalModule) { + Write-Message "The published local module is loaded from $( $WorkflowContext.LocalPublishActivation.ManifestPath )." + } + + if ($RestoredBuiltModule) { + Write-Message 'The freshly built dist module is loaded again for later commands in this session.' + } + + foreach ($line in (Get-NovaPublishWorkflowNextStepLine -WorkflowContext $WorkflowContext -WhatIfEnabled:$WhatIfEnabled)) { + Write-Message $line + } +} + +function Get-NovaPublishWorkflowStatusMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return "Publish plan ready for $( $WorkflowContext.ProjectInfo.ProjectName )" + } + + return "Published Nova module: $( $WorkflowContext.ProjectInfo.ProjectName )" +} + +function Get-NovaPublishWorkflowNextStepLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return @( + 'Next step:' + 'Run Publish-NovaModule without -WhatIf when you are ready to publish the module.' + ) + } + + if ($WorkflowContext.PublishInvocation.IsLocal) { + return @( + 'Next step:' + 'Get-NovaProjectInfo -Installed' + ) + } + + return @( + 'Next step:' + "Find-Module $( $WorkflowContext.ProjectInfo.ProjectName ) -Repository $( $WorkflowContext.PublishInvocation.Target )" + ) } diff --git a/tests/private/release/InvokeNovaPublishWorkflow.TestSupport.ps1 b/tests/private/release/InvokeNovaPublishWorkflow.TestSupport.ps1 new file mode 100644 index 00000000..a7d9db18 --- /dev/null +++ b/tests/private/release/InvokeNovaPublishWorkflow.TestSupport.ps1 @@ -0,0 +1,27 @@ +function Import-NovaBuiltModuleForCi { + param($ProjectInfo) + + $script:ciImportCalls += 1 +} + +function Invoke-NovaBuildValidation { + param($WorkflowContext) + + $script:validationCalls += 1 +} + +function Write-Message { + param( + [string]$Text, + [string]$color + ) +} + +function Write-Progress { + param( + [string]$Activity, + [string]$Status, + [int]$PercentComplete, + [switch]$Completed + ) +} diff --git a/tests/private/release/InvokeNovaPublishWorkflow.Tests.ps1 b/tests/private/release/InvokeNovaPublishWorkflow.Tests.ps1 index 5957439f..59281dfb 100644 --- a/tests/private/release/InvokeNovaPublishWorkflow.Tests.ps1 +++ b/tests/private/release/InvokeNovaPublishWorkflow.Tests.ps1 @@ -1,8 +1,7 @@ BeforeAll { $projectRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) . (Join-Path $projectRoot 'src/private/release/InvokeNovaPublishWorkflow.ps1') - function Import-NovaBuiltModuleForCi {param($ProjectInfo) $script:ciImportCalls += 1} - function Invoke-NovaBuildValidation {param($WorkflowContext) $script:validationCalls += 1} + . (Join-Path $PSScriptRoot 'InvokeNovaPublishWorkflow.TestSupport.ps1') } Describe 'Test-NovaPublishWorkflowShouldImportLocalModule' { @@ -20,6 +19,18 @@ Describe 'Test-NovaPublishWorkflowShouldImportLocalModule' { } } +Describe 'Test-NovaPublishWorkflowWhatIfEnabled' { + It 'returns true when WorkflowParams.WhatIf is enabled' { + $ctx = [pscustomobject]@{WorkflowParams = @{WhatIf = $true}} + Test-NovaPublishWorkflowWhatIfEnabled -WorkflowContext $ctx | Should -BeTrue + } + + It 'returns false when WorkflowParams.WhatIf is not enabled' { + $ctx = [pscustomobject]@{WorkflowParams = @{}} + Test-NovaPublishWorkflowWhatIfEnabled -WorkflowContext $ctx | Should -BeFalse + } +} + Describe 'Invoke-NovaPublishWorkflowCiRestore' { It 'invokes the importer when ShouldRun and CI requested' { $script:calls = 0 @@ -41,39 +52,99 @@ Describe 'Invoke-NovaPublishWorkflow' { $script:publishCalls = 0 $script:localImportCalls = 0 $script:ciImportCalls = 0 + Mock Write-Message {} + Mock Write-Progress {} } - It 'runs validation and publish; skips local import when activation is null' { + It 'runs validation and publish, reports progress, and suggests how to verify repository publish' { Mock Invoke-NovaBuildValidation {$script:validationCalls += 1} $ctx = [pscustomobject]@{ PublishParams = @{} - PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}} + PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}; Target = 'PSGallery'; IsLocal = $false} LocalPublishActivation = $null - ProjectInfo = [pscustomobject]@{} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'} + WorkflowParams = @{} + SkipTestsRequested = $false ContinuousIntegrationRequested = $false } Invoke-NovaPublishWorkflow -WorkflowContext $ctx -ShouldRun $script:validationCalls | Should -Be 1 $script:publishCalls | Should -Be 1 $script:localImportCalls | Should -Be 0 + Assert-MockCalled Write-Progress -Times 3 + Assert-MockCalled Write-Message -Times 4 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Published Nova module: NovaModuleTools' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Find-Module NovaModuleTools -Repository PSGallery' + } } - It 'imports local published module when LocalPublishActivation is set' { + It 'imports the local published module, restores the built module in CI, and reports the result' { Mock Invoke-NovaBuildValidation {$script:validationCalls += 1} $ctx = [pscustomobject]@{ PublishParams = @{} PublishInvocation = [pscustomobject]@{ Action = {param() $script:publishCalls += 1} + IsLocal = $true + Target = '/modules' Parameters = [pscustomobject]@{ProjectInfo = [pscustomobject]@{ProjectName='Mod'}} } LocalPublishActivation = [pscustomobject]@{ ManifestPath='/m/Mod.psd1' ImportAction = {param($ProjectName,$ManifestPath) $script:localImportCalls += 1} } - ProjectInfo = [pscustomobject]@{} - ContinuousIntegrationRequested = $false + ProjectInfo = [pscustomobject]@{ProjectName = 'Mod'} + WorkflowParams = @{} + SkipTestsRequested = $true + ContinuousIntegrationRequested = $true } Invoke-NovaPublishWorkflow -WorkflowContext $ctx -ShouldRun $script:localImportCalls | Should -Be 1 + $script:ciImportCalls | Should -Be 1 + Assert-MockCalled Write-Progress -Times 5 + Assert-MockCalled Write-Message -Times 6 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Pre-publish tests were skipped for this run.' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'The published local module is loaded from /m/Mod.psd1.' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'The freshly built dist module is loaded again for later commands in this session.' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Get-NovaProjectInfo -Installed' + } + } + + It 'writes a publish plan summary in WhatIf mode without importing or restoring modules' { + Mock Invoke-NovaBuildValidation {$script:validationCalls += 1} + $ctx = [pscustomobject]@{ + PublishParams = @{WhatIf = $true} + PublishInvocation = [pscustomobject]@{Action = {param() $script:publishCalls += 1}; Target = 'PSGallery'; IsLocal = $false} + LocalPublishActivation = [pscustomobject]@{ + ManifestPath='/m/Mod.psd1' + ImportAction = {param($ProjectName,$ManifestPath) $script:localImportCalls += 1} + } + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'} + WorkflowParams = @{WhatIf = $true} + SkipTestsRequested = $false + ContinuousIntegrationRequested = $true + } + + Invoke-NovaPublishWorkflow -WorkflowContext $ctx + + $script:publishCalls | Should -Be 1 + $script:localImportCalls | Should -Be 0 + $script:ciImportCalls | Should -Be 0 + Assert-MockCalled Write-Message -Times 4 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Publish plan ready for NovaModuleTools' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Run Publish-NovaModule without -WhatIf when you are ready to publish the module.' + } } } From 6d1b5fd583406b3c10045f0447cc59df1633608c Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 15:45:21 +0200 Subject: [PATCH 12/16] #215 feat: enhance Set-NovaUpdateNotificationPreference with WhatIf support and improved messaging refactor(Set-NovaUpdateNotificationPreference): align with terminal-ux-design principles Fixes #226 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../Set-NovaUpdateNotificationPreference.md | 10 +- ...ateNotificationPreferenceChangeContext.ps1 | 11 +- ...NovaUpdateNotificationPreferenceChange.ps1 | 130 +++++++++++++++++- .../SetNovaUpdateNotificationPreference.ps1 | 7 +- ...ificationPreferenceChangeContext.Tests.ps1 | 10 +- ...dateNotificationPreferenceChange.Tests.ps1 | 58 +++++++- ...dateNotificationPreference.TestSupport.ps1 | 30 ++++ ...NovaUpdateNotificationPreference.Tests.ps1 | 19 +-- 10 files changed, 247 insertions(+), 30 deletions(-) create mode 100644 tests/public/SetNovaUpdateNotificationPreference.TestSupport.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d50dd2d..2d366646 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. - `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. - `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. +- `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 9db7729c..8a15d52a 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -24,6 +24,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Invoke-NovaRelease` now shows progress for the main release phases, ends with the publish target plus a next-step hint, and uses a plan summary instead of a success summary when `-WhatIf` previews the release. - `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. - `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. +- `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md b/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md index 6c09762f..be8b3ee7 100644 --- a/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md +++ b/docs/NovaModuleTools/en-US/Set-NovaUpdateNotificationPreference.md @@ -38,6 +38,8 @@ The same stored preference is also used by `Update-NovaModuleTool` (alias: `Upda Stable self-updates remain available and do not require prerelease eligibility. +When the preference changes, Nova prints the settings file path together with the next recommended verification command. In `-WhatIf` mode, Nova ends with a preview summary instead of writing the preference file. + ## EXAMPLES ### EXAMPLE 1 @@ -46,7 +48,7 @@ Stable self-updates remain available and do not require prerelease eligibility. PS> Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications ``` -Turns off prerelease self-update eligibility and restricts `Update-NovaModuleTool` to stable releases only. +Turns off prerelease self-update eligibility, keeps stable releases available, and suggests `Get-NovaUpdateNotificationPreference` as the next verification step. ### EXAMPLE 2 @@ -55,7 +57,7 @@ PS> Set-NovaUpdateNotificationPreference -EnablePrereleaseNotifications ``` Turns prerelease self-update eligibility back on, which allows `Update-NovaModuleTool` / -`Update-NovaModuleTools` to consider a prerelease target again. +`Update-NovaModuleTools` to consider a prerelease target again and suggests `Get-NovaUpdateNotificationPreference` as the next verification step. ### EXAMPLE 3 @@ -63,7 +65,7 @@ Turns prerelease self-update eligibility back on, which allows `Update-NovaModul PS> Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications -WhatIf ``` -Previews the change that would disable prerelease self-update eligibility. +Previews the change that would disable prerelease self-update eligibility without writing the settings file. ### EXAMPLE 4 @@ -195,6 +197,8 @@ Returns the current prerelease self-update state, the always-available stable-up Use this command together with `Get-NovaUpdateNotificationPreference` when you want to confirm the stored setting. +Use `-WhatIf` or `-Confirm` when you want an easy way to preview or stop the change before the settings file is updated. + ## RELATED LINKS - [Get-NovaUpdateNotificationPreference](./Get-NovaUpdateNotificationPreference.md) diff --git a/src/private/update/GetNovaUpdateNotificationPreferenceChangeContext.ps1 b/src/private/update/GetNovaUpdateNotificationPreferenceChangeContext.ps1 index 6bbbf5be..a9926161 100644 --- a/src/private/update/GetNovaUpdateNotificationPreferenceChangeContext.ps1 +++ b/src/private/update/GetNovaUpdateNotificationPreferenceChangeContext.ps1 @@ -2,14 +2,16 @@ function Get-NovaUpdateNotificationPreferenceChangeContext { [CmdletBinding()] param( [switch]$EnablePrereleaseNotifications, - [switch]$DisablePrereleaseNotifications + [switch]$DisablePrereleaseNotifications, + [hashtable]$WorkflowParams = @{} ) if ($EnablePrereleaseNotifications.IsPresent) { return [pscustomobject]@{ PrereleaseNotificationsEnabled = $true Target = Get-NovaUpdateSettingsFilePath - Action = 'Enable prerelease update notifications' + Action = 'Enable prerelease self-update notifications' + WorkflowParams = $WorkflowParams } } @@ -17,9 +19,10 @@ function Get-NovaUpdateNotificationPreferenceChangeContext { return [pscustomobject]@{ PrereleaseNotificationsEnabled = $false Target = Get-NovaUpdateSettingsFilePath - Action = 'Disable prerelease update notifications' + Action = 'Disable prerelease self-update notifications' + WorkflowParams = $WorkflowParams } } - Stop-NovaOperation -Message 'Specify either -EnablePrereleaseNotifications or -DisablePrereleaseNotifications.' -ErrorId 'Nova.Validation.UpdateNotificationPreferenceChangeRequired' -Category InvalidArgument -TargetObject 'PrereleaseNotifications' + Stop-NovaOperation -Message 'Specify either -EnablePrereleaseNotifications or -DisablePrereleaseNotifications. Example: Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications' -ErrorId 'Nova.Validation.UpdateNotificationPreferenceChangeRequired' -Category InvalidArgument -TargetObject 'PrereleaseNotifications' } diff --git a/src/private/update/InvokeNovaUpdateNotificationPreferenceChange.ps1 b/src/private/update/InvokeNovaUpdateNotificationPreferenceChange.ps1 index 7199b65e..afe410e7 100644 --- a/src/private/update/InvokeNovaUpdateNotificationPreferenceChange.ps1 +++ b/src/private/update/InvokeNovaUpdateNotificationPreferenceChange.ps1 @@ -1,9 +1,135 @@ function Invoke-NovaUpdateNotificationPreferenceChange { [CmdletBinding()] param( - [Parameter(Mandatory)][pscustomobject]$WorkflowContext + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$ShouldRun ) + $whatIfEnabled = Test-NovaUpdateNotificationPreferenceChangeWhatIfEnabled -WorkflowContext $WorkflowContext + if (-not $ShouldRun) { + if ($whatIfEnabled) { + Write-NovaUpdateNotificationPreferenceChangeResult -WorkflowContext $WorkflowContext -WhatIfEnabled + } + + return + } + Write-NovaUpdateNotificationPreference -PrereleaseNotificationsEnabled:$WorkflowContext.PrereleaseNotificationsEnabled - return Get-NovaUpdateNotificationPreferenceStatus + $status = Get-NovaUpdateNotificationPreferenceStatus + Write-NovaUpdateNotificationPreferenceChangeResult -WorkflowContext $WorkflowContext -Status $status + return $status +} + +function Test-NovaUpdateNotificationPreferenceChangeWhatIfEnabled { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + return $WorkflowContext.WorkflowParams.ContainsKey('WhatIf') -and $WorkflowContext.WorkflowParams.WhatIf +} + +function Write-NovaUpdateNotificationPreferenceChangeResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [AllowNull()][pscustomobject]$Status, + [switch]$WhatIfEnabled + ) + + Write-Message (Get-NovaUpdateNotificationPreferenceChangeStatusMessage -WorkflowContext $WorkflowContext -WhatIfEnabled:$WhatIfEnabled) -color Green + Write-Message "Settings file: $( Get-NovaUpdateNotificationPreferenceChangeSettingsPath -WorkflowContext $WorkflowContext -Status $Status )" + Write-Message (Get-NovaUpdateNotificationPreferenceChangeAvailabilityMessage -WhatIfEnabled:$WhatIfEnabled) + + foreach ($line in (Get-NovaUpdateNotificationPreferenceChangeNextStepLine -WorkflowContext $WorkflowContext -WhatIfEnabled:$WhatIfEnabled)) { + Write-Message $line + } +} + +function Get-NovaUpdateNotificationPreferenceChangeStatusMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + $stateText = Get-NovaUpdateNotificationPreferenceChangeStateText -PrereleaseNotificationsEnabled:$WorkflowContext.PrereleaseNotificationsEnabled + if ($WhatIfEnabled) { + return "Notification preference plan ready: prerelease self-updates $stateText" + } + + return "Prerelease self-updates are now $stateText." +} + +function Get-NovaUpdateNotificationPreferenceChangeSettingsPath { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [AllowNull()][pscustomobject]$Status + ) + + if ($null -ne $Status -and ($Status.PSObject.Properties.Name -contains 'SettingsPath')) { + return $Status.SettingsPath + } + + return $WorkflowContext.Target +} + +function Get-NovaUpdateNotificationPreferenceChangeAvailabilityMessage { + [CmdletBinding()] + param( + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return 'Stable self-updates would remain available.' + } + + return 'Stable self-updates remain available.' +} + +function Get-NovaUpdateNotificationPreferenceChangeNextStepLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return @( + 'Next step:' + "Run $( Get-NovaUpdateNotificationPreferenceChangeCommandLine -WorkflowContext $WorkflowContext ) without -WhatIf when you are ready to store the preference." + ) + } + + return @( + 'Next step:' + 'Get-NovaUpdateNotificationPreference' + ) +} + +function Get-NovaUpdateNotificationPreferenceChangeCommandLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + if ($WorkflowContext.PrereleaseNotificationsEnabled) { + return 'Set-NovaUpdateNotificationPreference -EnablePrereleaseNotifications' + } + + return 'Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications' +} + +function Get-NovaUpdateNotificationPreferenceChangeStateText { + [CmdletBinding()] + param( + [Parameter(Mandatory)][bool]$PrereleaseNotificationsEnabled + ) + + if ($PrereleaseNotificationsEnabled) { + return 'enabled' + } + + return 'disabled' } diff --git a/src/public/SetNovaUpdateNotificationPreference.ps1 b/src/public/SetNovaUpdateNotificationPreference.ps1 index 6a178a08..998b40a3 100644 --- a/src/public/SetNovaUpdateNotificationPreference.ps1 +++ b/src/public/SetNovaUpdateNotificationPreference.ps1 @@ -8,11 +8,12 @@ function Set-NovaUpdateNotificationPreference { [switch]$DisablePrereleaseNotifications ) - $workflowContext = Get-NovaUpdateNotificationPreferenceChangeContext -EnablePrereleaseNotifications:$EnablePrereleaseNotifications -DisablePrereleaseNotifications:$DisablePrereleaseNotifications + $workflowContext = Get-NovaUpdateNotificationPreferenceChangeContext -EnablePrereleaseNotifications:$EnablePrereleaseNotifications -DisablePrereleaseNotifications:$DisablePrereleaseNotifications -WorkflowParams @{WhatIf = [bool]$WhatIfPreference} - if (-not $PSCmdlet.ShouldProcess($workflowContext.Target, $workflowContext.Action)) { + $shouldRun = $PSCmdlet.ShouldProcess($workflowContext.Target, $workflowContext.Action) + if (-not $shouldRun -and -not $WhatIfPreference) { return } - return Invoke-NovaUpdateNotificationPreferenceChange -WorkflowContext $workflowContext + return Invoke-NovaUpdateNotificationPreferenceChange -WorkflowContext $workflowContext -ShouldRun:$shouldRun } diff --git a/tests/private/update/GetNovaUpdateNotificationPreferenceChangeContext.Tests.ps1 b/tests/private/update/GetNovaUpdateNotificationPreferenceChangeContext.Tests.ps1 index 4657f2c8..2a3d4dde 100644 --- a/tests/private/update/GetNovaUpdateNotificationPreferenceChangeContext.Tests.ps1 +++ b/tests/private/update/GetNovaUpdateNotificationPreferenceChangeContext.Tests.ps1 @@ -16,21 +16,23 @@ Describe 'Get-NovaUpdateNotificationPreferenceChangeContext' { } It 'returns an enable context when -EnablePrereleaseNotifications is set' { - $context = Get-NovaUpdateNotificationPreferenceChangeContext -EnablePrereleaseNotifications + $context = Get-NovaUpdateNotificationPreferenceChangeContext -EnablePrereleaseNotifications -WorkflowParams @{WhatIf = $true} $context.PrereleaseNotificationsEnabled | Should -BeTrue - $context.Action | Should -Be 'Enable prerelease update notifications' + $context.Action | Should -Be 'Enable prerelease self-update notifications' $context.Target | Should -Not -BeNullOrEmpty + $context.WorkflowParams | Should -BeOfType Hashtable + $context.WorkflowParams.WhatIf | Should -BeTrue } It 'returns a disable context when -DisablePrereleaseNotifications is set' { $context = Get-NovaUpdateNotificationPreferenceChangeContext -DisablePrereleaseNotifications $context.PrereleaseNotificationsEnabled | Should -BeFalse - $context.Action | Should -Be 'Disable prerelease update notifications' + $context.Action | Should -Be 'Disable prerelease self-update notifications' } It 'throws via Stop-NovaOperation when neither switch is provided' { - {Get-NovaUpdateNotificationPreferenceChangeContext} | Should -Throw '*Specify either*' + {Get-NovaUpdateNotificationPreferenceChangeContext} | Should -Throw '*Example: Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications*' } } diff --git a/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 b/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 index e56a9665..ab35dee0 100644 --- a/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 +++ b/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 @@ -4,20 +4,74 @@ BeforeAll { function Write-NovaUpdateNotificationPreference {param([bool]$PrereleaseNotificationsEnabled)} function Get-NovaUpdateNotificationPreferenceStatus {return [pscustomobject]@{PrereleaseNotificationsEnabled = $true; SettingsPath = '/tmp/x.json'}} + function Write-Message {param([string]$Text, [string]$color)} +} + +Describe 'Test-NovaUpdateNotificationPreferenceChangeWhatIfEnabled' { + It 'returns true when WorkflowParams.WhatIf is enabled' { + $workflowContext = [pscustomobject]@{WorkflowParams = @{WhatIf = $true}} + Test-NovaUpdateNotificationPreferenceChangeWhatIfEnabled -WorkflowContext $workflowContext | Should -BeTrue + } + + It 'returns false when WorkflowParams.WhatIf is not enabled' { + $workflowContext = [pscustomobject]@{WorkflowParams = @{}} + Test-NovaUpdateNotificationPreferenceChangeWhatIfEnabled -WorkflowContext $workflowContext | Should -BeFalse + } } Describe 'Invoke-NovaUpdateNotificationPreferenceChange' { + BeforeEach { + Mock Write-Message {} + } + It 'writes the requested preference and returns the resulting status' { Mock Write-NovaUpdateNotificationPreference {} Mock Get-NovaUpdateNotificationPreferenceStatus {return [pscustomobject]@{PrereleaseNotificationsEnabled = $false; SettingsPath = '/tmp/x.json'}} - $workflowContext = [pscustomobject]@{PrereleaseNotificationsEnabled = $false; Action = 'Disable prerelease update notifications'; Target = '/tmp/x.json'} - $status = Invoke-NovaUpdateNotificationPreferenceChange -WorkflowContext $workflowContext + $workflowContext = [pscustomobject]@{ + PrereleaseNotificationsEnabled = $false + Action = 'Disable prerelease self-update notifications' + Target = '/tmp/x.json' + WorkflowParams = @{} + } + $status = Invoke-NovaUpdateNotificationPreferenceChange -WorkflowContext $workflowContext -ShouldRun Assert-MockCalled Write-NovaUpdateNotificationPreference -Times 1 -ParameterFilter { $PrereleaseNotificationsEnabled -eq $false } Assert-MockCalled Get-NovaUpdateNotificationPreferenceStatus -Times 1 + Assert-MockCalled Write-Message -Times 4 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Prerelease self-updates are now disabled.' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Get-NovaUpdateNotificationPreference' + } $status.PrereleaseNotificationsEnabled | Should -BeFalse } + + It 'writes a preview summary in WhatIf mode without storing the preference' { + Mock Write-NovaUpdateNotificationPreference {} + Mock Get-NovaUpdateNotificationPreferenceStatus {} + + $workflowContext = [pscustomobject]@{ + PrereleaseNotificationsEnabled = $true + Action = 'Enable prerelease self-update notifications' + Target = '/tmp/x.json' + WorkflowParams = @{WhatIf = $true} + } + + $status = Invoke-NovaUpdateNotificationPreferenceChange -WorkflowContext $workflowContext + + Assert-MockCalled Write-NovaUpdateNotificationPreference -Times 0 + Assert-MockCalled Get-NovaUpdateNotificationPreferenceStatus -Times 0 + Assert-MockCalled Write-Message -Times 4 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Notification preference plan ready: prerelease self-updates enabled' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Run Set-NovaUpdateNotificationPreference -EnablePrereleaseNotifications without -WhatIf when you are ready to store the preference.' + } + $status | Should -BeNullOrEmpty + } } diff --git a/tests/public/SetNovaUpdateNotificationPreference.TestSupport.ps1 b/tests/public/SetNovaUpdateNotificationPreference.TestSupport.ps1 new file mode 100644 index 00000000..75c295a6 --- /dev/null +++ b/tests/public/SetNovaUpdateNotificationPreference.TestSupport.ps1 @@ -0,0 +1,30 @@ +function Get-NovaUpdateNotificationPreferenceChangeContext { + param( + [switch]$EnablePrereleaseNotifications, + [switch]$DisablePrereleaseNotifications, + [hashtable]$WorkflowParams + ) + + $script:ctxArgs = @{ + Enable = [bool]$EnablePrereleaseNotifications + Disable = [bool]$DisablePrereleaseNotifications + WhatIf = $WorkflowParams.WhatIf + } + + return [pscustomobject]@{ + Target = 'nm' + Action = 'Set' + WorkflowParams = $WorkflowParams + } +} + +function Invoke-NovaUpdateNotificationPreferenceChange { + param( + $WorkflowContext, + [switch]$ShouldRun + ) + + $script:invoked = $true + $script:shouldRun = [bool]$ShouldRun + return [pscustomobject]@{Changed = $true} +} diff --git a/tests/public/SetNovaUpdateNotificationPreference.Tests.ps1 b/tests/public/SetNovaUpdateNotificationPreference.Tests.ps1 index fc1c3248..32578473 100644 --- a/tests/public/SetNovaUpdateNotificationPreference.Tests.ps1 +++ b/tests/public/SetNovaUpdateNotificationPreference.Tests.ps1 @@ -1,24 +1,17 @@ BeforeAll { $projectRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) . (Join-Path $projectRoot 'src/public/SetNovaUpdateNotificationPreference.ps1') - - function Get-NovaUpdateNotificationPreferenceChangeContext {param([switch]$EnablePrereleaseNotifications, [switch]$DisablePrereleaseNotifications) - $script:ctxArgs = @{Enable=[bool]$EnablePrereleaseNotifications; Disable=[bool]$DisablePrereleaseNotifications} - return [pscustomobject]@{Target='nm'; Action='Set'} - } - function Invoke-NovaUpdateNotificationPreferenceChange {param($WorkflowContext) - $script:invoked = $true - return [pscustomobject]@{Changed=$true} - } + . (Join-Path $PSScriptRoot 'SetNovaUpdateNotificationPreference.TestSupport.ps1') } Describe 'Set-NovaUpdateNotificationPreference' { - BeforeEach {$script:ctxArgs = $null; $script:invoked = $false} + BeforeEach {$script:ctxArgs = $null; $script:invoked = $false; $script:shouldRun = $null} It 'forwards -EnablePrereleaseNotifications to the context and invokes the workflow' { $result = Set-NovaUpdateNotificationPreference -EnablePrereleaseNotifications $script:ctxArgs.Enable | Should -BeTrue $script:invoked | Should -BeTrue + $script:shouldRun | Should -BeTrue $result.Changed | Should -BeTrue } @@ -27,8 +20,10 @@ Describe 'Set-NovaUpdateNotificationPreference' { $script:ctxArgs.Disable | Should -BeTrue } - It 'returns without invoking the workflow when -WhatIf is set' { + It 'invokes the workflow with ShouldRun=$false when -WhatIf is set' { Set-NovaUpdateNotificationPreference -EnablePrereleaseNotifications -WhatIf - $script:invoked | Should -BeFalse + $script:invoked | Should -BeTrue + $script:shouldRun | Should -BeFalse + $script:ctxArgs.WhatIf | Should -BeTrue } } From 0903c7c1e51e384d261e977a2c0ad0b20c669349 Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 15:58:37 +0200 Subject: [PATCH 13/16] #215 feat: enhance Test-NovaBuild with progress reporting and actionable guidance in WhatIf mode refactor(Test-NovaBuild): align with terminal-ux-design principles Fixes #227 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + docs/NovaModuleTools/en-US/Test-NovaBuild.md | 12 +- .../quality/InvokeNovaTestWorkflow.ps1 | 215 +++++++++++++++++- .../InvokeNovaTestWorkflow.TestSupport.ps1 | 4 +- .../quality/InvokeNovaTestWorkflow.Tests.ps1 | 70 +++++- 6 files changed, 279 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d366646..07737623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. - `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. - `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. +- `Test-NovaBuild` now shows progress across the main test phases, ends with the result file path plus a coverage summary and next-step hint when tests pass, uses a test-plan summary in `-WhatIf` mode, and fails with more actionable guidance when Pester or coverage checks fail. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index 8a15d52a..c6cadf6f 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -25,6 +25,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `New-NovaModulePackage` now shows progress for build validation and artifact creation, ends with the package target plus the next suggested deployment step, and uses a package-plan summary in `-WhatIf` mode. - `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. - `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. +- `Test-NovaBuild` now shows progress across the main test phases, ends with the result file path plus a coverage summary and next-step hint when tests pass, uses a test-plan summary in `-WhatIf` mode, and fails with more actionable guidance when Pester or coverage checks fail. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Test-NovaBuild.md b/docs/NovaModuleTools/en-US/Test-NovaBuild.md index 26a5b9db..e2a51466 100644 --- a/docs/NovaModuleTools/en-US/Test-NovaBuild.md +++ b/docs/NovaModuleTools/en-US/Test-NovaBuild.md @@ -40,6 +40,8 @@ With the default This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` to preview the planned test run and XML output path without creating `artifacts/` or invoking Pester. +During a test run, Nova shows progress for the optional pre-test build, test-result preparation, the Pester run, result writing, and code-coverage validation. When tests pass, Nova prints the result file path, a coverage summary when one is available, and a suggested next step. In `-WhatIf` mode, Nova ends with a test-plan summary instead of invoking Pester. + ## EXAMPLES ### EXAMPLE 1 @@ -48,7 +50,7 @@ This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShoul PS> Test-NovaBuild ``` -Runs the Pester tests for the current project. +Runs the Pester tests for the current project and prints the result file path plus the next suggested command when the run succeeds. ### EXAMPLE 2 @@ -56,7 +58,7 @@ Runs the Pester tests for the current project. PS> Test-NovaBuild -Build ``` -Builds the project first, then runs the configured Pester test workflow. +Builds the project first, then runs the configured Pester test workflow with the same completion summary as a normal test run. ### EXAMPLE 3 @@ -88,7 +90,7 @@ Overrides the console output settings for the current test run while keeping col PS> Test-NovaBuild -WhatIf ``` -Previews the planned Pester run without executing tests or writing `artifacts/TestResults.xml`. +Previews the planned Pester run, prints the planned result file path, and does not execute tests or write `artifacts/TestResults.xml`. ### EXAMPLE 7 @@ -96,7 +98,7 @@ Previews the planned Pester run without executing tests or writing `artifacts/Te PS> Test-NovaBuild -Build -WhatIf ``` -Previews the build-before-test workflow without rebuilding the project or running Pester. +Previews the build-before-test workflow, including the planned result file path and configured coverage target, without rebuilding the project or running Pester. ## PARAMETERS @@ -258,6 +260,8 @@ If `project.json` configures `Pester.CodeCoverage.CoveragePercentTarget`, `Test- `Test-NovaBuild` uses `SupportsShouldProcess`, so `Get-Help Test-NovaBuild -Full` surfaces native `-WhatIf` and `-Confirm` support. +Use `Ctrl+C` if you need to stop a running test workflow before Pester completes. + ## RELATED LINKS - [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) diff --git a/src/private/quality/InvokeNovaTestWorkflow.ps1 b/src/private/quality/InvokeNovaTestWorkflow.ps1 index bc2ace3b..9e314500 100644 --- a/src/private/quality/InvokeNovaTestWorkflow.ps1 +++ b/src/private/quality/InvokeNovaTestWorkflow.ps1 @@ -5,34 +5,225 @@ function Invoke-NovaTestWorkflow { [switch]$ShouldRun ) - if (Test-NovaTestWorkflowBuildRequested -WorkflowContext $WorkflowContext) { - $workflowParams = Get-NovaBuildCommandParameterMap -WorkflowParams $WorkflowContext.WorkflowParams -OverrideWarningRequested:(($WorkflowContext.PSObject.Properties.Name -contains 'OverrideWarningRequested') -and $WorkflowContext.OverrideWarningRequested) - Invoke-NovaBuild @workflowParams + $progressActivity = 'Running Nova test workflow' + $whatIfEnabled = Test-NovaWhatIfWorkflowContext -WorkflowContext $WorkflowContext + $testResult = $null + $shouldRunWorkflow = Test-NovaTestWorkflowShouldRun -WorkflowContext $WorkflowContext -BoundParameters $PSBoundParameters -ShouldRun:$ShouldRun + + try { + Invoke-NovaTestWorkflowBuildStep -WorkflowContext $WorkflowContext -Activity $progressActivity -WhatIfEnabled:$whatIfEnabled + + if (-not $shouldRunWorkflow) { + Write-NovaTestWorkflowPreviewResult -WorkflowContext $WorkflowContext -WhatIfEnabled:$whatIfEnabled + return + } + + $testResult = Invoke-NovaTestWorkflowExecution -WorkflowContext $WorkflowContext -Activity $progressActivity + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + Write-NovaTestWorkflowResult -WorkflowContext $WorkflowContext -TestResult $testResult +} + +function Invoke-NovaTestWorkflowBuildStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [Parameter(Mandatory)][string]$Activity, + [switch]$WhatIfEnabled + ) + + if (-not (Test-NovaTestWorkflowBuildRequested -WorkflowContext $WorkflowContext)) { + return } - if (-not (Test-NovaTestWorkflowShouldRun -WorkflowContext $WorkflowContext -BoundParameters $PSBoundParameters -ShouldRun:$ShouldRun)) { + $buildCommandParameters = Get-NovaBuildCommandParameterMap -WorkflowParams (Get-NovaPropertyValue -InputObject $WorkflowContext -Name 'WorkflowParams') -OverrideWarningRequested:(($WorkflowContext.PSObject.Properties.Name -contains 'OverrideWarningRequested') -and $WorkflowContext.OverrideWarningRequested) + Invoke-NovaTestWorkflowStep -Activity $Activity -Status (Get-NovaTestWorkflowBuildStatus -WhatIfEnabled:$WhatIfEnabled) -PercentComplete 20 -Action { + Invoke-NovaBuild @buildCommandParameters + } +} + +function Write-NovaTestWorkflowPreviewResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + if (-not $WhatIfEnabled) { return } - Initialize-NovaPesterArtifactDirectory -WorkflowContext $WorkflowContext + Write-NovaTestWorkflowResult -WorkflowContext $WorkflowContext -WhatIfEnabled +} + +function Invoke-NovaTestWorkflowExecution { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [Parameter(Mandatory)][string]$Activity + ) + + Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Preparing the test result directory' -PercentComplete 40 -Action { + Initialize-NovaPesterArtifactDirectory -WorkflowContext $WorkflowContext + } + $WorkflowContext.PesterConfig.TestResult.OutputPath = $WorkflowContext.TestResultPath $coverageTargetAssertion = Get-NovaCoverageTargetAssertionScriptBlock -WorkflowContext $WorkflowContext + $testResult = Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Running Pester tests' -PercentComplete 70 -Action { + Invoke-NovaPesterWithSuppressedProgress -Configuration $WorkflowContext.PesterConfig + } + + Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Writing the test result report' -PercentComplete 85 -Action { + & $WorkflowContext.TestResultArtifactWriter.ScriptBlock -TestResult $testResult -OutputPath $WorkflowContext.TestResultPath -ReportWriter $WorkflowContext.TestResultReportWriter.ScriptBlock + } + + if ($testResult.Result -ne 'Passed') { + Stop-NovaOperation -Message (Get-NovaTestWorkflowFailureMessage -WorkflowContext $WorkflowContext) -ErrorId 'Nova.Workflow.TestRunFailed' -Category InvalidOperation -TargetObject $WorkflowContext.TestResultPath + } + + Invoke-NovaTestWorkflowStep -Activity $Activity -Status 'Checking the configured code coverage target' -PercentComplete 95 -Action { + & $coverageTargetAssertion -WorkflowContext $WorkflowContext -TestResult $testResult + } + + return $testResult +} + +function Invoke-NovaTestWorkflowStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + +function Get-NovaTestWorkflowBuildStatus { + [CmdletBinding()] + param( + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return 'Previewing the build-before-test workflow' + } + + return 'Building the current project state' +} + +function Invoke-NovaPesterWithSuppressedProgress { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Configuration + ) $previousProgressPreference = $global:ProgressPreference $global:ProgressPreference = 'SilentlyContinue' try { - $testResult = Invoke-NovaPester -Configuration $WorkflowContext.PesterConfig + return Invoke-NovaPester -Configuration $Configuration } finally { $global:ProgressPreference = $previousProgressPreference } +} - & $WorkflowContext.TestResultArtifactWriter.ScriptBlock -TestResult $testResult -OutputPath $WorkflowContext.TestResultPath -ReportWriter $WorkflowContext.TestResultReportWriter.ScriptBlock +function Get-NovaTestWorkflowFailureMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) - if ($testResult.Result -ne 'Passed') { - Stop-NovaOperation -Message 'Tests failed' -ErrorId 'Nova.Workflow.TestRunFailed' -Category InvalidOperation -TargetObject $WorkflowContext.TestResultPath + return "Pester reported one or more failing tests. Review the output above and the test result file at $( $WorkflowContext.TestResultPath ), then rerun Test-NovaBuild." +} + +function Write-NovaTestWorkflowResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [AllowNull()][object]$TestResult, + [switch]$WhatIfEnabled + ) + + Write-Message (Get-NovaTestWorkflowStatusMessage -WorkflowContext $WorkflowContext -WhatIfEnabled:$WhatIfEnabled) -color Green + Write-Message "Results file: $( $WorkflowContext.TestResultPath )" + + $coverageMessage = Get-NovaTestWorkflowCoverageMessage -WorkflowContext $WorkflowContext -TestResult $TestResult -WhatIfEnabled:$WhatIfEnabled + if (-not [string]::IsNullOrWhiteSpace($coverageMessage)) { + Write-Message $coverageMessage + } + + foreach ($line in (Get-NovaTestWorkflowNextStepLine -WhatIfEnabled:$WhatIfEnabled)) { + Write-Message $line + } +} + +function Get-NovaTestWorkflowStatusMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return "Test plan ready for $( $WorkflowContext.ProjectInfo.ProjectName )" + } + + return "Pester tests passed for $( $WorkflowContext.ProjectInfo.ProjectName )" +} + +function Get-NovaTestWorkflowCoverageMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [AllowNull()][object]$TestResult, + [switch]$WhatIfEnabled + ) + + $coveragePercentTarget = Get-NovaConfiguredCoveragePercentTarget -WorkflowContext $WorkflowContext + if ($WhatIfEnabled) { + if ($null -eq $coveragePercentTarget) { + return $null + } + + return "Configured coverage target: $( Format-NovaCoveragePercentValue -Value ([double]$coveragePercentTarget) )%" } - & $coverageTargetAssertion -WorkflowContext $WorkflowContext -TestResult $testResult + $codeCoverage = Get-NovaPropertyValue -InputObject $TestResult -Name 'CodeCoverage' + $coveragePercent = Get-NovaPropertyValue -InputObject $codeCoverage -Name 'CoveragePercent' + if ($null -eq $coveragePercent -or [string]::IsNullOrWhiteSpace([string]$coveragePercent)) { + return $null + } + + $formattedCoverage = Format-NovaCoveragePercentValue -Value ([double]$coveragePercent) + if ($null -eq $coveragePercentTarget) { + return "Measured code coverage: $formattedCoverage%" + } + + $formattedTarget = Format-NovaCoveragePercentValue -Value ([double]$coveragePercentTarget) + return "Measured code coverage: $formattedCoverage% (target: $formattedTarget%)" +} + +function Get-NovaTestWorkflowNextStepLine { + [CmdletBinding()] + param( + [switch]$WhatIfEnabled + ) + + if ($WhatIfEnabled) { + return @( + 'Next step:' + 'Run Test-NovaBuild without -WhatIf when you are ready to execute the test workflow.' + ) + } + + return @( + 'Next step:' + 'Publish-NovaModule -Local' + ) } function Test-NovaTestWorkflowBuildRequested { @@ -129,7 +320,7 @@ function Get-NovaDefaultCoverageTargetAssertionScriptBlock { $codeCoverage = & $propertyReader -InputObject $TestResult -Name 'CodeCoverage' $coveragePercent = & $propertyReader -InputObject $codeCoverage -Name 'CoveragePercent' if ($null -eq $coveragePercent -or [string]::IsNullOrWhiteSpace([string]$coveragePercent)) { - $exception = [System.IO.InvalidDataException]::new("Code coverage target $formattedTarget% is configured, but the Pester result did not include a coverage percentage.") + $exception = [System.IO.InvalidDataException]::new("Code coverage target $formattedTarget% is configured, but the Pester result did not include a coverage percentage. Review the coverage settings in project.json and the test result file at $resolvedTargetObject.") $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'Nova.Workflow.CodeCoveragePercentMissing', [System.Management.Automation.ErrorCategory]::InvalidData, $resolvedTargetObject) throw $errorRecord } @@ -140,7 +331,7 @@ function Get-NovaDefaultCoverageTargetAssertionScriptBlock { } $formattedCoverage = & $percentFormatter -Value $coveragePercent - $exception = [System.InvalidOperationException]::new("Code coverage $formattedCoverage% did not meet the configured target $formattedTarget%.") + $exception = [System.InvalidOperationException]::new("Code coverage $formattedCoverage% did not meet the configured target $formattedTarget%. Review the failing tests or coverage settings, then rerun Test-NovaBuild.") $errorRecord = [System.Management.Automation.ErrorRecord]::new($exception, 'Nova.Workflow.CodeCoverageTargetNotMet', [System.Management.Automation.ErrorCategory]::InvalidOperation, $resolvedTargetObject) throw $errorRecord }.GetNewClosure() diff --git a/tests/private/quality/InvokeNovaTestWorkflow.TestSupport.ps1 b/tests/private/quality/InvokeNovaTestWorkflow.TestSupport.ps1 index adb9f255..10366f00 100644 --- a/tests/private/quality/InvokeNovaTestWorkflow.TestSupport.ps1 +++ b/tests/private/quality/InvokeNovaTestWorkflow.TestSupport.ps1 @@ -10,6 +10,8 @@ function Stop-NovaOperation { function Invoke-NovaBuild {} function Invoke-NovaPester {param($Configuration)} function Get-NovaBuildCommandParameterMap {param($WorkflowParams, [switch]$OverrideWarningRequested) return @{}} +function Write-Message {param([string]$Text, [string]$color)} +function Write-Progress {param([string]$Activity, [string]$Status, [int]$PercentComplete, [switch]$Completed)} function New-NovaInvokeNovaTestWorkflowContext { param( [hashtable]$PesterSettings = @{}, @@ -21,7 +23,7 @@ function New-NovaInvokeNovaTestWorkflowContext { return [pscustomobject]@{ BuildRequested = $BuildRequested WorkflowParams = $WorkflowParams - ProjectInfo = [pscustomobject]@{Pester = $PesterSettings} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; Pester = $PesterSettings} TestResultDirectory = $TestResultDirectory TestResultPath = Join-Path $TestResultDirectory 'TestResults.xml' PesterConfig = [pscustomobject]@{TestResult = [pscustomobject]@{OutputPath = $null}} diff --git a/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 b/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 index 8814588f..9e7e6f69 100644 --- a/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 +++ b/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 @@ -6,10 +6,15 @@ BeforeAll { } Describe 'Invoke-NovaTestWorkflow' { + BeforeEach { + Mock Write-Message {} + Mock Write-Progress {} + } + It 'uses the pre-resolved coverage assertion after the Pester run' { $global:coverageAssertionRan = $false $workflowContext = [pscustomobject]@{ - ProjectInfo = [pscustomobject]@{Pester = [ordered]@{}} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; Pester = [ordered]@{}} TestResultDirectory = '/tmp/nova-project/artifacts' TestResultPath = '/tmp/nova-project/artifacts/TestResults.xml' PesterConfig = [pscustomobject]@{TestResult = [pscustomobject]@{OutputPath = $null}} @@ -26,6 +31,14 @@ Describe 'Invoke-NovaTestWorkflow' { {Invoke-NovaTestWorkflow -WorkflowContext $workflowContext} | Should -Not -Throw $global:coverageAssertionRan | Should -BeTrue + Assert-MockCalled Write-Message -Times 4 + Assert-MockCalled Write-Progress -Times 5 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Pester tests passed for NovaModuleTools' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Publish-NovaModule -Local' + } } finally { Remove-Variable -Name coverageAssertionRan -Scope Global -ErrorAction SilentlyContinue } @@ -35,20 +48,21 @@ Describe 'Invoke-NovaTestWorkflow' { @{ Name = 'coverage below target' PesterResult = [pscustomobject]@{Result = 'Passed'; CodeCoverage = [pscustomobject]@{CoveragePercent = 66.67}} - ExpectedMessage = 'Code coverage 66.67% did not meet the configured target 90%.' + ExpectedMessage = 'Code coverage 66.67% did not meet the configured target 90%. Review the failing tests or coverage settings, then rerun Test-NovaBuild.' ExpectedErrorId = 'Nova.Workflow.CodeCoverageTargetNotMet' ExpectedCategory = [System.Management.Automation.ErrorCategory]::InvalidOperation } @{ Name = 'missing measured coverage' PesterResult = [pscustomobject]@{Result = 'Passed'} - ExpectedMessage = 'Code coverage target 90% is configured, but the Pester result did not include a coverage percentage.' + ExpectedMessage = 'Code coverage target 90% is configured, but the Pester result did not include a coverage percentage. Review the coverage settings in project.json and the test result file at /tmp/nova-project/artifacts/TestResults.xml.' ExpectedErrorId = 'Nova.Workflow.CodeCoveragePercentMissing' ExpectedCategory = [System.Management.Automation.ErrorCategory]::InvalidData } ) { $workflowContext = [pscustomobject]@{ ProjectInfo = [pscustomobject]@{ + ProjectName = 'NovaModuleTools' Pester = [ordered]@{CodeCoverage = [ordered]@{Enabled = $true; CoveragePercentTarget = 90}} } TestResultDirectory = '/tmp/nova-project/artifacts' @@ -73,7 +87,7 @@ Describe 'Invoke-NovaTestWorkflow' { It 'does not enforce a coverage threshold when project.json does not configure one' { $workflowContext = [pscustomobject]@{ - ProjectInfo = [pscustomobject]@{Pester = [ordered]@{}} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; Pester = [ordered]@{}} TestResultDirectory = '/tmp/nova-project/artifacts' TestResultPath = '/tmp/nova-project/artifacts/TestResults.xml' PesterConfig = [pscustomobject]@{TestResult = [pscustomobject]@{OutputPath = $null}} @@ -88,6 +102,10 @@ Describe 'Invoke-NovaTestWorkflow' { {Invoke-NovaTestWorkflow -WorkflowContext $workflowContext} | Should -Not -Throw $workflowContext.PesterConfig.TestResult.OutputPath | Should -Be '/tmp/nova-project/artifacts/TestResults.xml' + Assert-MockCalled Write-Message -Times 5 + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Measured code coverage: 10%' + } } It 'falls back to CodeCoverage as the target object when the workflow context has no test result path' { @@ -123,7 +141,7 @@ Describe 'Invoke-NovaTestWorkflow' { It 'suppresses global progress output around the Pester run and restores the previous preference' { $workflowContext = [pscustomobject]@{ - ProjectInfo = [pscustomobject]@{Pester = [ordered]@{}} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; Pester = [ordered]@{}} TestResultDirectory = '/tmp/nova-project/artifacts' TestResultPath = '/tmp/nova-project/artifacts/TestResults.xml' PesterConfig = [pscustomobject]@{TestResult = [pscustomobject]@{OutputPath = $null}} @@ -153,7 +171,7 @@ Describe 'Invoke-NovaTestWorkflow' { It 'restores the previous progress preference even when Pester throws' { $workflowContext = [pscustomobject]@{ - ProjectInfo = [pscustomobject]@{Pester = [ordered]@{}} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; Pester = [ordered]@{}} TestResultDirectory = '/tmp/nova-project/artifacts' TestResultPath = '/tmp/nova-project/artifacts/TestResults.xml' PesterConfig = [pscustomobject]@{TestResult = [pscustomobject]@{OutputPath = $null}} @@ -185,6 +203,7 @@ Describe 'Invoke-NovaTestWorkflow' { ExpectedBuildCalls = 1 ExpectedPesterCalls = 1 ExpectedErrorId = $null + ExpectedMessageCount = 4 } @{ Name = 'an explicit ShouldRun=false' @@ -195,6 +214,7 @@ Describe 'Invoke-NovaTestWorkflow' { ExpectedBuildCalls = 0 ExpectedPesterCalls = 0 ExpectedErrorId = $null + ExpectedMessageCount = 0 } @{ Name = 'a WhatIf workflow parameter' @@ -205,6 +225,7 @@ Describe 'Invoke-NovaTestWorkflow' { ExpectedBuildCalls = 0 ExpectedPesterCalls = 0 ExpectedErrorId = $null + ExpectedMessageCount = 4 } @{ Name = 'a failed Pester result' @@ -215,6 +236,7 @@ Describe 'Invoke-NovaTestWorkflow' { ExpectedBuildCalls = 0 ExpectedPesterCalls = 1 ExpectedErrorId = 'Nova.Workflow.TestRunFailed' + ExpectedMessageCount = 0 } ) { $workflowContext = New-NovaInvokeNovaTestWorkflowContext -BuildRequested $BuildRequested -WorkflowParams $WorkflowParams @@ -237,6 +259,9 @@ Describe 'Invoke-NovaTestWorkflow' { if ($ExpectedErrorId) { $thrown | Should -Not -BeNullOrEmpty $thrown.FullyQualifiedErrorId | Should -Be $ExpectedErrorId + if ($ExpectedErrorId -eq 'Nova.Workflow.TestRunFailed') { + $thrown.Exception.Message | Should -Be 'Pester reported one or more failing tests. Review the output above and the test result file at /tmp/nova-project/artifacts/TestResults.xml, then rerun Test-NovaBuild.' + } } else { $thrown | Should -BeNullOrEmpty @@ -244,12 +269,13 @@ Describe 'Invoke-NovaTestWorkflow' { Should -Invoke Invoke-NovaBuild -Times $ExpectedBuildCalls Should -Invoke Invoke-NovaPester -Times $ExpectedPesterCalls + Assert-MockCalled Write-Message -Times $ExpectedMessageCount } It 'creates the artifact directory when it does not exist' { $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([guid]::NewGuid()) $workflowContext = [pscustomobject]@{ - ProjectInfo = [pscustomobject]@{Pester = [ordered]@{}} + ProjectInfo = [pscustomobject]@{ProjectName = 'NovaModuleTools'; Pester = [ordered]@{}} TestResultDirectory = $tempDir TestResultPath = (Join-Path $tempDir 'TestResults.xml') PesterConfig = [pscustomobject]@{TestResult = [pscustomobject]@{OutputPath = $null}} @@ -265,4 +291,34 @@ Describe 'Invoke-NovaTestWorkflow' { } } + It 'writes a test plan summary in WhatIf mode after previewing the nested build step' { + $workflowContext = New-NovaInvokeNovaTestWorkflowContext -BuildRequested $true -WorkflowParams @{WhatIf = $true} -PesterSettings @{ + CodeCoverage = [ordered]@{ + Enabled = $true + CoveragePercentTarget = 99 + } + } + Mock Invoke-NovaBuild {} + Mock Invoke-NovaPester {} + + Invoke-NovaTestWorkflow -WorkflowContext $workflowContext + + Should -Invoke Invoke-NovaBuild -Times 1 + Should -Invoke Invoke-NovaPester -Times 0 + Assert-MockCalled Write-Message -Times 5 + Assert-MockCalled Write-Progress -Times 2 + Assert-MockCalled Write-Progress -Times 1 -ParameterFilter { + $Status -eq 'Previewing the build-before-test workflow' -and $PercentComplete -eq 20 + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Test plan ready for NovaModuleTools' -and $color -eq 'Green' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Configured coverage target: 99%' + } + Assert-MockCalled Write-Message -Times 1 -ParameterFilter { + $Text -eq 'Run Test-NovaBuild without -WhatIf when you are ready to execute the test workflow.' + } + } + } From b5066da30e100e022b45368f31cb47b8529f50ef Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 16:11:21 +0200 Subject: [PATCH 14/16] #215 feat: enhance Update-NovaModuleTool with improved workflow and WhatIf support refactor(Update-NovaModuleTools): align with terminal-ux-design principles Fixes #228 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../en-US/Update-NovaModuleTools.md | 13 +- ...GetNovaModuleSelfUpdateWorkflowContext.ps1 | 5 +- .../InvokeNovaModuleSelfUpdateWorkflow.ps1 | 191 +++++++++++++++++- src/public/UpdateNovaModuleTools.ps1 | 22 +- tests/ArchitectureGuardrails.Tests.ps1 | 2 +- ...aModuleSelfUpdateWorkflowContext.Tests.ps1 | 11 + ...vaModuleSelfUpdateWorkflow.TestSupport.ps1 | 2 + ...vokeNovaModuleSelfUpdateWorkflow.Tests.ps1 | 93 ++++++++- .../UpdateNovaModuleTools.TestSupport.ps1 | 15 +- tests/public/UpdateNovaModuleTools.Tests.ps1 | 53 ++++- 12 files changed, 364 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07737623..a7793f9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. - `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. - `Test-NovaBuild` now shows progress across the main test phases, ends with the result file path plus a coverage summary and next-step hint when tests pass, uses a test-plan summary in `-WhatIf` mode, and fails with more actionable guidance when Pester or coverage checks fail. +- `Update-NovaModuleTool` now ends with a visible up-to-date, preview, cancelled, or updated summary, skips prerelease confirmation during `-WhatIf`, shows progress for the actual self-update step, and suggests `Get-NovaProjectInfo -Installed` after a successful update. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index c6cadf6f..a00dcee0 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -26,6 +26,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Publish-NovaModule` now shows progress for build validation, publish, local import, and CI restore phases, ends with the publish target plus a suggested verification step, and uses a publish-plan summary in `-WhatIf` mode. - `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. - `Test-NovaBuild` now shows progress across the main test phases, ends with the result file path plus a coverage summary and next-step hint when tests pass, uses a test-plan summary in `-WhatIf` mode, and fails with more actionable guidance when Pester or coverage checks fail. +- `Update-NovaModuleTool` now ends with a visible up-to-date, preview, cancelled, or updated summary, skips prerelease confirmation during `-WhatIf`, shows progress for the actual self-update step, and suggests `Get-NovaProjectInfo -Installed` after a successful update. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md b/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md index 2bc054a2..2b4bda46 100644 --- a/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md +++ b/docs/NovaModuleTools/en-US/Update-NovaModuleTools.md @@ -38,7 +38,9 @@ When prerelease notifications are enabled, `Update-NovaModuleTool` may target a Stable updates do not require prerelease confirmation. -After a successful update, `Update-NovaModuleTool` prints the release notes link from the installed module manifest. +When a newer version is available, `Update-NovaModuleTool` shows progress while it installs the update and reads the release-notes link from the updated module. Every command path ends with a visible summary: up-to-date, preview, cancelled, or updated. + +After a successful update, `Update-NovaModuleTool` prints the release notes link from the installed module manifest and suggests `Get-NovaProjectInfo -Installed` as the next verification step. ## EXAMPLES @@ -48,7 +50,7 @@ After a successful update, `Update-NovaModuleTool` prints the release notes link PS> Update-NovaModuleTool ``` -Updates the installed `NovaModuleTools` module by using the stored prerelease preference to resolve the update candidate. +Updates the installed `NovaModuleTools` module by using the stored prerelease preference to resolve the update candidate, then prints the current version, target version, and the next suggested verification step. ### EXAMPLE 2 @@ -76,13 +78,13 @@ Successful updates print the release notes link from the installed module manife PS> Update-NovaModuleTool -WhatIf ``` -Previews the resolved update action without running `Update-Module`. +Previews the resolved update action without prompting for prerelease confirmation or running `Update-Module`. ## PARAMETERS ### -WhatIf -Shows what would happen if the cmdlet runs. `Update-NovaModuleTool` resolves the target version first, then previews the selected stable or prerelease update action without changing the installed module. +Shows what would happen if the cmdlet runs. `Update-NovaModuleTool` resolves the target version first, then previews the selected stable or prerelease update action without prompting for prerelease confirmation or changing the installed module. ```yaml Type: System.Management.Automation.SwitchParameter @@ -147,8 +149,11 @@ If the PowerShell Gallery cannot be reached well enough to resolve an update can Use `Get-NovaUpdateNotificationPreference` and `Set-NovaUpdateNotificationPreference` to inspect or change the stored prerelease setting. +Use `Ctrl+C` if you need to stop an active self-update before `Update-Module` finishes. + ## RELATED LINKS - [Get-NovaUpdateNotificationPreference](./Get-NovaUpdateNotificationPreference.md) - [Set-NovaUpdateNotificationPreference](./Set-NovaUpdateNotificationPreference.md) +- [Get-NovaProjectInfo](./Get-NovaProjectInfo.md) - [Invoke-NovaBuild](./Invoke-NovaBuild.md) diff --git a/src/private/update/GetNovaModuleSelfUpdateWorkflowContext.ps1 b/src/private/update/GetNovaModuleSelfUpdateWorkflowContext.ps1 index 51341520..d036284c 100644 --- a/src/private/update/GetNovaModuleSelfUpdateWorkflowContext.ps1 +++ b/src/private/update/GetNovaModuleSelfUpdateWorkflowContext.ps1 @@ -4,7 +4,7 @@ function Get-NovaModuleSelfUpdateWorkflowContext { [pscustomobject]$Preference, [pscustomobject]$InstalledModule, [pscustomobject]$LookupResult, - [int]$TimeoutMilliseconds = 10000 + [hashtable]$WorkflowParams = @{} ) $resolvedPreference = if ($null -ne $Preference) { @@ -20,7 +20,7 @@ function Get-NovaModuleSelfUpdateWorkflowContext { $resolvedLookupResult = if ($null -ne $LookupResult) { $LookupResult } else { - Invoke-NovaModuleUpdateLookup -AllowPrereleaseNotifications:$resolvedPreference.PrereleaseNotificationsEnabled -TimeoutMilliseconds $TimeoutMilliseconds + Invoke-NovaModuleUpdateLookup -AllowPrereleaseNotifications:$resolvedPreference.PrereleaseNotificationsEnabled -TimeoutMilliseconds 10000 } if ($null -eq $resolvedLookupResult) { @@ -38,6 +38,7 @@ function Get-NovaModuleSelfUpdateWorkflowContext { Preference = $resolvedPreference InstalledModule = $resolvedInstalledModule LookupResult = $resolvedLookupResult + WorkflowParams = $WorkflowParams Plan = $plan Action = $action } diff --git a/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 b/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 index cd0d26ae..40b52668 100644 --- a/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 +++ b/src/private/update/InvokeNovaModuleSelfUpdateWorkflow.ps1 @@ -7,7 +7,8 @@ function Invoke-NovaModuleSelfUpdateOrStop { try { $null = Invoke-NovaModuleSelfUpdate -ModuleName $Plan.ModuleName -AllowPrerelease:$Plan.UsedAllowPrerelease } catch { - Stop-NovaOperation -Message $_.Exception.Message -ErrorId 'Nova.Dependency.ModuleSelfUpdateFailed' -Category InvalidOperation -TargetObject $Plan.ModuleName + $message = "NovaModuleTools self-update failed: $( $_.Exception.Message ) Confirm that the PowerShell Gallery is reachable and that this session can update installed modules, then rerun Update-NovaModuleTool." + Stop-NovaOperation -Message $message -ErrorId 'Nova.Dependency.ModuleSelfUpdateFailed' -Category InvalidOperation -TargetObject $Plan.ModuleName } } @@ -30,16 +31,190 @@ function Complete-NovaModuleSelfUpdateResult { function Invoke-NovaModuleSelfUpdateWorkflow { [CmdletBinding()] param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$ShouldRun + ) + + $progressActivity = 'Updating NovaModuleTools' + $result = $null + + if (Test-NovaModuleSelfUpdateWorkflowShouldSkipExecution -WorkflowContext $WorkflowContext -ShouldRun:$ShouldRun) { + $result = Complete-NovaModuleSelfUpdateResult -Plan $WorkflowContext.Plan -ReleaseNotesUri $null + Write-NovaModuleSelfUpdateWorkflowResult -Result $result -WorkflowContext $WorkflowContext + return $result + } + + try { + Invoke-NovaModuleSelfUpdateWorkflowStep -Activity $progressActivity -Status (Get-NovaModuleSelfUpdateWorkflowUpdateStatus -WorkflowContext $WorkflowContext) -PercentComplete 80 -Action { + Invoke-NovaModuleSelfUpdateOrStop -Plan $WorkflowContext.Plan + } + + $WorkflowContext.Plan.Updated = $true + $releaseNotesUri = Invoke-NovaModuleSelfUpdateWorkflowStep -Activity $progressActivity -Status 'Reading release notes from the updated module' -PercentComplete 95 -Action { + Get-NovaModuleReleaseNotesUri + } + + $result = Complete-NovaModuleSelfUpdateResult -Plan $WorkflowContext.Plan -ReleaseNotesUri $releaseNotesUri + } finally { + Write-Progress -Activity $progressActivity -Completed + } + + Write-NovaModuleSelfUpdateWorkflowResult -Result $result -WorkflowContext $WorkflowContext + return $result +} + +function Test-NovaModuleSelfUpdateWorkflowShouldSkipExecution { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [switch]$ShouldRun + ) + + return (-not $WorkflowContext.Plan.UpdateAvailable) -or (-not $ShouldRun) +} + +function Invoke-NovaModuleSelfUpdateWorkflowStep { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + & $Action +} + +function Write-NovaModuleSelfUpdateWorkflowResult { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Result, [Parameter(Mandatory)][pscustomobject]$WorkflowContext ) - $plan = $WorkflowContext.Plan - if (-not $plan.UpdateAvailable) { - return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null + $whatIfEnabled = Test-NovaModuleSelfUpdateWorkflowWhatIfEnabled -WorkflowContext $WorkflowContext + Write-Message (Get-NovaModuleSelfUpdateWorkflowStatusMessage -Result $Result -WhatIfEnabled:$whatIfEnabled) -color (Get-NovaModuleSelfUpdateWorkflowStatusColor -Result $Result) + Write-Message "Current version: $( $Result.CurrentVersion )" + + $targetVersionLine = Get-NovaModuleSelfUpdateWorkflowTargetVersionLine -Result $Result + if (-not [string]::IsNullOrWhiteSpace($targetVersionLine)) { + Write-Message $targetVersionLine + } + + $repositoryLine = Get-NovaModuleSelfUpdateWorkflowRepositoryLine -Result $Result + if (-not [string]::IsNullOrWhiteSpace($repositoryLine)) { + Write-Message $repositoryLine + } + + foreach ($line in (Get-NovaModuleSelfUpdateWorkflowNextStepLine -Result $Result -WhatIfEnabled:$whatIfEnabled)) { + Write-Message $line } +} - Invoke-NovaModuleSelfUpdateOrStop -Plan $plan - $plan.Updated = $true - $releaseNotesUri = Get-NovaModuleReleaseNotesUri - return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $releaseNotesUri +function Test-NovaModuleSelfUpdateWorkflowWhatIfEnabled { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + return $WorkflowContext.WorkflowParams.ContainsKey('WhatIf') -and $WorkflowContext.WorkflowParams.WhatIf +} + +function Get-NovaModuleSelfUpdateWorkflowUpdateStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + if ($WorkflowContext.Plan.IsPrereleaseTarget) { + return "Installing prerelease version $( $WorkflowContext.Plan.TargetVersion )" + } + + return "Installing version $( $WorkflowContext.Plan.TargetVersion )" +} + +function Get-NovaModuleSelfUpdateWorkflowStatusMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Result, + [switch]$WhatIfEnabled + ) + + if (-not $Result.UpdateAvailable) { + return 'NovaModuleTools is already up to date.' + } + + if ($WhatIfEnabled) { + return 'Self-update plan ready for NovaModuleTools' + } + + if ($Result.Cancelled) { + return 'Self-update cancelled for NovaModuleTools.' + } + + return "Updated NovaModuleTools to version $( $Result.TargetVersion )." +} + +function Get-NovaModuleSelfUpdateWorkflowStatusColor { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Result + ) + + if ($Result.Cancelled) { + return 'Blue' + } + + return 'Green' +} + +function Get-NovaModuleSelfUpdateWorkflowTargetVersionLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Result + ) + + if (-not $Result.UpdateAvailable) { + return $null + } + + return "Target version: $( $Result.TargetVersion )" +} + +function Get-NovaModuleSelfUpdateWorkflowRepositoryLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Result + ) + + if ([string]::IsNullOrWhiteSpace($Result.LookupRepository)) { + return $null + } + + return "Repository: $( $Result.LookupRepository )" +} + +function Get-NovaModuleSelfUpdateWorkflowNextStepLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$Result, + [switch]$WhatIfEnabled + ) + + if (-not $Result.UpdateAvailable -or $Result.Cancelled) { + return @() + } + + if ($WhatIfEnabled) { + return @( + 'Next step:' + "Run Update-NovaModuleTool without -WhatIf when you are ready to install version $( $Result.TargetVersion )." + ) + } + + return @( + 'Next step:' + 'Get-NovaProjectInfo -Installed' + ) } diff --git a/src/public/UpdateNovaModuleTools.ps1 b/src/public/UpdateNovaModuleTools.ps1 index f54082fc..08ed0a1f 100644 --- a/src/public/UpdateNovaModuleTools.ps1 +++ b/src/public/UpdateNovaModuleTools.ps1 @@ -3,22 +3,22 @@ function Update-NovaModuleTool { [Alias('Update-NovaModuleTools')] param() - $workflowContext = Get-NovaModuleSelfUpdateWorkflowContext + $workflowContext = Get-NovaModuleSelfUpdateWorkflowContext -WorkflowParams @{WhatIf = [bool]$WhatIfPreference} $plan = $workflowContext.Plan - if (-not $plan.UpdateAvailable) { - return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null + if ($plan.IsPrereleaseTarget -and -not $WhatIfPreference) { + $prereleaseConfirmed = Confirm-NovaPrereleaseModuleUpdate -Cmdlet $PSCmdlet -CurrentVersion $plan.CurrentVersion -TargetVersion $plan.TargetVersion + if (-not $prereleaseConfirmed) { + $plan.Cancelled = $true + return Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext $workflowContext -ShouldRun:$false + } } - if ($plan.IsPrereleaseTarget -and -not (Confirm-NovaPrereleaseModuleUpdate -Cmdlet $PSCmdlet -CurrentVersion $plan.CurrentVersion -TargetVersion $plan.TargetVersion)) { - $plan.Cancelled = $true - return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null + $shouldRun = $plan.UpdateAvailable -and $PSCmdlet.ShouldProcess($plan.ModuleName, $workflowContext.Action) + if (-not $shouldRun) { + $plan.Cancelled = (-not $WhatIfPreference) -and $plan.UpdateAvailable } - if (-not $PSCmdlet.ShouldProcess($plan.ModuleName, $workflowContext.Action)) { - return Complete-NovaModuleSelfUpdateResult -Plan $plan -ReleaseNotesUri $null - } - - $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext $workflowContext + $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext $workflowContext -ShouldRun:$shouldRun Write-NovaModuleReleaseNotesLink -ReleaseNotesUri $result.ReleaseNotesUri return $result } diff --git a/tests/ArchitectureGuardrails.Tests.ps1 b/tests/ArchitectureGuardrails.Tests.ps1 index dc07e3df..e9cf6190 100644 --- a/tests/ArchitectureGuardrails.Tests.ps1 +++ b/tests/ArchitectureGuardrails.Tests.ps1 @@ -95,7 +95,7 @@ Describe 'Architecture guardrails' { [pscustomobject]@{Path = 'src/public/PublishNovaModule.ps1'; ExpectedHelpers = @('Get-NovaDynamicDeliveryParameterDictionary', 'Get-NovaProjectInfo', 'Get-NovaPublishWorkflowContext', 'Get-NovaShouldProcessForwardingParameter', 'Invoke-NovaPublishWorkflow', 'Write-NovaPublishWorkflowContext')} [pscustomobject]@{Path = 'src/public/SetNovaUpdateNotificationPreference.ps1'; ExpectedHelpers = @('Get-NovaUpdateNotificationPreferenceChangeContext', 'Invoke-NovaUpdateNotificationPreferenceChange')} [pscustomobject]@{Path = 'src/public/TestNovaBuild.ps1'; ExpectedHelpers = @('Get-NovaTestWorkflowContext', 'Invoke-NovaTestWorkflow', 'New-NovaTestDynamicParameterDictionary')} - [pscustomobject]@{Path = 'src/public/UpdateNovaModuleTools.ps1'; ExpectedHelpers = @('Complete-NovaModuleSelfUpdateResult', 'Confirm-NovaPrereleaseModuleUpdate', 'Get-NovaModuleSelfUpdateWorkflowContext', 'Invoke-NovaModuleSelfUpdateWorkflow', 'Write-NovaModuleReleaseNotesLink')} + [pscustomobject]@{Path = 'src/public/UpdateNovaModuleTools.ps1'; ExpectedHelpers = @('Confirm-NovaPrereleaseModuleUpdate', 'Get-NovaModuleSelfUpdateWorkflowContext', 'Invoke-NovaModuleSelfUpdateWorkflow', 'Write-NovaModuleReleaseNotesLink')} [pscustomobject]@{Path = 'src/public/UpdateNovaModuleVersion.ps1'; ExpectedHelpers = @('Get-NovaVersionUpdateWorkflowContext', 'Invoke-NovaVersionUpdateCiActivation', 'Invoke-NovaVersionUpdateWorkflow', 'Write-NovaVersionUpdateResultOutput')} ) $expectedPaths = @($testCases | ForEach-Object Path | Sort-Object) diff --git a/tests/private/update/GetNovaModuleSelfUpdateWorkflowContext.Tests.ps1 b/tests/private/update/GetNovaModuleSelfUpdateWorkflowContext.Tests.ps1 index bdd790e7..fe4e6477 100644 --- a/tests/private/update/GetNovaModuleSelfUpdateWorkflowContext.Tests.ps1 +++ b/tests/private/update/GetNovaModuleSelfUpdateWorkflowContext.Tests.ps1 @@ -28,6 +28,17 @@ Describe 'Get-NovaModuleSelfUpdateWorkflowContext' { $ctx.Action | Should -Be 'Update NovaModuleTools to prerelease version 3.0.0-rc1' } + It 'carries forwarded workflow parameters into the context' { + Mock Read-NovaUpdateNotificationPreference {[pscustomobject]@{PrereleaseNotificationsEnabled=$false}} + Mock Get-NovaInstalledModuleVersionInfo {[pscustomobject]@{Version='1.0.0'}} + Mock Invoke-NovaModuleUpdateLookup {[pscustomobject]@{Version='2.0.0'}} + Mock Get-NovaModuleSelfUpdatePlan {[pscustomobject]@{TargetVersion='2.0.0'; IsPrereleaseTarget=$false}} + + $ctx = Get-NovaModuleSelfUpdateWorkflowContext -WorkflowParams @{WhatIf = $true} + + $ctx.WorkflowParams.WhatIf | Should -BeTrue + } + It 'throws when no lookup result is available' { Mock Read-NovaUpdateNotificationPreference {[pscustomobject]@{PrereleaseNotificationsEnabled=$false}} Mock Get-NovaInstalledModuleVersionInfo {[pscustomobject]@{Version='1.0.0'}} diff --git a/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.TestSupport.ps1 b/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.TestSupport.ps1 index a48d5a1c..f9e1943a 100644 --- a/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.TestSupport.ps1 +++ b/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.TestSupport.ps1 @@ -1,3 +1,5 @@ function Invoke-NovaModuleSelfUpdate {param([string]$ModuleName,[switch]$AllowPrerelease)} function Get-NovaModuleReleaseNotesUri {'https://example.com/notes'} function Stop-NovaOperation {param([string]$Message,[string]$ErrorId,$Category,$TargetObject) throw [System.Management.Automation.ErrorRecord]::new([System.Exception]::new($Message),$ErrorId,$Category,$TargetObject)} +function Write-Message {param([string]$Text, [string]$color)} +function Write-Progress {param([string]$Activity, [string]$Status, [int]$PercentComplete, [switch]$Completed)} diff --git a/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 b/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 index a6581e04..526f60d0 100644 --- a/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 +++ b/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 @@ -30,26 +30,107 @@ Describe 'Invoke-NovaModuleSelfUpdateOrStop' { } It 'wraps update failures in a Nova.Dependency.ModuleSelfUpdateFailed error' { Mock Invoke-NovaModuleSelfUpdate {throw 'gallery offline'} - { Invoke-NovaModuleSelfUpdateOrStop -Plan ([pscustomobject]@{ModuleName='Nova'; UsedAllowPrerelease=$false}) } | Should -Throw -ErrorId 'Nova.Dependency.ModuleSelfUpdateFailed' + $thrown = $null + try { + Invoke-NovaModuleSelfUpdateOrStop -Plan ([pscustomobject]@{ModuleName='Nova'; UsedAllowPrerelease=$false}) + } catch { + $thrown = $_ + } + + $thrown | Should -Not -BeNullOrEmpty + $thrown.FullyQualifiedErrorId | Should -Be 'Nova.Dependency.ModuleSelfUpdateFailed' + $thrown.Exception.Message | Should -Be 'NovaModuleTools self-update failed: gallery offline Confirm that the PowerShell Gallery is reachable and that this session can update installed modules, then rerun Update-NovaModuleTool.' } } Describe 'Invoke-NovaModuleSelfUpdateWorkflow' { + BeforeEach { + Mock Write-Message {} + Mock Write-Progress {} + } + It 'returns plan with null release notes when no update is available' { Mock Invoke-NovaModuleSelfUpdate {} Mock Get-NovaModuleReleaseNotesUri {throw 'should not run'} - $plan = [pscustomobject]@{UpdateAvailable=$false; ModuleName='Nova'; UsedAllowPrerelease=$false; Updated=$false; ReleaseNotesUri=$null} - $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext ([pscustomobject]@{Plan=$plan}) + $plan = [pscustomobject]@{UpdateAvailable=$false; ModuleName='Nova'; CurrentVersion='1.0.0'; UsedAllowPrerelease=$false; Updated=$false; Cancelled=$false; LookupRepository='PSGallery'; ReleaseNotesUri=$null} + $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext ([pscustomobject]@{Plan=$plan; WorkflowParams=@{}}) -ShouldRun:$false $result.Updated | Should -BeFalse $result.ReleaseNotesUri | Should -BeNull + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'NovaModuleTools is already up to date.' -and $color -eq 'Green'} + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Current version: 1.0.0'} + Should -Invoke Invoke-NovaModuleSelfUpdate -Times 0 } - It 'performs update, sets Updated, and resolves release notes when update is available' { + It 'writes a preview summary in WhatIf mode without updating the module' { + Mock Invoke-NovaModuleSelfUpdate {} + Mock Get-NovaModuleReleaseNotesUri {throw 'should not run'} + $plan = [pscustomobject]@{ + UpdateAvailable = $true + IsPrereleaseTarget = $false + ModuleName = 'NovaModuleTools' + CurrentVersion = '1.0.0' + TargetVersion = '1.1.0' + UsedAllowPrerelease = $false + Updated = $false + Cancelled = $false + LookupRepository = 'PSGallery' + ReleaseNotesUri = $null + } + + $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext ([pscustomobject]@{Plan=$plan; WorkflowParams=@{WhatIf=$true}}) -ShouldRun:$false + + $result.ReleaseNotesUri | Should -BeNull + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Self-update plan ready for NovaModuleTools' -and $color -eq 'Green'} + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Target version: 1.1.0'} + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Repository: PSGallery'} + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Run Update-NovaModuleTool without -WhatIf when you are ready to install version 1.1.0.'} + Should -Invoke Invoke-NovaModuleSelfUpdate -Times 0 + } + + It 'writes a cancellation summary when the update is cancelled before execution' { + Mock Invoke-NovaModuleSelfUpdate {} + $plan = [pscustomobject]@{ + UpdateAvailable = $true + IsPrereleaseTarget = $false + ModuleName = 'NovaModuleTools' + CurrentVersion = '1.0.0' + TargetVersion = '1.1.0' + UsedAllowPrerelease = $false + Updated = $false + Cancelled = $true + LookupRepository = 'PSGallery' + ReleaseNotesUri = $null + } + + $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext ([pscustomobject]@{Plan=$plan; WorkflowParams=@{}}) -ShouldRun:$false + + $result.Cancelled | Should -BeTrue + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Self-update cancelled for NovaModuleTools.' -and $color -eq 'Blue'} + Should -Invoke Invoke-NovaModuleSelfUpdate -Times 0 + } + + It 'performs update, sets Updated, resolves release notes, and prints the next step when update is available' { Mock Invoke-NovaModuleSelfUpdate {} Mock Get-NovaModuleReleaseNotesUri {'https://example.com/n'} - $plan = [pscustomobject]@{UpdateAvailable=$true; ModuleName='Nova'; UsedAllowPrerelease=$false; Updated=$false; ReleaseNotesUri=$null} - $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext ([pscustomobject]@{Plan=$plan}) + $plan = [pscustomobject]@{ + UpdateAvailable = $true + IsPrereleaseTarget = $false + ModuleName = 'NovaModuleTools' + CurrentVersion = '1.0.0' + TargetVersion = '1.1.0' + UsedAllowPrerelease = $false + Updated = $false + Cancelled = $false + LookupRepository = 'PSGallery' + ReleaseNotesUri = $null + } + + $result = Invoke-NovaModuleSelfUpdateWorkflow -WorkflowContext ([pscustomobject]@{Plan=$plan; WorkflowParams=@{}}) -ShouldRun + $result.Updated | Should -BeTrue $result.ReleaseNotesUri | Should -Be 'https://example.com/n' + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Updated NovaModuleTools to version 1.1.0.' -and $color -eq 'Green'} + Assert-MockCalled Write-Message -Times 1 -ParameterFilter {$Text -eq 'Get-NovaProjectInfo -Installed'} + Assert-MockCalled Write-Progress -Times 3 } } diff --git a/tests/public/UpdateNovaModuleTools.TestSupport.ps1 b/tests/public/UpdateNovaModuleTools.TestSupport.ps1 index eed9dcbf..a72ffafa 100644 --- a/tests/public/UpdateNovaModuleTools.TestSupport.ps1 +++ b/tests/public/UpdateNovaModuleTools.TestSupport.ps1 @@ -1,8 +1,13 @@ -function Get-NovaModuleSelfUpdateWorkflowContext {return [pscustomobject]@{Plan=$script:plan; Action='Update'}} -function Complete-NovaModuleSelfUpdateResult {param($Plan, $ReleaseNotesUri) return [pscustomobject]@{Plan=$Plan; ReleaseNotesUri=$ReleaseNotesUri; Completed=$true}} -function Confirm-NovaPrereleaseModuleUpdate {param($Cmdlet, $CurrentVersion, $TargetVersion) return $script:confirmResult} -function Invoke-NovaModuleSelfUpdateWorkflow {param($WorkflowContext) +function Get-NovaShouldProcessForwardingParameter {param([switch]$WhatIfEnabled) return @{WhatIf = [bool]$WhatIfEnabled}} +function Get-NovaModuleSelfUpdateWorkflowContext {param([hashtable]$WorkflowParams) $script:workflowParams = $WorkflowParams; return [pscustomobject]@{Plan=$script:plan; Action='Update'; WorkflowParams=$WorkflowParams}} +function Confirm-NovaPrereleaseModuleUpdate {param($Cmdlet, $CurrentVersion, $TargetVersion) $script:confirmCallCount += 1; return $script:confirmResult} +function Invoke-NovaModuleSelfUpdateWorkflow {param($WorkflowContext, [switch]$ShouldRun) $script:invoked = $true - return [pscustomobject]@{ReleaseNotesUri='https://x/rel'} + $script:workflowContext = $WorkflowContext + $script:shouldRun = [bool]$ShouldRun + $releaseNotesUri = if ($ShouldRun) {'https://x/rel'} else {$null} + $WorkflowContext.Plan | Add-Member -NotePropertyName 'ReleaseNotesUri' -NotePropertyValue $releaseNotesUri -Force + + return $WorkflowContext.Plan } function Write-NovaModuleReleaseNotesLink {param($ReleaseNotesUri) $script:notesUri = $ReleaseNotesUri} diff --git a/tests/public/UpdateNovaModuleTools.Tests.ps1 b/tests/public/UpdateNovaModuleTools.Tests.ps1 index 9d0b5ae3..4311cf33 100644 --- a/tests/public/UpdateNovaModuleTools.Tests.ps1 +++ b/tests/public/UpdateNovaModuleTools.Tests.ps1 @@ -7,30 +7,67 @@ BeforeAll { Describe 'Update-NovaModuleTool' { BeforeEach { - $script:plan = [pscustomobject]@{UpdateAvailable=$true; IsPrereleaseTarget=$false; ModuleName='NovaModuleTools'; CurrentVersion='1.0.0'; TargetVersion='1.1.0'; Cancelled=$false} + $script:plan = [pscustomobject]@{ + UpdateAvailable = $true + IsPrereleaseTarget = $false + ModuleName = 'NovaModuleTools' + CurrentVersion = '1.0.0' + TargetVersion = '1.1.0' + Cancelled = $false + LookupRepository = 'PSGallery' + } $script:confirmResult = $true - $script:invoked = $false; $script:notesUri = $null + $script:confirmCallCount = 0 + $script:invoked = $false + $script:notesUri = $null + $script:shouldRun = $null + $script:workflowContext = $null + $script:workflowParams = $null } - It 'short-circuits with a completion result when no update is available' { + It 'invokes the workflow with ShouldRun=$false when no update is available' { $script:plan.UpdateAvailable = $false + $result = Update-NovaModuleTool - $script:invoked | Should -BeFalse - $result.Completed | Should -BeTrue + + $script:invoked | Should -BeTrue + $script:shouldRun | Should -BeFalse + $result.UpdateAvailable | Should -BeFalse + $script:notesUri | Should -BeNull } - It 'cancels when a prerelease target is not confirmed' { + It 'cancels when a prerelease target is not confirmed and still invokes the workflow for the summary' { $script:plan.IsPrereleaseTarget = $true $script:confirmResult = $false + $result = Update-NovaModuleTool - $script:invoked | Should -BeFalse - $result.Plan.Cancelled | Should -BeTrue + + $script:invoked | Should -BeTrue + $script:confirmCallCount | Should -Be 1 + $script:shouldRun | Should -BeFalse + $result.Cancelled | Should -BeTrue + $script:notesUri | Should -BeNull } It 'runs the self-update workflow and writes the release notes link' { $result = Update-NovaModuleTool + $script:invoked | Should -BeTrue + $script:shouldRun | Should -BeTrue + $script:workflowParams.WhatIf | Should -BeFalse $script:notesUri | Should -Be 'https://x/rel' $result.ReleaseNotesUri | Should -Be 'https://x/rel' } + + It 'invokes the workflow with ShouldRun=$false in WhatIf mode without prompting for prerelease confirmation' { + $script:plan.IsPrereleaseTarget = $true + + Update-NovaModuleTool -WhatIf | Out-Null + + $script:invoked | Should -BeTrue + $script:shouldRun | Should -BeFalse + $script:workflowContext.WorkflowParams.WhatIf | Should -BeTrue + $script:confirmCallCount | Should -Be 0 + $script:notesUri | Should -BeNull + } } From 4c1b8635d3d5878deabacfc5fadf4344039606e8 Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 16:24:06 +0200 Subject: [PATCH 15/16] #215 feat: enhance version update workflow with progress reporting and detailed summaries refactor(Update-NovaModuleVersion): align with terminal-ux-design principles Fixes #229 --- CHANGELOG.md | 1 + RELEASE_NOTE.md | 1 + .../en-US/Update-NovaModuleVersion.md | 10 +- .../InvokeNovaVersionUpdateWorkflow.ps1 | 111 +++++++++++--- .../WriteNovaVersionUpdateResultMessages.ps1 | 142 +++++++++++++++++- .../InvokeNovaVersionUpdateWorkflow.Tests.ps1 | 44 ++++-- ...eNovaVersionUpdateResultMessages.Tests.ps1 | 72 ++++++++- .../UpdateNovaModuleVersion.TestSupport.ps1 | 1 + .../public/UpdateNovaModuleVersion.Tests.ps1 | 7 + 9 files changed, 338 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7793f9a..fda1aab7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. - `Test-NovaBuild` now shows progress across the main test phases, ends with the result file path plus a coverage summary and next-step hint when tests pass, uses a test-plan summary in `-WhatIf` mode, and fails with more actionable guidance when Pester or coverage checks fail. - `Update-NovaModuleTool` now ends with a visible up-to-date, preview, cancelled, or updated summary, skips prerelease confirmation during `-WhatIf`, shows progress for the actual self-update step, and suggests `Get-NovaProjectInfo -Installed` after a successful update. +- `Update-NovaModuleVersion` now ends with a visible preview, cancelled, or updated summary, prints the resolved version file plus the detected/applied release label, shows progress while writing `project.json`, and suggests the next command to run after the bump. ### Deprecated diff --git a/RELEASE_NOTE.md b/RELEASE_NOTE.md index a00dcee0..d7cae159 100644 --- a/RELEASE_NOTE.md +++ b/RELEASE_NOTE.md @@ -27,6 +27,7 @@ This file summarizes the release notes for NovaModuleTools. **UNRELEASED** chang - `Set-NovaUpdateNotificationPreference` now ends with a clear success or preview summary, prints the settings file path, suggests `Get-NovaUpdateNotificationPreference` as the next verification step, and gives a more actionable validation error when no enable/disable switch is supplied. - `Test-NovaBuild` now shows progress across the main test phases, ends with the result file path plus a coverage summary and next-step hint when tests pass, uses a test-plan summary in `-WhatIf` mode, and fails with more actionable guidance when Pester or coverage checks fail. - `Update-NovaModuleTool` now ends with a visible up-to-date, preview, cancelled, or updated summary, skips prerelease confirmation during `-WhatIf`, shows progress for the actual self-update step, and suggests `Get-NovaProjectInfo -Installed` after a successful update. +- `Update-NovaModuleVersion` now ends with a visible preview, cancelled, or updated summary, prints the resolved version file plus the detected/applied release label, shows progress while writing `project.json`, and suggests the next command to run after the bump. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md b/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md index 37ad8594..989e0977 100644 --- a/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md +++ b/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md @@ -43,7 +43,7 @@ When Git tags exist, only commits since the latest tag are considered. If Git-ba If the repository exists but has no commits yet, the command stops with: `Cannot bump version because the repository has no commits yet. Create an initial commit first.` -This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` to preview the calculated release label and the exact next version without changing the stored version. +This command supports `-WhatIf` and `-Confirm` through PowerShell `SupportsShouldProcess`. Use `-WhatIf` to preview the calculated release label and the exact next version without changing the stored version. The command now ends with a clear plan, cancellation, or success summary, prints the resolved version file plus the detected/applied release label, and suggests the next step to run. Use `-ContinuousIntegration` when the same session should first re-activate the built `dist/` module before the version bump workflow starts. This is useful in CI/self-hosting flows where an earlier command changed the active module state. @@ -58,7 +58,7 @@ When the current version is already a prerelease for the selected release line, PS> Update-NovaModuleVersion ``` -Updates the version in the current project using the release label inferred from recent commit messages. +Updates the version in the current project using the release label inferred from recent commit messages, then prints the version file plus the next suggested validation step. ### EXAMPLE 2 @@ -81,7 +81,7 @@ Label: Minor CommitCount: 12 ``` -Shows the calculated version update without modifying `project.json`. +Shows the calculated version update without modifying `project.json`, then ends with a version-update plan summary and a reminder to rerun without `-WhatIf` when you are ready to write the new version. ### EXAMPLE 4 @@ -111,7 +111,7 @@ Label: Minor CommitCount: 12 ``` -Shows how `-Preview` keeps the detected semantic label for reporting but deterministically enters the next patch preview track when the current version is stable. +Shows how `-Preview` keeps the detected semantic label for reporting but deterministically enters the next patch preview track when the current version is stable, while the plan summary still points to the exact next version that would be written. ### EXAMPLE 6 @@ -165,7 +165,7 @@ Label: Major CommitCount: 34 ``` -Shows how stable `0.y.z` bumps still warn that `1.0.0` must be set manually when the API becomes stable, while breaking-change commits on that line continue to plan the next minor version instead of jumping straight to `1.0.0`. +Shows how stable `0.y.z` bumps still warn that `1.0.0` must be set manually when the API becomes stable, while breaking-change commits on that line continue to plan the next minor version instead of jumping straight to `1.0.0`. The plan output also keeps both the detected and applied release labels visible. ### EXAMPLE 10 diff --git a/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 b/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 index 4b6dfdfe..cb67f717 100644 --- a/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 +++ b/src/private/release/InvokeNovaVersionUpdateWorkflow.ps1 @@ -7,25 +7,46 @@ function Invoke-NovaVersionUpdateWorkflow { ) $versionWriteResult = $null - if ($ShouldRun) { - $versionWriteResult = Set-NovaModuleVersion -ProjectInfo $WorkflowContext.ProjectInfo -Label (Get-NovaVersionUpdateEffectiveLabel -WorkflowContext $WorkflowContext) -PreviewRelease:$WorkflowContext.PreviewRelease -Confirm:$false + $progressActivity = 'Updating Nova module version' + try { + if ($ShouldRun) { + $versionWriteResult = Invoke-NovaVersionUpdateWorkflowStep -Activity $progressActivity -Status (Get-NovaVersionUpdateApplyStatus -WorkflowContext $WorkflowContext) -PercentComplete 80 -Action { + Set-NovaModuleVersion -ProjectInfo $WorkflowContext.ProjectInfo -Label (Get-NovaVersionUpdateEffectiveLabel -WorkflowContext $WorkflowContext) -PreviewRelease:$WorkflowContext.PreviewRelease -Confirm:$false + } + } } - - if (-not (Test-NovaVersionUpdateResultRequired -ShouldRun:$ShouldRun -WhatIfEnabled:$WhatIfEnabled)) { - return + finally { + if ($ShouldRun) { + Write-Progress -Activity $progressActivity -Completed + } } - return Get-NovaVersionUpdateResult -WorkflowContext $WorkflowContext -Applied:($null -ne $versionWriteResult -and $versionWriteResult.Applied) + $wasApplied = $null -ne $versionWriteResult -and $versionWriteResult.Applied + $wasCancelled = (-not $ShouldRun) -and (-not $WhatIfEnabled) + return Get-NovaVersionUpdateResult -WorkflowContext $WorkflowContext -Applied:$wasApplied -Previewed:$WhatIfEnabled -Cancelled:$wasCancelled } -function Test-NovaVersionUpdateResultRequired { +function Invoke-NovaVersionUpdateWorkflowStep { [CmdletBinding()] param( - [switch]$ShouldRun, - [switch]$WhatIfEnabled + [Parameter(Mandatory)][string]$Activity, + [Parameter(Mandatory)][string]$Status, + [Parameter(Mandatory)][int]$PercentComplete, + [Parameter(Mandatory)][scriptblock]$Action + ) + + Write-Progress -Activity $Activity -Status $Status -PercentComplete $PercentComplete + return & $Action +} + +function Get-NovaVersionUpdateApplyStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext ) - return $ShouldRun -or $WhatIfEnabled + $target = Get-NovaVersionUpdateTarget -WorkflowContext $WorkflowContext + return "Writing version $( $WorkflowContext.NewVersion ) to $target" } function Get-NovaVersionUpdateEffectiveLabel { @@ -34,28 +55,35 @@ function Get-NovaVersionUpdateEffectiveLabel { [Parameter(Mandatory)][pscustomobject]$WorkflowContext ) - if ($WorkflowContext.PSObject.Properties.Name -contains 'EffectiveLabel' -and -not [string]::IsNullOrWhiteSpace($WorkflowContext.EffectiveLabel)) { - return $WorkflowContext.EffectiveLabel + $effectiveLabel = Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'EffectiveLabel' + if (-not [string]::IsNullOrWhiteSpace($effectiveLabel)) { + return $effectiveLabel } - return $WorkflowContext.Label + return Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'Label' } function Get-NovaVersionUpdateResult { [CmdletBinding()] param( [Parameter(Mandatory)][pscustomobject]$WorkflowContext, - [switch]$Applied + [switch]$Applied, + [switch]$Previewed, + [switch]$Cancelled ) return [pscustomobject]@{ - PreviousVersion = $WorkflowContext.PreviousVersion - NewVersion = $WorkflowContext.NewVersion - Label = $WorkflowContext.Label + PreviousVersion = Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'PreviousVersion' + NewVersion = Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'NewVersion' + Label = Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'Label' EffectiveLabel = Get-NovaVersionUpdateEffectiveLabel -WorkflowContext $WorkflowContext AdvisoryMessage = Get-NovaVersionUpdateAdvisoryMessage -WorkflowContext $WorkflowContext - CommitCount = $WorkflowContext.CommitCount + CommitCount = Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'CommitCount' + ProjectFile = Get-NovaVersionUpdateProjectFile -WorkflowContext $WorkflowContext + Target = Get-NovaVersionUpdateTarget -WorkflowContext $WorkflowContext Applied = [bool]$Applied + Previewed = [bool]$Previewed + Cancelled = [bool]$Cancelled } } @@ -65,10 +93,53 @@ function Get-NovaVersionUpdateAdvisoryMessage { [Parameter(Mandatory)][pscustomobject]$WorkflowContext ) - if ($WorkflowContext.PSObject.Properties.Name -notcontains 'AdvisoryMessage') { + return Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'AdvisoryMessage' +} + +function Get-NovaVersionUpdateProjectFile { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + $projectInfo = Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'ProjectInfo' + if ($null -eq $projectInfo) { + return $null + } + + $projectFileProperty = $projectInfo.PSObject.Properties['ProjectJSON'] + if ($null -eq $projectFileProperty) { return $null } - return $WorkflowContext.AdvisoryMessage + return $projectFileProperty.Value +} + +function Get-NovaVersionUpdateTarget { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext + ) + + $projectFile = Get-NovaVersionUpdateProjectFile -WorkflowContext $WorkflowContext + if (-not [string]::IsNullOrWhiteSpace($projectFile)) { + return [System.IO.Path]::GetFileName($projectFile) + } + + return Get-NovaVersionUpdateWorkflowPropertyValue -WorkflowContext $WorkflowContext -Name 'Target' } +function Get-NovaVersionUpdateWorkflowPropertyValue { + [CmdletBinding()] + param( + [Parameter(Mandatory)][pscustomobject]$WorkflowContext, + [Parameter(Mandatory)][string]$Name + ) + + $property = $WorkflowContext.PSObject.Properties[$Name] + if ($null -eq $property) { + return $null + } + + return $property.Value +} diff --git a/src/private/release/WriteNovaVersionUpdateResultMessages.ps1 b/src/private/release/WriteNovaVersionUpdateResultMessages.ps1 index 589bb10d..958c74cf 100644 --- a/src/private/release/WriteNovaVersionUpdateResultMessages.ps1 +++ b/src/private/release/WriteNovaVersionUpdateResultMessages.ps1 @@ -33,8 +33,14 @@ function Write-NovaVersionUpdateResultOutput { Write-Warning $advisoryMessage } - if ($Result.Applied) { - Write-Host "Version bumped to : $( $Result.NewVersion )" + Write-Message (Get-NovaVersionUpdateResultStatusMessage -Result $Result) -color (Get-NovaVersionUpdateResultStatusColor -Result $Result) + + foreach ($line in (Get-NovaVersionUpdateResultDetailText -Result $Result)) { + Write-Message $line + } + + foreach ($line in (Get-NovaVersionUpdateResultNextStepText -Result $Result)) { + Write-Message $line } } @@ -44,11 +50,137 @@ function Get-NovaVersionUpdateResultAdvisoryMessage { [Parameter(Mandatory)][object]$Result ) - if ($Result.PSObject.Properties.Name -notcontains 'AdvisoryMessage') { - return $null + return Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'AdvisoryMessage' +} + +function Get-NovaVersionUpdateResultStatusMessage { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result + ) + + if ([bool](Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'Cancelled')) { + return 'Version update cancelled before changing project.json.' + } + + if ([bool](Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'Previewed')) { + return "Version update plan ready -> $( Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'NewVersion' )" + } + + return "Updated project version to $( Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'NewVersion' )" +} + +function Get-NovaVersionUpdateResultStatusColor { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result + ) + + if ([bool](Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'Cancelled')) { + return 'Blue' + } + + return 'Green' +} + +function Get-NovaVersionUpdateResultDetailText { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result + ) + + $lines = @() + $targetLine = Get-NovaVersionUpdateResultTargetLine -Result $Result + if (-not [string]::IsNullOrWhiteSpace($targetLine)) { + $lines += $targetLine + } + + $lines += "Previous version: $( Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'PreviousVersion' )" + $lines += "New version: $( Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'NewVersion' )" + $lines += Get-NovaVersionUpdateResultLabelText -Result $Result + + $commitCount = Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'CommitCount' + if ($null -ne $commitCount) { + $lines += "Commits considered: $commitCount" + } + + return $lines +} + +function Get-NovaVersionUpdateResultTargetLine { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result + ) + + $projectFile = Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'ProjectFile' + if (-not [string]::IsNullOrWhiteSpace($projectFile)) { + return "Version file: $projectFile" + } + + $target = Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'Target' + if (-not [string]::IsNullOrWhiteSpace($target)) { + return "Version file: $target" } - return $Result.AdvisoryMessage + return $null } +function Get-NovaVersionUpdateResultLabelText { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result + ) + $effectiveLabel = [string](Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'EffectiveLabel') + $detectedLabel = [string](Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'Label') + if ($effectiveLabel -eq $detectedLabel) { + return @("Release label: $effectiveLabel") + } + + return @( + "Detected release label: $detectedLabel" + "Applied release label: $effectiveLabel" + ) +} + +function Get-NovaVersionUpdateResultNextStepText { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result + ) + + if ([bool](Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'Cancelled')) { + return @( + 'Next step:' + 'Run Update-NovaModuleVersion again when you are ready to write the new version to project.json.' + ) + } + + if ([bool](Get-NovaVersionUpdateResultPropertyValue -Result $Result -Name 'Previewed')) { + return @( + 'Next step:' + 'Run Update-NovaModuleVersion without -WhatIf when you are ready to apply the version change.' + ) + } + + return @( + 'Next step:' + 'Invoke-NovaBuild' + ) +} + +function Get-NovaVersionUpdateResultPropertyValue { + [CmdletBinding()] + param( + [Parameter(Mandatory)][object]$Result, + [Parameter(Mandatory)][string]$Name + ) + + $property = $Result.PSObject.Properties[$Name] + if ($null -eq $property) { + return $null + } + + return $property.Value +} diff --git a/tests/private/release/InvokeNovaVersionUpdateWorkflow.Tests.ps1 b/tests/private/release/InvokeNovaVersionUpdateWorkflow.Tests.ps1 index 164ffcee..63aeb188 100644 --- a/tests/private/release/InvokeNovaVersionUpdateWorkflow.Tests.ps1 +++ b/tests/private/release/InvokeNovaVersionUpdateWorkflow.Tests.ps1 @@ -5,17 +5,12 @@ BeforeAll { function Set-NovaModuleVersion {param($ProjectInfo, $Label, [switch]$PreviewRelease, [switch]$Confirm) return [pscustomobject]@{Applied=$true}} } -Describe 'Test-NovaVersionUpdateResultRequired' { - It 'returns true when ShouldRun is set' { - Test-NovaVersionUpdateResultRequired -ShouldRun | Should -BeTrue - } - - It 'returns true when WhatIfEnabled is set' { - Test-NovaVersionUpdateResultRequired -WhatIfEnabled | Should -BeTrue - } - - It 'returns false when neither is set' { - Test-NovaVersionUpdateResultRequired | Should -BeFalse +Describe 'Invoke-NovaVersionUpdateWorkflowStep' { + It 'reports progress before invoking the action' { + Mock Write-Progress {} + $result = Invoke-NovaVersionUpdateWorkflowStep -Activity 'Updating' -Status 'Writing version' -PercentComplete 80 -Action { 'done' } + $result | Should -Be 'done' + Should -Invoke Write-Progress -Times 1 -ParameterFilter { $Activity -eq 'Updating' -and $Status -eq 'Writing version' -and $PercentComplete -eq 80 } } } @@ -41,32 +36,49 @@ Describe 'Get-NovaVersionUpdateAdvisoryMessage' { Describe 'Get-NovaVersionUpdateResult' { It 'assembles a structured result' { - $ctx = [pscustomobject]@{PreviousVersion='1.0.0'; NewVersion='1.0.1'; Label='Patch'; CommitCount=2} - $result = Get-NovaVersionUpdateResult -WorkflowContext $ctx -Applied + $ctx = [pscustomobject]@{ + PreviousVersion = '1.0.0' + NewVersion = '1.0.1' + Label = 'Patch' + CommitCount = 2 + ProjectInfo = [pscustomobject]@{ProjectJSON='/p/project.json'} + } + $result = Get-NovaVersionUpdateResult -WorkflowContext $ctx -Applied -Previewed $result.PreviousVersion | Should -Be '1.0.0' $result.NewVersion | Should -Be '1.0.1' $result.Label | Should -Be 'Patch' $result.EffectiveLabel | Should -Be 'Patch' $result.CommitCount | Should -Be 2 + $result.ProjectFile | Should -Be '/p/project.json' + $result.Target | Should -Be 'project.json' $result.Applied | Should -BeTrue + $result.Previewed | Should -BeTrue } } Describe 'Invoke-NovaVersionUpdateWorkflow' { - It 'returns null when neither ShouldRun nor WhatIfEnabled is set' { + It 'returns a cancelled result when confirmation is declined before the write step' { $ctx = [pscustomobject]@{ProjectInfo=[pscustomobject]@{}; PreviewRelease=$false; Label='Patch'; EffectiveLabel='Patch'} - Invoke-NovaVersionUpdateWorkflow -WorkflowContext $ctx | Should -BeNullOrEmpty + $result = Invoke-NovaVersionUpdateWorkflow -WorkflowContext $ctx + $result.Applied | Should -BeFalse + $result.Cancelled | Should -BeTrue } + It 'returns a result with Applied=true when ShouldRun and the write succeeds' { Mock Set-NovaModuleVersion {[pscustomobject]@{Applied=$true}} - $ctx = [pscustomobject]@{ProjectInfo=[pscustomobject]@{}; PreviewRelease=$false; Label='Patch'; EffectiveLabel='Patch'; PreviousVersion='1.0.0'; NewVersion='1.0.1'; CommitCount=1} + Mock Write-Progress {} + $ctx = [pscustomobject]@{ProjectInfo=[pscustomobject]@{ProjectJSON='/p/project.json'}; PreviewRelease=$false; Label='Patch'; EffectiveLabel='Patch'; PreviousVersion='1.0.0'; NewVersion='1.0.1'; CommitCount=1} $r = Invoke-NovaVersionUpdateWorkflow -WorkflowContext $ctx -ShouldRun $r.Applied | Should -BeTrue + Should -Invoke Write-Progress -Times 1 -ParameterFilter { $Status -eq 'Writing version 1.0.1 to project.json' } + Should -Invoke Write-Progress -Times 1 -ParameterFilter { $Completed } } + It 'returns a result with Applied=false in WhatIf mode without invoking the writer' { Mock Set-NovaModuleVersion {throw 'should not be called'} $ctx = [pscustomobject]@{ProjectInfo=[pscustomobject]@{}; PreviewRelease=$false; Label='Patch'; EffectiveLabel='Patch'; PreviousVersion='1.0.0'; NewVersion='1.0.1'; CommitCount=0} $r = Invoke-NovaVersionUpdateWorkflow -WorkflowContext $ctx -WhatIfEnabled $r.Applied | Should -BeFalse + $r.Previewed | Should -BeTrue } } diff --git a/tests/private/release/WriteNovaVersionUpdateResultMessages.Tests.ps1 b/tests/private/release/WriteNovaVersionUpdateResultMessages.Tests.ps1 index 67599977..1a37b439 100644 --- a/tests/private/release/WriteNovaVersionUpdateResultMessages.Tests.ps1 +++ b/tests/private/release/WriteNovaVersionUpdateResultMessages.Tests.ps1 @@ -3,6 +3,7 @@ BeforeAll { . (Join-Path $projectRoot 'src/private/release/WriteNovaVersionUpdateResultMessages.ps1') function Get-NovaVersionUpdateCiActivatedCommand {param($ProjectRoot) return $null} + function Write-Message {param($Message, $color) $script:messages += [pscustomobject]@{Text=$Message; Color=$color}} } Describe 'Invoke-NovaVersionUpdateCiActivation' { @@ -43,15 +44,76 @@ Describe 'Get-NovaVersionUpdateResultAdvisoryMessage' { } Describe 'Write-NovaVersionUpdateResultOutput' { + BeforeEach { + $script:messages = @() + } + It 'writes a warning when an advisory message is present' { - $r = [pscustomobject]@{AdvisoryMessage='watch out'; Applied=$false} + $r = [pscustomobject]@{AdvisoryMessage='watch out'; Applied=$false; Previewed=$true; NewVersion='1.2.3'; PreviousVersion='1.2.2'; Label='Patch'; EffectiveLabel='Patch'; CommitCount=2} Write-NovaVersionUpdateResultOutput -Result $r -WarningVariable w -WarningAction SilentlyContinue 6> $null $w[0].Message | Should -Be 'watch out' } - It 'writes the bumped version host message when Applied is true' { - $r = [pscustomobject]@{AdvisoryMessage=''; Applied=$true; NewVersion='1.2.3'} - $output = Write-NovaVersionUpdateResultOutput -Result $r 6>&1 | Out-String - $output | Should -Match 'Version bumped to : 1.2.3' + It 'writes a completion summary, details, and next step when Applied is true' { + $r = [pscustomobject]@{ + AdvisoryMessage = '' + Applied = $true + Previewed = $false + Cancelled = $false + NewVersion = '1.2.3' + PreviousVersion = '1.2.2' + Label = 'Patch' + EffectiveLabel = 'Patch' + CommitCount = 2 + ProjectFile = '/p/project.json' + } + Write-NovaVersionUpdateResultOutput -Result $r + $script:messages[0].Text | Should -Be 'Updated project version to 1.2.3' + $script:messages[0].Color | Should -Be 'Green' + $script:messages.Text | Should -Contain 'Version file: /p/project.json' + $script:messages.Text | Should -Contain 'Previous version: 1.2.2' + $script:messages.Text | Should -Contain 'New version: 1.2.3' + $script:messages.Text | Should -Contain 'Release label: Patch' + $script:messages.Text | Should -Contain 'Commits considered: 2' + $script:messages.Text | Should -Contain 'Invoke-NovaBuild' + } + + It 'writes a preview summary and next step when WhatIf created the result' { + $r = [pscustomobject]@{ + AdvisoryMessage = '' + Applied = $false + Previewed = $true + Cancelled = $false + NewVersion = '0.2.0' + PreviousVersion = '0.1.0' + Label = 'Major' + EffectiveLabel = 'Minor' + CommitCount = 34 + Target = 'project.json' + } + Write-NovaVersionUpdateResultOutput -Result $r + $script:messages[0].Text | Should -Be 'Version update plan ready -> 0.2.0' + $script:messages.Text | Should -Contain 'Version file: project.json' + $script:messages.Text | Should -Contain 'Detected release label: Major' + $script:messages.Text | Should -Contain 'Applied release label: Minor' + $script:messages.Text | Should -Contain 'Run Update-NovaModuleVersion without -WhatIf when you are ready to apply the version change.' + } + + It 'writes a cancellation summary when the command does not proceed past confirmation' { + $r = [pscustomobject]@{ + AdvisoryMessage = '' + Applied = $false + Previewed = $false + Cancelled = $true + NewVersion = '1.2.3' + PreviousVersion = '1.2.2' + Label = 'Patch' + EffectiveLabel = 'Patch' + CommitCount = 2 + } + Write-NovaVersionUpdateResultOutput -Result $r + $script:messages[0].Text | Should -Be 'Version update cancelled before changing project.json.' + $script:messages[0].Color | Should -Be 'Blue' + $script:messages.Text | Should -Contain 'Run Update-NovaModuleVersion again when you are ready to write the new version to project.json.' } } diff --git a/tests/public/UpdateNovaModuleVersion.TestSupport.ps1 b/tests/public/UpdateNovaModuleVersion.TestSupport.ps1 index f1342320..7ee54883 100644 --- a/tests/public/UpdateNovaModuleVersion.TestSupport.ps1 +++ b/tests/public/UpdateNovaModuleVersion.TestSupport.ps1 @@ -7,6 +7,7 @@ function Get-NovaVersionUpdateWorkflowContext {param($ProjectRoot, [switch]$Prev } function Invoke-NovaVersionUpdateWorkflow {param($WorkflowContext, [switch]$ShouldRun, [switch]$WhatIfEnabled) $script:invoked = $true + $script:workflowArgs = @{ShouldRun=[bool]$ShouldRun; WhatIfEnabled=[bool]$WhatIfEnabled} return $script:workflowResult } function Write-NovaVersionUpdateResultOutput {param($Result) $script:outputResult = $Result} diff --git a/tests/public/UpdateNovaModuleVersion.Tests.ps1 b/tests/public/UpdateNovaModuleVersion.Tests.ps1 index 8879a2da..aeec709f 100644 --- a/tests/public/UpdateNovaModuleVersion.Tests.ps1 +++ b/tests/public/UpdateNovaModuleVersion.Tests.ps1 @@ -9,6 +9,7 @@ Describe 'Update-NovaModuleVersion' { BeforeEach { $script:ciActivation = [pscustomobject]@{ShouldReturn=$false; Result=$null} $script:ctxArgs = $null; $script:invoked = $false + $script:workflowArgs = $null $script:workflowResult = [pscustomobject]@{NewVersion='1.1.0'} $script:outputResult = $null } @@ -27,6 +28,12 @@ Describe 'Update-NovaModuleVersion' { $result.NewVersion | Should -Be '1.1.0' } + It 'forwards the WhatIf preview flags to the workflow' { + Update-NovaModuleVersion -Path . -WhatIf | Out-Null + $script:workflowArgs.ShouldRun | Should -BeFalse + $script:workflowArgs.WhatIfEnabled | Should -BeTrue + } + It 'returns nothing when the workflow returns null' { $script:workflowResult = $null Update-NovaModuleVersion -Path . | Should -BeNullOrEmpty From 19aa9a64ef3ccf618f6645321f9dfde8bbf2990b Mon Sep 17 00:00:00 2001 From: Stiwi Gabriel Courage Date: Sun, 24 May 2026 17:00:49 +0200 Subject: [PATCH 16/16] #215 feat: add tests for NovaPackage and NovaTest workflows, enhancing coverage and status messaging --- .../InvokeNovaPackageWorkflow.Tests.ps1 | 23 +++++++++++++++ .../quality/InvokeNovaTestWorkflow.Tests.ps1 | 23 +++++++++++++++ .../InvokeNovaReleaseWorkflow.Tests.ps1 | 28 +++++++++++++++++++ ...vokeNovaModuleSelfUpdateWorkflow.Tests.ps1 | 19 +++++++++++++ ...dateNotificationPreferenceChange.Tests.ps1 | 7 +++++ 5 files changed, 100 insertions(+) diff --git a/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 b/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 index a1f078fe..5dcad366 100644 --- a/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 +++ b/tests/private/package/InvokeNovaPackageWorkflow.Tests.ps1 @@ -65,3 +65,26 @@ Describe 'Invoke-NovaPackageWorkflow' { $result | Should -BeNullOrEmpty } } + +Describe 'Get-NovaPackageWorkflowStatusMessage' { + It 'reports the created artifact count when more than one artifact was produced' { + $workflowContext = [pscustomobject]@{ + ProjectInfo = [pscustomobject]@{ + ProjectName = 'Demo' + } + } + + Get-NovaPackageWorkflowStatusMessage -WorkflowContext $workflowContext -ArtifactCount 2 | Should -Be 'Created 2 package artifacts for Demo' + } +} + +Describe 'Get-NovaPackageWorkflowResultTarget' { + It 'falls back to the workflow target when artifacts do not expose output directories' { + $workflowContext = [pscustomobject]@{ + Target = '/p/Demo.1.0.0.nupkg' + } + + $artifacts = @([pscustomobject]@{PackagePath = '/p/Demo.1.0.0.nupkg'; OutputDirectory = $null}) + Get-NovaPackageWorkflowResultTarget -WorkflowContext $workflowContext -Artifacts $artifacts | Should -Be '/p/Demo.1.0.0.nupkg' + } +} diff --git a/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 b/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 index 9e7e6f69..c10bc180 100644 --- a/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 +++ b/tests/private/quality/InvokeNovaTestWorkflow.Tests.ps1 @@ -322,3 +322,26 @@ Describe 'Invoke-NovaTestWorkflow' { } } + +Describe 'Get-NovaTestWorkflowCoverageMessage' { + It 'includes the measured and configured coverage values when both are available' { + $workflowContext = [pscustomobject]@{ + ProjectInfo = [pscustomobject]@{ + Pester = [ordered]@{ + CodeCoverage = [ordered]@{ + Enabled = $true + CoveragePercentTarget = 90 + } + } + } + } + + $testResult = [pscustomobject]@{ + CodeCoverage = [pscustomobject]@{ + CoveragePercent = 66.67 + } + } + + Get-NovaTestWorkflowCoverageMessage -WorkflowContext $workflowContext -TestResult $testResult | Should -Be 'Measured code coverage: 66.67% (target: 90%)' + } +} diff --git a/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 b/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 index 072fcec0..2bd26fcd 100644 --- a/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 +++ b/tests/private/release/InvokeNovaReleaseWorkflow.Tests.ps1 @@ -23,6 +23,34 @@ Describe 'Get-NovaReleaseBuildWorkflowParameterMap' { } } +Describe 'Get-NovaReleaseWorkflowResultVersion' { + It 'returns null when no version result is available' { + Get-NovaReleaseWorkflowResultVersion -VersionResult $null | Should -BeNullOrEmpty + } + + It 'prefers NewVersion when the version result exposes it' { + Get-NovaReleaseWorkflowResultVersion -VersionResult ([pscustomobject]@{NewVersion = '1.2.3'; Version = '1.2.2'}) | Should -Be '1.2.3' + } + + It 'falls back to Version when NewVersion is not present' { + Get-NovaReleaseWorkflowResultVersion -VersionResult ([pscustomobject]@{Version = '1.2.3'}) | Should -Be '1.2.3' + } + + It 'returns null when the version result has no version-like properties' { + Get-NovaReleaseWorkflowResultVersion -VersionResult ([pscustomobject]@{Other = 'x'}) | Should -BeNullOrEmpty + } +} + +Describe 'Get-NovaReleaseWorkflowStatusMessage' { + It 'returns a plan-ready message without a version in WhatIf mode' { + Get-NovaReleaseWorkflowStatusMessage -ProjectInfo ([pscustomobject]@{ProjectName = 'NovaModuleTools'}) -WhatIfEnabled | Should -Be 'Release plan ready for NovaModuleTools' + } + + It 'returns a released message without a version when no version was resolved' { + Get-NovaReleaseWorkflowStatusMessage -ProjectInfo ([pscustomobject]@{ProjectName = 'NovaModuleTools'}) | Should -Be 'Released Nova module: NovaModuleTools' + } +} + Describe 'Test-NovaReleaseWorkflowShouldRestoreBuiltModule' { It 'returns true when CI is on and WhatIf is not set' { Test-NovaReleaseWorkflowShouldRestoreBuiltModule -WorkflowParams @{} -ContinuousIntegrationRequested | Should -BeTrue diff --git a/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 b/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 index 526f60d0..8cf42bc9 100644 --- a/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 +++ b/tests/private/update/InvokeNovaModuleSelfUpdateWorkflow.Tests.ps1 @@ -43,6 +43,25 @@ Describe 'Invoke-NovaModuleSelfUpdateOrStop' { } } +Describe 'Get-NovaModuleSelfUpdateWorkflowUpdateStatus' { + It 'describes prerelease installs explicitly' { + $workflowContext = [pscustomobject]@{ + Plan = [pscustomobject]@{ + IsPrereleaseTarget = $true + TargetVersion = '1.2.0-preview1' + } + } + + Get-NovaModuleSelfUpdateWorkflowUpdateStatus -WorkflowContext $workflowContext | Should -Be 'Installing prerelease version 1.2.0-preview1' + } +} + +Describe 'Get-NovaModuleSelfUpdateWorkflowRepositoryLine' { + It 'returns null when the repository name is blank' { + Get-NovaModuleSelfUpdateWorkflowRepositoryLine -Result ([pscustomobject]@{LookupRepository = ''}) | Should -BeNullOrEmpty + } +} + Describe 'Invoke-NovaModuleSelfUpdateWorkflow' { BeforeEach { Mock Write-Message {} diff --git a/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 b/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 index ab35dee0..be4a8080 100644 --- a/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 +++ b/tests/private/update/InvokeNovaUpdateNotificationPreferenceChange.Tests.ps1 @@ -19,6 +19,13 @@ Describe 'Test-NovaUpdateNotificationPreferenceChangeWhatIfEnabled' { } } +Describe 'Get-NovaUpdateNotificationPreferenceChangeCommandLine' { + It 'returns the disable command line when prerelease notifications are being turned off' { + $workflowContext = [pscustomobject]@{PrereleaseNotificationsEnabled = $false} + Get-NovaUpdateNotificationPreferenceChangeCommandLine -WorkflowContext $workflowContext | Should -Be 'Set-NovaUpdateNotificationPreference -DisablePrereleaseNotifications' + } +} + Describe 'Invoke-NovaUpdateNotificationPreferenceChange' { BeforeEach { Mock Write-Message {}