diff --git a/CHANGELOG.md b/CHANGELOG.md index a49889d..cb70aa3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,13 @@ Keep stable `Update-NovaModuleVersion` / `% nova bump` releases on the SemVer ma minor version instead of auto-jumping to `1.0.0`. - Stable `0.y.z` bump results now print one warning about manually setting `1.0.0` once the software is stable, while breaking-change bumps still report the detected `Major` label. - - `-Preview` behavior is unchanged. +- Make `Update-NovaModuleVersion -Preview` / `% nova bump --preview` enter the preview track deterministically from + stable versions. + - Stable versions now always become the next patch preview, for example `0.2.0 -> 0.2.1-preview`, instead of + reusing semantic history inference for the semantic core. + - Existing prerelease versions still keep their current semantic core and continue the prerelease sequence. + - Running the bump without `-Preview` still finalizes or advances prerelease versions by Nova's normal semantic + rules. ### Deprecated diff --git a/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md b/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md index 1e5df23..695839b 100644 --- a/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md +++ b/docs/NovaModuleTools/en-US/Update-NovaModuleVersion.md @@ -30,8 +30,9 @@ project repository, chooses a semantic version bump label, calculates the next s version back to `project.json`. Use `-Preview` when you want an explicit prerelease-continuation bump instead of the default prerelease finalization -behavior. In preview mode, stable versions first calculate the normal semantic bump target and then append -`-preview`. Existing prerelease versions keep the same semantic core and preserve the current prerelease stem while +behavior. In preview mode, stable versions always enter the next patch preview track by incrementing the patch version +and appending `-preview`. Existing prerelease versions keep the same semantic core and preserve the current prerelease +stem while appending or incrementing trailing digits. Any bare prerelease label now starts at `01` for predictable ordering, for example `preview -> preview01`, `preview09 -> preview10`, `rc -> rc01`, `rc1 -> rc2`, and `SNAPSHOT -> SNAPSHOT01`. @@ -45,7 +46,8 @@ The release label is inferred from the commit set: When the current stable version is still on `0.y.z` and the inferred label is `Major`, Nova keeps the release on the initial-development line and plans the next minor version instead of jumping straight to `1.0.0`. The result still reports the detected `Major` label so you can see that the commit set contained a breaking change, and Nova prints -guidance about manually setting `1.0.0` once the software is stable. `-Preview` behavior stays unchanged. +guidance about manually setting `1.0.0` once the software is stable. With `-Preview`, stable versions still enter the +next patch preview track instead of applying semantic history inference to the semantic core. When Git tags exist, only commits since the latest tag are considered. If the folder is not a Git repository, the command falls back to a patch bump. @@ -120,13 +122,13 @@ PS> Update-NovaModuleVersion -Preview -WhatIf What if: Performing the operation "Update module version using Minor release label" on target "project.json". PreviousVersion: 1.5.3 -NewVersion: 1.6.0-preview +NewVersion: 1.5.4-preview Label: Minor CommitCount: 12 ``` -Shows how `-Preview` keeps the normal bump label selection but emits a preview target 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. ### EXAMPLE 6 @@ -211,7 +213,7 @@ HelpMessage: '' Opt into preview bump mode. -When the current version is stable, Nova calculates the normal semantic target and appends `-preview`. When the current +When the current version is stable, Nova increments the patch version and appends `-preview`. When the current version already has any prerelease label, Nova keeps the same semantic core version and increments the current prerelease suffix instead of finalizing or advancing to another release line. Any prerelease stem without a trailing number starts at `01`, while labels that already include a number simply increment that number. diff --git a/src/private/release/GetNovaVersionUpdatePlan.ps1 b/src/private/release/GetNovaVersionUpdatePlan.ps1 index 4e595b0..58c5972 100644 --- a/src/private/release/GetNovaVersionUpdatePlan.ps1 +++ b/src/private/release/GetNovaVersionUpdatePlan.ps1 @@ -57,8 +57,12 @@ function Get-NovaVersionPartForUpdatePlan { [switch]$PreviewRelease ) - if ($PreviewRelease -and -not [string]::IsNullOrWhiteSpace($CurrentVersion.PreReleaseLabel)) { - return Get-NovaVersionPartObject -CurrentVersion $CurrentVersion + if ($PreviewRelease) { + if (-not [string]::IsNullOrWhiteSpace($CurrentVersion.PreReleaseLabel)) { + return Get-NovaVersionPartObject -CurrentVersion $CurrentVersion + } + + return Get-NovaVersionPartForLabel -CurrentVersion $CurrentVersion -Label Patch } return Get-NovaVersionPartForLabel -CurrentVersion $CurrentVersion -Label $Label diff --git a/src/private/release/GetNovaVersionUpdateWorkflowContext.ps1 b/src/private/release/GetNovaVersionUpdateWorkflowContext.ps1 index 36535ca..f6b3223 100644 --- a/src/private/release/GetNovaVersionUpdateWorkflowContext.ps1 +++ b/src/private/release/GetNovaVersionUpdateWorkflowContext.ps1 @@ -26,6 +26,10 @@ function Get-NovaVersionUpdateLabelResolution { $effectiveLabel = $Label $advisoryMessage = $null $currentVersion = Get-NovaCurrentVersionForUpdatePlan -ProjectInfo $ProjectInfo + if (Test-NovaVersionUpdateUsesPreviewPatchFallback -CurrentVersion $currentVersion -PreviewRelease:$PreviewRelease) { + $effectiveLabel = 'Patch' + } + if (Test-NovaVersionUpdateUsesInitialDevelopmentAdvisory -CurrentVersion $currentVersion -PreviewRelease:$PreviewRelease) { $advisoryMessage = Get-NovaInitialDevelopmentVersioningMessage } @@ -40,6 +44,20 @@ function Get-NovaVersionUpdateLabelResolution { } } +function Test-NovaVersionUpdateUsesPreviewPatchFallback { + [CmdletBinding()] + param( + [Parameter(Mandatory)][semver]$CurrentVersion, + [switch]$PreviewRelease + ) + + if (-not $PreviewRelease) { + return $false + } + + return [string]::IsNullOrWhiteSpace($CurrentVersion.PreReleaseLabel) +} + function Test-NovaVersionUpdateUsesInitialDevelopmentAdvisory { [CmdletBinding()] param( diff --git a/tests/CoverageGaps.ReleaseInternals.Tests.ps1 b/tests/CoverageGaps.ReleaseInternals.Tests.ps1 index efcc34f..5527d32 100644 --- a/tests/CoverageGaps.ReleaseInternals.Tests.ps1 +++ b/tests/CoverageGaps.ReleaseInternals.Tests.ps1 @@ -129,9 +129,9 @@ Describe 'Coverage gaps for release and git internals' { } } - It 'Get-NovaVersionUpdatePlan appends a preview label to the normal bump target when preview mode starts from stable' -ForEach @( - @{CurrentVersion = '1.5.3'; Label = 'Major'; ExpectedVersion = '2.0.0-preview'} - @{CurrentVersion = '1.5.3'; Label = 'Minor'; ExpectedVersion = '1.6.0-preview'} + It 'Get-NovaVersionUpdatePlan enters the next patch preview track from stable versions regardless of the inferred label' -ForEach @( + @{CurrentVersion = '1.5.3'; Label = 'Major'; ExpectedVersion = '1.5.4-preview'} + @{CurrentVersion = '1.5.3'; Label = 'Minor'; ExpectedVersion = '1.5.4-preview'} @{CurrentVersion = '1.5.3'; Label = 'Patch'; ExpectedVersion = '1.5.4-preview'} ) { InModuleScope $script:moduleName -Parameters @{TestCase = $_} { @@ -170,14 +170,14 @@ Describe 'Coverage gaps for release and git internals' { ExpectedPlanLabel = 'Minor' } @{ - Name = 'preview mode remains unchanged' + Name = 'preview mode enters the next patch preview from stable major-zero versions' CurrentVersion = '0.1.0' Label = 'Major' PreviewRelease = $true - PlannedVersion = '1.0.0-preview' - ExpectedEffectiveLabel = 'Major' + PlannedVersion = '0.1.1-preview' + ExpectedEffectiveLabel = 'Patch' ExpectedAdvisoryPattern = $null - ExpectedPlanLabel = 'Major' + ExpectedPlanLabel = 'Patch' } ) { InModuleScope $script:moduleName -Parameters @{TestCase = $_} { @@ -284,9 +284,9 @@ Describe 'Coverage gaps for release and git internals' { $updatedProject = Get-Content -LiteralPath $projectJsonPath -Raw | ConvertFrom-Json $result.PreviousVersion | Should -Be '1.2.3' - $result.NewVersion | Should -Be '1.3.0-preview' + $result.NewVersion | Should -Be '1.2.4-preview' $result.Applied | Should -BeTrue - $updatedProject.Version | Should -Be '1.3.0-preview' + $updatedProject.Version | Should -Be '1.2.4-preview' $updatedProject.Package.Auth.HeaderName | Should -Be 'Authorization' $updatedProject.Package.Repositories.Count | Should -Be 1 ($updatedProject.Package.Repositories[0] -is [string]) | Should -BeFalse @@ -301,7 +301,7 @@ Describe 'Coverage gaps for release and git internals' { Mock Get-NovaVersionUpdatePlan { [pscustomobject]@{ ProjectFile = '/tmp/project.json' - NewVersion = [semver]'1.3.0-preview' + NewVersion = [semver]'1.2.4-preview' } } Mock Read-ProjectJsonData { @@ -322,12 +322,12 @@ Describe 'Coverage gaps for release and git internals' { $result.ProjectFile | Should -Be '/tmp/project.json' $result.PreviousVersion | Should -Be '1.2.3' - $result.NewVersion | Should -Be '1.3.0-preview' + $result.NewVersion | Should -Be '1.2.4-preview' $result.Applied | Should -BeTrue Assert-MockCalled Read-ProjectJsonData -Times 1 -ParameterFilter {$ProjectJsonPath -eq '/tmp/project.json'} Assert-MockCalled Write-ProjectJsonData -Times 1 -ParameterFilter { $ProjectJsonPath -eq '/tmp/project.json' -and - $Data.Version -eq '1.3.0-preview' -and + $Data.Version -eq '1.2.4-preview' -and $Data.Package.Repositories[0].Name -eq 'staging' } } @@ -338,7 +338,7 @@ Describe 'Coverage gaps for release and git internals' { Mock Get-NovaVersionUpdatePlan { [pscustomobject]@{ ProjectFile = '/tmp/project.json' - NewVersion = [semver]'1.3.0-preview' + NewVersion = [semver]'1.2.4-preview' } } Mock Read-ProjectJsonData { @@ -352,7 +352,7 @@ Describe 'Coverage gaps for release and git internals' { $result.ProjectFile | Should -Be '/tmp/project.json' $result.PreviousVersion | Should -Be '1.2.3' - $result.NewVersion | Should -Be '1.3.0-preview' + $result.NewVersion | Should -Be '1.2.4-preview' $result.Applied | Should -BeFalse Assert-MockCalled Read-ProjectJsonData -Times 1 -ParameterFilter {$ProjectJsonPath -eq '/tmp/project.json'} Assert-MockCalled Write-ProjectJsonData -Times 0 diff --git a/tests/NovaCommandModel.BumpAndCli.Tests.ps1 b/tests/NovaCommandModel.BumpAndCli.Tests.ps1 index 58a8831..466f46d 100644 --- a/tests/NovaCommandModel.BumpAndCli.Tests.ps1 +++ b/tests/NovaCommandModel.BumpAndCli.Tests.ps1 @@ -138,6 +138,36 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { } } + It 'Get-NovaVersionUpdateWorkflowContext keeps the detected label but enters the next patch preview track from a stable version' { + InModuleScope $script:moduleName { + Mock Get-NovaProjectInfo { + [pscustomobject]@{ + ProjectName = 'NovaModuleTools' + Version = '1.5.3' + ProjectJSON = '/tmp/project.json' + } + } + Mock Get-GitCommitMessageForVersionBump {@('feat: add change')} + Mock Get-NovaVersionLabelForBump {'Minor'} + Mock Get-NovaVersionUpdatePlan { + [pscustomobject]@{ + NewVersion = [semver]'1.5.4-preview' + } + } + + $result = Get-NovaVersionUpdateWorkflowContext -ProjectRoot '/tmp/project' -PreviewRelease + + $result.Label | Should -Be 'Minor' + $result.EffectiveLabel | Should -Be 'Patch' + $result.NewVersion | Should -Be '1.5.4-preview' + Assert-MockCalled Get-NovaVersionUpdatePlan -Times 1 -ParameterFilter { + $ProjectInfo.ProjectName -eq 'NovaModuleTools' -and + $Label -eq 'Patch' -and + $PreviewRelease + } + } + } + It 'Get-NovaVersionUpdateWorkflowContext carries ContinuousIntegrationRequested when requested' { InModuleScope $script:moduleName { Mock Get-NovaProjectInfo { @@ -423,11 +453,11 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { } It 'Update-NovaModuleVersion -WhatIf returns the expected next version without persisting it when ' -ForEach @( - @{Name = 'the default bump flow is used'; CurrentVersion = '1.0.0'; CommitMessages = @('feat: add change'); Label = 'Minor'; NewVersion = '1.1.0'; Preview = $false} - @{Name = 'preview mode starts from a stable version'; CurrentVersion = '1.5.3'; CommitMessages = @('feat: add change'); Label = 'Minor'; NewVersion = '1.6.0-preview'; Preview = $true} - @{Name = 'preview mode zero-pads compact preview labels for gallery ordering'; CurrentVersion = '1.5.3-preview'; CommitMessages = @('fix: patch bug'); Label = 'Patch'; NewVersion = '1.5.3-preview01'; Preview = $true} - @{Name = 'preview mode starts any bare prerelease stem at 01'; CurrentVersion = '1.5.3-SNAPSHOT'; CommitMessages = @('feat!: breaking api'); Label = 'Major'; NewVersion = '1.5.3-SNAPSHOT01'; Preview = $true} - @{Name = 'preview mode continues an existing prerelease version'; CurrentVersion = '1.5.3-rc1'; CommitMessages = @('feat!: breaking api'); Label = 'Major'; NewVersion = '1.5.3-rc2'; Preview = $true} + @{Name = 'the default bump flow is used'; CurrentVersion = '1.0.0'; CommitMessages = @('feat: add change'); Label = 'Minor'; EffectiveLabel = 'Minor'; NewVersion = '1.1.0'; Preview = $false} + @{Name = 'preview mode enters the next patch preview track from a stable version'; CurrentVersion = '1.5.3'; CommitMessages = @('feat: add change'); Label = 'Minor'; EffectiveLabel = 'Patch'; NewVersion = '1.5.4-preview'; Preview = $true} + @{Name = 'preview mode zero-pads compact preview labels for gallery ordering'; CurrentVersion = '1.5.3-preview'; CommitMessages = @('fix: patch bug'); Label = 'Patch'; EffectiveLabel = 'Patch'; NewVersion = '1.5.3-preview01'; Preview = $true} + @{Name = 'preview mode starts any bare prerelease stem at 01'; CurrentVersion = '1.5.3-SNAPSHOT'; CommitMessages = @('feat!: breaking api'); Label = 'Major'; EffectiveLabel = 'Major'; NewVersion = '1.5.3-SNAPSHOT01'; Preview = $true} + @{Name = 'preview mode continues an existing prerelease version'; CurrentVersion = '1.5.3-rc1'; CommitMessages = @('feat!: breaking api'); Label = 'Major'; EffectiveLabel = 'Major'; NewVersion = '1.5.3-rc2'; Preview = $true} ) { InModuleScope $script:moduleName -Parameters @{TestCase = $_} { param($TestCase) @@ -459,9 +489,10 @@ Describe 'Nova command model - bump and CLI confirmation behavior' { $result.PreviousVersion | Should -Be $TestCase.CurrentVersion $result.NewVersion | Should -Be $TestCase.NewVersion $result.Label | Should -Be $TestCase.Label - Assert-MockCalled Get-NovaVersionUpdatePlan -Times 1 -ParameterFilter {$Label -eq $TestCase.Label} + $result.EffectiveLabel | Should -Be $TestCase.EffectiveLabel + Assert-MockCalled Get-NovaVersionUpdatePlan -Times 1 -ParameterFilter {$Label -eq $TestCase.EffectiveLabel} Assert-MockCalled Get-NovaVersionUpdatePlan -Times 1 -ParameterFilter { - $Label -eq $TestCase.Label -and + $Label -eq $TestCase.EffectiveLabel -and ([bool]$PreviewRelease) -eq ([bool]$TestCase.Preview) } diff --git a/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 b/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 index 2315134..ddd488a 100644 --- a/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 +++ b/tests/NovaCommandModel.TestSupport/CliProjectSupport.ps1 @@ -216,7 +216,7 @@ function Assert-TestNovaCliWhatIfResultMap { $ResultMap.Bump.Text | Should -Match 'Version plan: 0\.0\.1 -> 0\.1\.0 \| Label: Minor \| Commits: 1' $ResultMap.BumpCi.Text | Should -Match 'Version plan: 0\.0\.1 -> 0\.1\.0 \| Label: Minor \| Commits: 1' - $ResultMap.PreviewBump.Text | Should -Match 'Version plan: 0\.0\.1 -> 0\.1\.0-preview \| Label: Minor \| Commits: 1' + $ResultMap.PreviewBump.Text | Should -Match 'Version plan: 0\.0\.1 -> 0\.0\.2-preview \| Label: Minor \| Commits: 1' $ResultMap.Bump.Text | Should -Not -Match 'Version bumped to :' $ResultMap.PreviewBump.Text | Should -Not -Match 'Version bumped to :' ((Get-Content -LiteralPath $ProjectJsonPath -Raw | ConvertFrom-Json).Version) | Should -Be '0.0.1'