diff --git a/README.md b/README.md index cd54bdda..2de4e4ae 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ preference to decide whether prerelease self-updates are eligible. When prerelea self-update stays on stable releases. When they are enabled, self-update may target a prerelease, but it asks for explicit confirmation before proceeding. + Successful `Update-NovaModuleTool`, CLI:`% nova update`, and `Install-NovaCli` runs print the release notes link from the installed module manifest. When `Invoke-NovaBuild` detects a newer `NovaModuleTools` version after a build, the update diff --git a/scripts/build/ci/Install-CiPowerShellModules.ps1 b/scripts/build/ci/Install-CiPowerShellModules.ps1 index 31936870..4ff476c8 100644 --- a/scripts/build/ci/Install-CiPowerShellModules.ps1 +++ b/scripts/build/ci/Install-CiPowerShellModules.ps1 @@ -11,8 +11,9 @@ function Install-CiModule { [Parameter(Mandatory)][string]$Name ) + $repositoryName = 'PSGallery' Write-Host "Installing PowerShell module '$Name'..." - Install-Module -Name $Name -AllowPrerelease -Scope CurrentUser -Force -ErrorAction Stop | Out-Null + Install-Module -Name $Name -Repository $repositoryName -AllowPrerelease -Scope CurrentUser -Force -ErrorAction Stop | Out-Null $installedModule = Get-InstalledModule -Name $Name -ErrorAction Stop | Sort-Object Version -Descending | diff --git a/src/private/update/ConvertToNovaModuleSelfUpdatePlan.ps1 b/src/private/update/ConvertToNovaModuleSelfUpdatePlan.ps1 index e70b63d2..f436f712 100644 --- a/src/private/update/ConvertToNovaModuleSelfUpdatePlan.ps1 +++ b/src/private/update/ConvertToNovaModuleSelfUpdatePlan.ps1 @@ -1,8 +1,77 @@ +function Get-NovaModuleSelfUpdateLookupCandidate { + [CmdletBinding()] + param( + [pscustomobject]$LookupResult, + [Parameter(Mandatory)][bool]$PrereleaseNotificationsEnabled + ) + + if ($null -eq $LookupResult) { + return $null + } + + if ($PrereleaseNotificationsEnabled -and $null -ne $LookupResult.Prerelease) { + return $LookupResult.Prerelease + } + + if ($null -ne $LookupResult.Stable) { + return $LookupResult.Stable + } + + return $LookupResult.Prerelease +} + +function Get-NovaModuleSelfUpdateLookupRepository { + [CmdletBinding()] + param( + [pscustomobject]$LookupResult, + [object]$LookupCandidate + ) + + if ($null -ne $LookupCandidate -and $LookupCandidate.PSObject.Properties.Name -contains 'Repository') { + return $LookupCandidate.Repository + } + + if ($null -ne $LookupResult -and $LookupResult.PSObject.Properties.Name -contains 'SourceRepository') { + return $LookupResult.SourceRepository + } + + return $null +} + +function Get-NovaModuleSelfUpdatePlanContext { + [CmdletBinding()] + param( + [pscustomobject]$LookupResult, + [Parameter(Mandatory)][bool]$PrereleaseNotificationsEnabled + ) + + $lookupCandidate = Get-NovaModuleSelfUpdateLookupCandidate -LookupResult $LookupResult -PrereleaseNotificationsEnabled:$PrereleaseNotificationsEnabled + $lookupCandidateVersion = if ($null -ne $lookupCandidate -and $lookupCandidate.PSObject.Properties.Name -contains 'Version') { + $lookupCandidate.Version + } + else { + $null + } + $lookupCandidateChannel = if ($null -ne $lookupCandidate -and $lookupCandidate.PSObject.Properties.Name -contains 'Channel') { + $lookupCandidate.Channel + } + else { + $null + } + + return [pscustomobject]@{ + LookupCandidateVersion = $lookupCandidateVersion + LookupCandidateChannel = $lookupCandidateChannel + LookupRepository = Get-NovaModuleSelfUpdateLookupRepository -LookupResult $LookupResult -LookupCandidate $lookupCandidate + PrereleaseNotificationsEnabled = $PrereleaseNotificationsEnabled + } +} + function ConvertTo-NovaModuleSelfUpdatePlan { [CmdletBinding()] param( [Parameter(Mandatory)][pscustomobject]$InstalledModule, - [Parameter(Mandatory)][bool]$PrereleaseNotificationsEnabled, + [Parameter(Mandatory)][pscustomobject]$PlanContext, [string]$TargetVersion, [switch]$PrereleaseTarget ) @@ -11,7 +80,10 @@ function ConvertTo-NovaModuleSelfUpdatePlan { ModuleName = $InstalledModule.ModuleName CurrentVersion = $InstalledModule.Version TargetVersion = $TargetVersion - PrereleaseNotificationsEnabled = $PrereleaseNotificationsEnabled + LookupCandidateVersion = $PlanContext.LookupCandidateVersion + LookupCandidateChannel = $PlanContext.LookupCandidateChannel + LookupRepository = $PlanContext.LookupRepository + PrereleaseNotificationsEnabled = $PlanContext.PrereleaseNotificationsEnabled UpdateAvailable = -not [string]::IsNullOrWhiteSpace($TargetVersion) Updated = $false Cancelled = $false diff --git a/src/private/update/GetNovaModuleSelfUpdatePlan.ps1 b/src/private/update/GetNovaModuleSelfUpdatePlan.ps1 index 5707d560..439f671b 100644 --- a/src/private/update/GetNovaModuleSelfUpdatePlan.ps1 +++ b/src/private/update/GetNovaModuleSelfUpdatePlan.ps1 @@ -6,14 +6,15 @@ function Get-NovaModuleSelfUpdatePlan { [Parameter(Mandatory)][bool]$PrereleaseNotificationsEnabled ) + $planContext = Get-NovaModuleSelfUpdatePlanContext -LookupResult $LookupResult -PrereleaseNotificationsEnabled:$PrereleaseNotificationsEnabled $stableVersion = Get-NovaAvailableSemanticVersion -VersionInfo $LookupResult.Stable if (Test-NovaPrereleaseUpdateAvailable -LookupResult $LookupResult -InstalledVersion $InstalledModule.SemanticVersion -StableVersion $stableVersion -PrereleaseNotificationsEnabled $PrereleaseNotificationsEnabled) { - return ConvertTo-NovaModuleSelfUpdatePlan -InstalledModule $InstalledModule -PrereleaseNotificationsEnabled:$PrereleaseNotificationsEnabled -TargetVersion $LookupResult.Prerelease.Version -PrereleaseTarget + return ConvertTo-NovaModuleSelfUpdatePlan -InstalledModule $InstalledModule -PlanContext $planContext -TargetVersion $LookupResult.Prerelease.Version -PrereleaseTarget } if (Test-NovaStableUpdateAvailable -StableVersion $stableVersion -InstalledVersion $InstalledModule.SemanticVersion) { - return ConvertTo-NovaModuleSelfUpdatePlan -InstalledModule $InstalledModule -PrereleaseNotificationsEnabled:$PrereleaseNotificationsEnabled -TargetVersion $LookupResult.Stable.Version + return ConvertTo-NovaModuleSelfUpdatePlan -InstalledModule $InstalledModule -PlanContext $planContext -TargetVersion $LookupResult.Stable.Version } - return ConvertTo-NovaModuleSelfUpdatePlan -InstalledModule $InstalledModule -PrereleaseNotificationsEnabled:$PrereleaseNotificationsEnabled + return ConvertTo-NovaModuleSelfUpdatePlan -InstalledModule $InstalledModule -PlanContext $planContext } diff --git a/src/resources/update/ModuleUpdateLookup.ps1.txt b/src/resources/update/ModuleUpdateLookup.ps1.txt index 58d800f2..d2b6f55e 100644 --- a/src/resources/update/ModuleUpdateLookup.ps1.txt +++ b/src/resources/update/ModuleUpdateLookup.ps1.txt @@ -4,6 +4,7 @@ $WarningPreference = 'SilentlyContinue' $InformationPreference = 'SilentlyContinue' $VerbosePreference = 'SilentlyContinue' $ErrorActionPreference = 'Stop' +$repositoryName = 'PSGallery' function Get-PrereleaseLabel { param([object]$Item) @@ -47,19 +48,21 @@ function ConvertTo-VersionInfo { return [pscustomobject]@{ ModuleName = $ResolvedModuleName Channel = $Channel + Repository = $repositoryName Version = $versionText } } -$stableItem = Find-Module $ResolvedModuleName -ErrorAction Stop -WarningAction SilentlyContinue +$stableItem = Find-Module $ResolvedModuleName -Repository $repositoryName -ErrorAction Stop -WarningAction SilentlyContinue $prereleaseItem = if ($IncludePrerelease) { - Find-Module $ResolvedModuleName -AllowPrerelease -ErrorAction Stop -WarningAction SilentlyContinue + Find-Module $ResolvedModuleName -Repository $repositoryName -AllowPrerelease -ErrorAction Stop -WarningAction SilentlyContinue } else { $null } [pscustomobject]@{ + SourceRepository = $repositoryName Stable = ConvertTo-VersionInfo -Item $stableItem -Channel 'Stable' Prerelease = ConvertTo-VersionInfo -Item $prereleaseItem -Channel 'Prerelease' } diff --git a/tests/UpdateNotification.Tests.ps1 b/tests/UpdateNotification.Tests.ps1 index bbf50359..daf0c602 100644 --- a/tests/UpdateNotification.Tests.ps1 +++ b/tests/UpdateNotification.Tests.ps1 @@ -243,6 +243,71 @@ Describe 'Update notification behavior' { } } + It 'Get-NovaModuleSelfUpdatePlan preserves the lookup candidate and repository for no-update results' { + InModuleScope $script:moduleName { + $installedModule = [pscustomobject]@{ + ModuleName = 'NovaModuleTools' + Version = '2.0.0-preview9920' + SemanticVersion = [semver]'2.0.0-preview9920' + IsPrerelease = $true + } + $lookupResult = [pscustomobject]@{ + SourceRepository = 'PSGallery' + Stable = [pscustomobject]@{ + Version = '1.9.1' + Channel = 'Stable' + Repository = 'PSGallery' + } + Prerelease = [pscustomobject]@{ + Version = '2.0.0-beta' + Channel = 'Prerelease' + Repository = 'PSGallery' + } + } + + $result = Get-NovaModuleSelfUpdatePlan -InstalledModule $installedModule -LookupResult $lookupResult -PrereleaseNotificationsEnabled:$true + + $result.UpdateAvailable | Should -BeFalse + $result.TargetVersion | Should -BeNullOrEmpty + $result.LookupCandidateVersion | Should -Be '2.0.0-beta' + $result.LookupCandidateChannel | Should -Be 'Prerelease' + $result.LookupRepository | Should -Be 'PSGallery' + } + } + + It 'Get-NovaModuleSelfUpdatePlanContext returns empty lookup fields when no lookup result is available' { + InModuleScope $script:moduleName { + $result = Get-NovaModuleSelfUpdatePlanContext -LookupResult $null -PrereleaseNotificationsEnabled:$true + + $result.LookupCandidateVersion | Should -BeNullOrEmpty + $result.LookupCandidateChannel | Should -BeNullOrEmpty + $result.LookupRepository | Should -BeNullOrEmpty + $result.PrereleaseNotificationsEnabled | Should -BeTrue + } + } + + It 'Get-NovaModuleSelfUpdatePlanContext falls back to SourceRepository when the selected lookup candidate omits Repository' { + InModuleScope $script:moduleName { + $lookupResult = [pscustomobject]@{ + SourceRepository = 'PSGallery' + Stable = [pscustomobject]@{ + Version = '1.9.1' + Channel = 'Stable' + } + Prerelease = [pscustomobject]@{ + Version = '2.0.0-beta' + Channel = 'Prerelease' + } + } + + $result = Get-NovaModuleSelfUpdatePlanContext -LookupResult $lookupResult -PrereleaseNotificationsEnabled:$true + + $result.LookupCandidateVersion | Should -Be '2.0.0-beta' + $result.LookupCandidateChannel | Should -Be 'Prerelease' + $result.LookupRepository | Should -Be 'PSGallery' + } + } + It 'Get-NovaModuleSelfUpdateWorkflowContext throws when update lookup cannot resolve a candidate' { InModuleScope $script:moduleName { Mock Read-NovaUpdateNotificationPreference {[pscustomobject]@{PrereleaseNotificationsEnabled = $true}} @@ -549,6 +614,9 @@ Describe 'Update notification behavior' { ModuleName = 'NovaModuleTools' CurrentVersion = '2.0.0-prerelease3' TargetVersion = $null + LookupCandidateVersion = '2.0.0-beta' + LookupCandidateChannel = 'Prerelease' + LookupRepository = 'PSGallery' PrereleaseNotificationsEnabled = $true UpdateAvailable = $false Updated = $false @@ -567,6 +635,17 @@ Describe 'Update notification behavior' { } } + It 'Get-NovaModuleUpdateLookupScript pins lookups to PSGallery and returns repository metadata' { + InModuleScope $script:moduleName { + $scriptText = Get-NovaModuleUpdateLookupScript + + $scriptText | Should -Match 'Find-Module \$ResolvedModuleName -Repository \$repositoryName' + $scriptText | Should -Match 'Find-Module \$ResolvedModuleName -Repository \$repositoryName -AllowPrerelease' + $scriptText | Should -Match 'SourceRepository = \$repositoryName' + $scriptText | Should -Match 'Repository = \$repositoryName' + } + } + It 'ConvertFrom-NovaUpdateCliArgument allows no arguments and rejects unsupported usage' { InModuleScope $script:moduleName { (ConvertFrom-NovaUpdateCliArgument).Count | Should -Be 0