From f71271d10140e6de68514fcc781febe4910aa35a Mon Sep 17 00:00:00 2001 From: ASIF Adeel Date: Mon, 29 Dec 2025 15:42:22 +0100 Subject: [PATCH 1/6] Add PSResourceGet support --- PSDepend/PSDependMap.psd1 | 88 +++--- PSDepend/PSDependScripts/PSResourceGet.ps1 | 339 +++++++++++++++++++++ 2 files changed, 386 insertions(+), 41 deletions(-) create mode 100644 PSDepend/PSDependScripts/PSResourceGet.ps1 diff --git a/PSDepend/PSDependMap.psd1 b/PSDepend/PSDependMap.psd1 index d150c72..568c135 100644 --- a/PSDepend/PSDependMap.psd1 +++ b/PSDepend/PSDependMap.psd1 @@ -6,87 +6,93 @@ # In some cases, it may be beneficial to include 'aliases'. Just add nodes for these. @{ - Chocolatey = @{ - Script = 'Chocolatey.ps1' + Chocolatey = @{ + Script = 'Chocolatey.ps1' Description = 'Install a Chocolatey package from a Chocolatey feed' - Supports = 'windows' + Supports = 'windows' } - Command = @{ - Script = 'Command.ps1' + Command = @{ + Script = 'Command.ps1' Description = 'Invoke a command in PowerShell' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - DotnetSdk = @{ - Script = 'DotnetSdk.ps1' + DotnetSdk = @{ + Script = 'DotnetSdk.ps1' Description = "Installs the .NET Core SDK" - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - FileDownload = @{ - Script = 'FileDownload.ps1' + FileDownload = @{ + Script = 'FileDownload.ps1' Description = 'Download a file' - Supports = 'windows' + Supports = 'windows' } - FileSystem = @{ - Script = 'FileSystem.ps1' + FileSystem = @{ + Script = 'FileSystem.ps1' Description = 'Copy a file or folder' - Supports = 'windows' + Supports = 'windows' } - Git = @{ - Script = 'Git.ps1' + Git = @{ + Script = 'Git.ps1' Description = 'Clone a git repository' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - GitHub = @{ - Script = 'GitHub.ps1' + GitHub = @{ + Script = 'GitHub.ps1' Description = 'Download and extract a GitHub repo' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Npm = @{ - Script = 'Npm.ps1' + Npm = @{ + Script = 'Npm.ps1' Description = 'Install a node package' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Noop = @{ - Script = 'Noop.ps1' + Noop = @{ + Script = 'Noop.ps1' Description = 'Display parameters that a depends script would receive. Use for testing and validation' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Nuget = @{ - Script = 'Nuget.ps1' + Nuget = @{ + Script = 'Nuget.ps1' Description = 'Install a Nuget package from a Nuget feed' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Package = @{ - Script = 'Package.ps1' + Package = @{ + Script = 'Package.ps1' Description = 'EXPERIMENTAL: Install a package via PackageManagement Install-Package' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } PSGalleryModule = @{ - Script= 'PSGalleryModule.ps1' + Script = 'PSGalleryModule.ps1' Description = 'Install a PowerShell module from the PowerShell Gallery' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - PSGalleryNuget = @{ - Script = 'PSGalleryNuget.ps1' + PSGalleryNuget = @{ + Script = 'PSGalleryNuget.ps1' Description = 'Install a PowerShell module from the PowerShell Gallery without the PowerShellGet dependency' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } - Task = @{ - Script = 'Task.ps1' + PsRessourceGet = @{ + Script = 'PSResourceGet.ps1' + Description = 'Installs a PowerShell resource (module) from a PowerShell repository using PSResourceGet' + Supports = 'windows', 'core', 'macos', 'linux' + } + + Task = @{ + Script = 'Task.ps1' Description = 'Support dependencies by handling simple tasks' - Supports = 'windows', 'core', 'macos', 'linux' + Supports = 'windows', 'core', 'macos', 'linux' } } diff --git a/PSDepend/PSDependScripts/PSResourceGet.ps1 b/PSDepend/PSDependScripts/PSResourceGet.ps1 new file mode 100644 index 0000000..055ebae --- /dev/null +++ b/PSDepend/PSDependScripts/PSResourceGet.ps1 @@ -0,0 +1,339 @@ +<# + .SYNOPSIS + Installs a PowerShell resource (module) from a PowerShell repository using PSResourceGet. + + .DESCRIPTION + Installs a PowerShell module from a PowerShell repository (such as the PowerShell Gallery) + using the PSResourceGet module, which replaces the deprecated PowerShellGet module. + + Relevant Dependency metadata: + Name: The name of the module to install + Version: Used to identify existing installs meeting this criteria, and as RequiredVersion + for installation. Defaults to 'latest' + Target: Used as 'Scope' for Install-PSResource. + If this is a filesystem path, Save-PSResource is used instead. + Defaults to 'CurrentUser' + AddToPath: If Target is used as a path, prepend that path to $ENV:PSModulePath + Credential: The username and password used to authenticate against a private repository + + This provider relies on PSResourceGet cmdlets such as: + - Find-PSResource + - Install-PSResource + - Save-PSResource + + If PSResourceGet is not available, it must be installed prior to using this provider. + + .PARAMETER Repository + PSResource repository to download from. + Defaults to PSGallery. + + .PARAMETER NoClobber + Allow installation of modules that overwrite existing commands. + Defaults to $false. + + .PARAMETER AcceptLicense + Accepts the license agreement during installation. + + .PARAMETER Prerelease + If specified, allows installation of prerelease versions. + + If specified along with version 'latest', a prerelease will be selected + if it is the most recent available version. + + Sorting assumes prereleases are named appropriately + (e.g. alpha < beta < rc). + + .PARAMETER Import + If specified, imports the module into the global scope. + + Deprecated. Moving to PSDependAction. + + .PARAMETER PSDependAction + Test, Install, or Import the module. + Defaults to Install. + + Test: Returns true or false depending on whether the dependency is present + Install: Installs the dependency + Import: Imports the dependency + + .EXAMPLE + @{ + BuildHelpers = 'latest' + PSDeploy = '' + InvokeBuild = '3.2.1' + } + + # From the PSGallery repository... + # Install the latest BuildHelpers and PSDeploy + # Install version 3.2.1 of InvokeBuild + + .EXAMPLE + @{ + BuildHelpers = @{ + Target = 'C:\Build' + } + } + + # Install the latest BuildHelpers module from PSGallery to C:\Build + # (i.e. C:\Build\BuildHelpers will be the module folder) + + .EXAMPLE + @{ + BuildHelpers = @{ + Parameters = @{ + Repository = 'PSPrivateGallery' + SkipPublisherCheck = $true + } + } + } + + # Install the latest BuildHelpers module from a custom registered repository + # and bypass the catalog signing check. + + # Examples of private repositories include: + # - PSPrivateGallery + # - Artifactory + # - ProGet + # - Gitlab + + .EXAMPLE + @{ + 'vmware.powercli' = @{ + Parameters = @{ + Prerelease = $true + } + } + } + + # Install the latest version of PowerCLI, allowing prerelease versions. +#> + +[cmdletbinding()] +param( + [PSTypeName('PSDepend.Dependency')] + [psobject[]]$Dependency, + + [AllowNull()] + [string]$Repository = 'PSGallery', # From Parameters... + + [bool]$NoClobber = $false, + + [bool]$AcceptLicense, + + [bool]$Prerelease, + + [switch]$Import, + + [ValidateSet('Test', 'Install', 'Import')] + [string[]]$PSDependAction = @('Install') +) + +# Extract data from Dependency +$DependencyName = $Dependency.DependencyName +$Name = $Dependency.Name +if(-not $Name) +{ + $Name = $DependencyName +} + +$Version = $Dependency.Version +if(-not $Version) +{ + $Version = 'latest' +} + +# We use target as a proxy for Scope +if(-not $Dependency.Target) +{ + $Scope = 'CurrentUser' +} +else +{ + $Scope = $Dependency.Target +} + +$Credential = $Dependency.Credential + +if('AllUsers', 'CurrentUser' -notcontains $Scope) +{ + $command = 'save' +} +else +{ + $command = 'install' +} + +if(-not (Get-PackageProvider -Name Nuget)) +{ + # Grab nuget bits. + $null = Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null +} + +Write-Verbose -Message "Getting dependency [$name] from PowerShell repository [$Repository]" + +# Validate that $target has been setup as a valid PowerShell repository, +# but allow to rely on all PS repos registered. +if($Repository) +{ + $validRepo = Get-PSResourceRepository -Name $Repository -Verbose:$false -ErrorAction SilentlyContinue + if (-not $validRepo) + { + Write-Error "[$Repository] has not been setup as a valid PowerShell repository." + return + } +} + +$params = @{ + Name = $Name + NoClobber = $NoClobber + Verbose = $VerbosePreference +} + +if($PSBoundParameters.ContainsKey('Prerelease')) +{ + $params.Add('Prerelease', $Prerelease) +} + +if($PSBoundParameters.ContainsKey('AcceptLicense')) +{ + $params.Add('AcceptLicense', $AcceptLicense) +} + +if($Repository) +{ + $params.Add('Repository',$Repository) +} + +if($Version -and $Version -ne 'latest') +{ + $Params.add('RequiredVersion', $Version) +} + +if($Credential) +{ + $Params.add('Credential', $Credential) +} + +# This code works for both install and save scenarios. +if($command -eq 'Save') +{ + $ModuleName = Join-Path $Scope $Name + $Params.Remove('NoClobber') +} +elseif ($Command -eq 'Install') +{ + $ModuleName = $Name +} + +$availableParameters = (Get-Command "Install-Module").Parameters +$tempParams = $Params.Clone() +foreach($thisParameter in $Params.Keys) +{ + if(-Not ($availableParameters.ContainsKey($thisParameter))) + { + Write-Verbose -Message "Removing parameter [$thisParameter] from [Install-Module] as it is not available" + $tempParams.Remove($thisParameter) + } +} +$Params = $tempParams.Clone() + +Add-ToPsModulePathIfRequired -Dependency $Dependency -Action $PSDependAction + +$Existing = $null +$Existing = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue + +if($Existing) +{ + Write-Verbose "Found existing module [$Name]" + # Thanks to Brandon Padgett! + $ExistingVersion = $Existing | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum + $FindModuleParams = @{Name = $Name } + if($Repository) + { + $FindModuleParams.Add('Repository', $Repository) + } + if($Credential) + { + $FindModuleParams.Add('Credential', $Credential) + } + if($Prerelease) + { + $FindModuleParams.Add('Prerelease', $Prerelease) + } + + # Version string, and equal to current + if($Version -and $Version -ne 'latest' -and $Version -eq $ExistingVersion) + { + Write-Verbose "You have the requested version [$Version] of [$Name]" + # Conditional import + Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion + + if($PSDependAction -contains 'Test') + { + return $true + } + return $null + } + + Write-verbose "$($Repository)" + $GalleryVersion = Find-PSResource @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum + [System.Version]$parsedVersion = $null + [System.Management.Automation.SemanticVersion]$parsedSemanticVersion = $null + [System.Management.Automation.SemanticVersion]$parsedTempSemanticVersion = $null + $isGalleryVersionLessEquals = if ( + [System.Management.Automation.SemanticVersion]::TryParse($ExistingVersion, [ref]$parsedSemanticVersion) -and + [System.Management.Automation.SemanticVersion]::TryParse($GalleryVersion, [ref]$parsedTempSemanticVersion) + ) + { + $GalleryVersion -le $parsedSemanticVersion + } + elseif ([System.Version]::TryParse($ExistingVersion, [ref]$parsedVersion)) + { + $GalleryVersion -le $parsedVersion + } + + # latest, and we have latest + if( $Version -and ($Version -eq 'latest' -or $Version -eq '') -and $isGalleryVersionLessEquals) + { + Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and PSGallery version [$GalleryVersion]" + # Conditional import + Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion + + if($PSDependAction -contains 'Test') + { + return $True + } + return $null + } + Write-Verbose "Continuing to install [$Name]: Requested version [$version], existing version [$ExistingVersion]" +} + +#No dependency found, return false if we're testing alone... +if( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) +{ + return $False +} + +if($PSDependAction -contains 'Install') +{ + if('AllUsers', 'CurrentUser' -contains $Scope) + { + Write-Verbose "Installing [$Name] with scope [$Scope]" + Write-verbose "$params" + Install-PSResource @params + } + else + { + Write-Verbose "Saving [$Name] with path [$Scope]" + Write-Verbose "Creating directory path to [$Scope]" + if(-not (Test-Path $Scope -ErrorAction SilentlyContinue)) + { + $Null = New-Item -ItemType Directory -Path $Scope -Force -ErrorAction SilentlyContinue + } + Save-PSResource @params -Path $Scope + } +} + +# Conditional import +$importVs = $params['RequiredVersion'] +Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $importVs \ No newline at end of file From 29fb380c9549bc09a45199428e6623536e798484 Mon Sep 17 00:00:00 2001 From: ASIF Adeel Date: Mon, 29 Dec 2025 15:44:31 +0100 Subject: [PATCH 2/6] Fix typo --- PSDepend/PSDependMap.psd1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PSDepend/PSDependMap.psd1 b/PSDepend/PSDependMap.psd1 index 568c135..1a41a22 100644 --- a/PSDepend/PSDependMap.psd1 +++ b/PSDepend/PSDependMap.psd1 @@ -84,7 +84,7 @@ Supports = 'windows', 'core', 'macos', 'linux' } - PsRessourceGet = @{ + PsResourceGet = @{ Script = 'PSResourceGet.ps1' Description = 'Installs a PowerShell resource (module) from a PowerShell repository using PSResourceGet' Supports = 'windows', 'core', 'macos', 'linux' From 2daf69f205b1baeb9f1383950a1937fa7e35bfd9 Mon Sep 17 00:00:00 2001 From: ASIF Adeel Date: Mon, 29 Dec 2025 15:47:01 +0100 Subject: [PATCH 3/6] Fix typo --- PSDepend/PSDependMap.psd1 | 2 +- PSDepend/PSDependScripts/PSResourceGet.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PSDepend/PSDependMap.psd1 b/PSDepend/PSDependMap.psd1 index 1a41a22..371b819 100644 --- a/PSDepend/PSDependMap.psd1 +++ b/PSDepend/PSDependMap.psd1 @@ -86,7 +86,7 @@ PsResourceGet = @{ Script = 'PSResourceGet.ps1' - Description = 'Installs a PowerShell resource (module) from a PowerShell repository using PSResourceGet' + Description = 'Installs a PowerShell resource from a PowerShell repository using PSResourceGet' Supports = 'windows', 'core', 'macos', 'linux' } diff --git a/PSDepend/PSDependScripts/PSResourceGet.ps1 b/PSDepend/PSDependScripts/PSResourceGet.ps1 index 055ebae..c07fa04 100644 --- a/PSDepend/PSDependScripts/PSResourceGet.ps1 +++ b/PSDepend/PSDependScripts/PSResourceGet.ps1 @@ -1,6 +1,6 @@ <# .SYNOPSIS - Installs a PowerShell resource (module) from a PowerShell repository using PSResourceGet. + Installs a PowerShell resource from a PowerShell repository using PSResourceGet. .DESCRIPTION Installs a PowerShell module from a PowerShell repository (such as the PowerShell Gallery) From 8c02c84d7c7a7cc6b95bba52320fb21684b45229 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 18 May 2026 22:45:16 -0700 Subject: [PATCH 4/6] fix(PSResourceGet): address review findings from team code review - Rename map key PsResourceGet -> PSResourceGet to match module name - Update PSGalleryModule description to note legacy status - Replace Get-PackageProvider/NuGet bootstrap (PSResourceGet has own NuGet client) with an explicit Install-PSResource availability guard - Fix Install-Module parameter filter: was validating $params against Install-Module (PSGet v2); now routes to Install-PSResource or Save-PSResource depending on $command, so NoClobber and other PSResourceGet-only params survive - Fix RequiredVersion -> Version (Install-PSResource uses -Version with NuGet range syntax, not -RequiredVersion) - Change NoClobber, AcceptLicense, Prerelease from [bool] to [switch] to match Install-PSResource signature; only splat when explicitly provided - Remove Verbose = $VerbosePreference from $params splat (common param, not a named one) - Add TrustRepository = $true to params so unattended CI installs do not hang on a trust prompt - Pass -Scope $Scope explicitly to Install-PSResource (was missing) - Fix $command casing: set and compared consistently as lowercase - Fix $params/$Params variable casing drift throughout - Fix [cmdletbinding()] -> [CmdletBinding()] - Rename $isGalleryVersionLessEquals -> $existingIsUpToDate for clarity - Remove Write-verbose "$($Repository)" and Write-verbose "$params" debug remnants; replace with null/safe messages - Fix $True/$False/$Null -> $true/$false/$null - Fix double space in Join-Path assignment - Add newline at end of file - Fix .PARAMETER NoClobber description (was inverted: said Allow, means Prevents) - Add .PARAMETER Dependency block - Fix Example 3: remove SkipPublisherCheck (PSGet v2 only); replace with valid private-repo example - Add DependencyType = 'PSResourceGet' to all examples - Fix Credential description to name [PSCredential] type - Use ASCII hyphen in map descriptions (em dash caused Pester module-scope failures due to encoding) Co-Authored-By: Claude Sonnet 4.6 --- PSDepend/PSDependMap.psd1 | 6 +- PSDepend/PSDependScripts/PSResourceGet.ps1 | 226 +++++++++++---------- 2 files changed, 121 insertions(+), 111 deletions(-) diff --git a/PSDepend/PSDependMap.psd1 b/PSDepend/PSDependMap.psd1 index 371b819..f6674b0 100644 --- a/PSDepend/PSDependMap.psd1 +++ b/PSDepend/PSDependMap.psd1 @@ -74,7 +74,7 @@ PSGalleryModule = @{ Script = 'PSGalleryModule.ps1' - Description = 'Install a PowerShell module from the PowerShell Gallery' + Description = 'Install a PowerShell module from the PowerShell Gallery (legacy, PowerShellGet v2 - prefer PSResourceGet for new projects)' Supports = 'windows', 'core', 'macos', 'linux' } @@ -84,9 +84,9 @@ Supports = 'windows', 'core', 'macos', 'linux' } - PsResourceGet = @{ + PSResourceGet = @{ Script = 'PSResourceGet.ps1' - Description = 'Installs a PowerShell resource from a PowerShell repository using PSResourceGet' + Description = 'Install a PowerShell module from a PowerShell repository using PSResourceGet (preferred over PSGalleryModule)' Supports = 'windows', 'core', 'macos', 'linux' } diff --git a/PSDepend/PSDependScripts/PSResourceGet.ps1 b/PSDepend/PSDependScripts/PSResourceGet.ps1 index c07fa04..4c09b86 100644 --- a/PSDepend/PSDependScripts/PSResourceGet.ps1 +++ b/PSDepend/PSDependScripts/PSResourceGet.ps1 @@ -4,35 +4,40 @@ .DESCRIPTION Installs a PowerShell module from a PowerShell repository (such as the PowerShell Gallery) - using the PSResourceGet module, which replaces the deprecated PowerShellGet module. + using PSResourceGet (Microsoft.PowerShell.PSResourceGet), the successor to the deprecated + PowerShellGet v2 module. PSResourceGet must be installed before using this provider. + + Prefer this provider over PSGalleryModule for new projects. PSGalleryModule targets + PowerShellGet v2 (Install-Module); this provider targets PSResourceGet v3 (Install-PSResource). Relevant Dependency metadata: - Name: The name of the module to install - Version: Used to identify existing installs meeting this criteria, and as RequiredVersion - for installation. Defaults to 'latest' - Target: Used as 'Scope' for Install-PSResource. - If this is a filesystem path, Save-PSResource is used instead. - Defaults to 'CurrentUser' - AddToPath: If Target is used as a path, prepend that path to $ENV:PSModulePath - Credential: The username and password used to authenticate against a private repository - - This provider relies on PSResourceGet cmdlets such as: + Name: The name of the module to install + Version: Used to identify existing installs and as -Version for installation. + Supports NuGet range syntax (e.g. '[1.0.0, ]'). Defaults to 'latest'. + Target: Used as -Scope for Install-PSResource (CurrentUser or AllUsers). + If this is a filesystem path, Save-PSResource is used instead. + Defaults to 'CurrentUser'. + AddToPath: If Target is a filesystem path, prepend that path to $env:PSModulePath. + Credential: A [PSCredential] for authenticating against a private repository. + Use Get-Credential or [PSCredential]::new() to construct one. + + This provider calls the following PSResourceGet cmdlets: - Find-PSResource - Install-PSResource - Save-PSResource - If PSResourceGet is not available, it must be installed prior to using this provider. + .PARAMETER Dependency + The PSDepend.Dependency object passed by Invoke-PSDepend. Not supplied directly by the caller. .PARAMETER Repository PSResource repository to download from. Defaults to PSGallery. .PARAMETER NoClobber - Allow installation of modules that overwrite existing commands. - Defaults to $false. + Prevents installation if the module would overwrite commands already present on the system. .PARAMETER AcceptLicense - Accepts the license agreement during installation. + Suppresses the license acceptance prompt during installation. .PARAMETER Prerelease If specified, allows installation of prerelease versions. @@ -44,62 +49,68 @@ (e.g. alpha < beta < rc). .PARAMETER Import - If specified, imports the module into the global scope. + If specified, imports the module into the global scope after installation. - Deprecated. Moving to PSDependAction. + Deprecated. Use PSDependAction = 'Import' instead. This parameter may be + removed in a future release. .PARAMETER PSDependAction Test, Install, or Import the module. Defaults to Install. - Test: Returns true or false depending on whether the dependency is present + Test: Returns $true or $false depending on whether the dependency is present Install: Installs the dependency - Import: Imports the dependency + Import: Imports the dependency .EXAMPLE @{ - BuildHelpers = 'latest' - PSDeploy = '' - InvokeBuild = '3.2.1' + BuildHelpers = @{ + DependencyType = 'PSResourceGet' + Version = 'latest' + } + InvokeBuild = @{ + DependencyType = 'PSResourceGet' + Version = '3.2.1' + } } - # From the PSGallery repository... - # Install the latest BuildHelpers and PSDeploy - # Install version 3.2.1 of InvokeBuild + # Install the latest BuildHelpers and version 3.2.1 of InvokeBuild from PSGallery. + # Omitting Version, or setting it to '', also resolves to latest. .EXAMPLE @{ BuildHelpers = @{ - Target = 'C:\Build' + DependencyType = 'PSResourceGet' + Target = 'C:\Build' } } - # Install the latest BuildHelpers module from PSGallery to C:\Build + # Save the latest BuildHelpers module from PSGallery to C:\Build # (i.e. C:\Build\BuildHelpers will be the module folder) .EXAMPLE @{ BuildHelpers = @{ - Parameters = @{ + DependencyType = 'PSResourceGet' + Parameters = @{ Repository = 'PSPrivateGallery' - SkipPublisherCheck = $true } } } - # Install the latest BuildHelpers module from a custom registered repository - # and bypass the catalog signing check. - + # Install the latest BuildHelpers from a registered private repository. + # Register the repository first with Register-PSResourceRepository. + # # Examples of private repositories include: - # - PSPrivateGallery # - Artifactory # - ProGet - # - Gitlab + # - GitLab Package Registry .EXAMPLE @{ 'vmware.powercli' = @{ - Parameters = @{ + DependencyType = 'PSResourceGet' + Parameters = @{ Prerelease = $true } } @@ -108,19 +119,19 @@ # Install the latest version of PowerCLI, allowing prerelease versions. #> -[cmdletbinding()] +[CmdletBinding()] param( [PSTypeName('PSDepend.Dependency')] [psobject[]]$Dependency, [AllowNull()] - [string]$Repository = 'PSGallery', # From Parameters... + [string]$Repository = 'PSGallery', - [bool]$NoClobber = $false, + [switch]$NoClobber, - [bool]$AcceptLicense, + [switch]$AcceptLicense, - [bool]$Prerelease, + [switch]$Prerelease, [switch]$Import, @@ -128,22 +139,28 @@ param( [string[]]$PSDependAction = @('Install') ) +if (-not (Get-Command -Name Install-PSResource -ErrorAction SilentlyContinue)) +{ + Write-Error "PSResourceGet (Microsoft.PowerShell.PSResourceGet) is required but not available. Install it before using the PSResourceGet dependency type." + return +} + # Extract data from Dependency $DependencyName = $Dependency.DependencyName $Name = $Dependency.Name -if(-not $Name) +if (-not $Name) { $Name = $DependencyName } $Version = $Dependency.Version -if(-not $Version) +if (-not $Version) { $Version = 'latest' } -# We use target as a proxy for Scope -if(-not $Dependency.Target) +# Target doubles as Scope: AllUsers/CurrentUser = install scope; any other value = filesystem path +if (-not $Dependency.Target) { $Scope = 'CurrentUser' } @@ -154,7 +171,7 @@ else $Credential = $Dependency.Credential -if('AllUsers', 'CurrentUser' -notcontains $Scope) +if ('AllUsers', 'CurrentUser' -notcontains $Scope) { $command = 'save' } @@ -163,124 +180,119 @@ else $command = 'install' } -if(-not (Get-PackageProvider -Name Nuget)) -{ - # Grab nuget bits. - $null = Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null -} +Write-Verbose -Message "Getting dependency [$Name] from PowerShell repository [$Repository]" -Write-Verbose -Message "Getting dependency [$name] from PowerShell repository [$Repository]" - -# Validate that $target has been setup as a valid PowerShell repository, -# but allow to rely on all PS repos registered. -if($Repository) +if ($Repository) { $validRepo = Get-PSResourceRepository -Name $Repository -Verbose:$false -ErrorAction SilentlyContinue if (-not $validRepo) { - Write-Error "[$Repository] has not been setup as a valid PowerShell repository." + Write-Error "[$Repository] has not been set up as a valid PowerShell repository." return } } +# TrustRepository defaults to $true so unattended / CI installs do not hang on a trust prompt $params = @{ - Name = $Name - NoClobber = $NoClobber - Verbose = $VerbosePreference + Name = $Name + TrustRepository = $true } -if($PSBoundParameters.ContainsKey('Prerelease')) +if ($PSBoundParameters.ContainsKey('NoClobber')) +{ + $params.Add('NoClobber', $NoClobber) +} + +if ($PSBoundParameters.ContainsKey('Prerelease')) { $params.Add('Prerelease', $Prerelease) } -if($PSBoundParameters.ContainsKey('AcceptLicense')) +if ($PSBoundParameters.ContainsKey('AcceptLicense')) { $params.Add('AcceptLicense', $AcceptLicense) } -if($Repository) +if ($Repository) { - $params.Add('Repository',$Repository) + $params.Add('Repository', $Repository) } -if($Version -and $Version -ne 'latest') +if ($Version -and $Version -ne 'latest') { - $Params.add('RequiredVersion', $Version) + $params.Add('Version', $Version) } -if($Credential) +if ($Credential) { - $Params.add('Credential', $Credential) + $params.Add('Credential', $Credential) } -# This code works for both install and save scenarios. -if($command -eq 'Save') +if ($command -eq 'save') { - $ModuleName = Join-Path $Scope $Name - $Params.Remove('NoClobber') + $ModuleName = Join-Path $Scope $Name } -elseif ($Command -eq 'Install') +elseif ($command -eq 'install') { $ModuleName = $Name } -$availableParameters = (Get-Command "Install-Module").Parameters -$tempParams = $Params.Clone() -foreach($thisParameter in $Params.Keys) +# Filter params to only those accepted by the target command +$targetCmd = if ($command -eq 'save') { 'Save-PSResource' } else { 'Install-PSResource' } +$availableParameters = (Get-Command $targetCmd).Parameters +$tempParams = $params.Clone() +foreach ($thisParameter in $params.Keys) { - if(-Not ($availableParameters.ContainsKey($thisParameter))) + if (-not $availableParameters.ContainsKey($thisParameter)) { - Write-Verbose -Message "Removing parameter [$thisParameter] from [Install-Module] as it is not available" + Write-Verbose -Message "Removing parameter [$thisParameter] from [$targetCmd] as it is not available" $tempParams.Remove($thisParameter) } } -$Params = $tempParams.Clone() +$params = $tempParams.Clone() Add-ToPsModulePathIfRequired -Dependency $Dependency -Action $PSDependAction -$Existing = $null $Existing = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue -if($Existing) +if ($Existing) { Write-Verbose "Found existing module [$Name]" # Thanks to Brandon Padgett! $ExistingVersion = $Existing | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum - $FindModuleParams = @{Name = $Name } - if($Repository) + $FindModuleParams = @{ Name = $Name } + if ($Repository) { $FindModuleParams.Add('Repository', $Repository) } - if($Credential) + if ($Credential) { $FindModuleParams.Add('Credential', $Credential) } - if($Prerelease) + if ($Prerelease) { - $FindModuleParams.Add('Prerelease', $Prerelease) + $FindModuleParams.Add('Prerelease', $true) } # Version string, and equal to current - if($Version -and $Version -ne 'latest' -and $Version -eq $ExistingVersion) + if ($Version -and $Version -ne 'latest' -and $Version -eq $ExistingVersion) { Write-Verbose "You have the requested version [$Version] of [$Name]" - # Conditional import Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion - if($PSDependAction -contains 'Test') + if ($PSDependAction -contains 'Test') { return $true } return $null } - Write-verbose "$($Repository)" $GalleryVersion = Find-PSResource @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum + # Compare using SemanticVersion first (PSResourceGet uses SemVer); fall back to System.Version [System.Version]$parsedVersion = $null [System.Management.Automation.SemanticVersion]$parsedSemanticVersion = $null [System.Management.Automation.SemanticVersion]$parsedTempSemanticVersion = $null - $isGalleryVersionLessEquals = if ( + $existingIsUpToDate = if ( [System.Management.Automation.SemanticVersion]::TryParse($ExistingVersion, [ref]$parsedSemanticVersion) -and [System.Management.Automation.SemanticVersion]::TryParse($GalleryVersion, [ref]$parsedTempSemanticVersion) ) @@ -293,47 +305,45 @@ if($Existing) } # latest, and we have latest - if( $Version -and ($Version -eq 'latest' -or $Version -eq '') -and $isGalleryVersionLessEquals) + if ($Version -and ($Version -eq 'latest' -or $Version -eq '') -and $existingIsUpToDate) { - Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and PSGallery version [$GalleryVersion]" - # Conditional import + Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and repository version [$GalleryVersion]" Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion - if($PSDependAction -contains 'Test') + if ($PSDependAction -contains 'Test') { - return $True + return $true } return $null } - Write-Verbose "Continuing to install [$Name]: Requested version [$version], existing version [$ExistingVersion]" + Write-Verbose "Continuing to install [$Name]: Requested version [$Version], existing version [$ExistingVersion]" } -#No dependency found, return false if we're testing alone... -if( $PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) +# No dependency found, return false if we're testing alone... +if ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) { - return $False + return $false } -if($PSDependAction -contains 'Install') +if ($PSDependAction -contains 'Install') { - if('AllUsers', 'CurrentUser' -contains $Scope) + if ('AllUsers', 'CurrentUser' -contains $Scope) { Write-Verbose "Installing [$Name] with scope [$Scope]" - Write-verbose "$params" - Install-PSResource @params + Install-PSResource @params -Scope $Scope } else { - Write-Verbose "Saving [$Name] with path [$Scope]" + Write-Verbose "Saving [$Name] to path [$Scope]" Write-Verbose "Creating directory path to [$Scope]" - if(-not (Test-Path $Scope -ErrorAction SilentlyContinue)) + if (-not (Test-Path $Scope -ErrorAction SilentlyContinue)) { - $Null = New-Item -ItemType Directory -Path $Scope -Force -ErrorAction SilentlyContinue + $null = New-Item -ItemType Directory -Path $Scope -Force -ErrorAction SilentlyContinue } Save-PSResource @params -Path $Scope } } # Conditional import -$importVs = $params['RequiredVersion'] -Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $importVs \ No newline at end of file +$importVs = $params['Version'] +Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $importVs From 98297bcfd2c412ef1dbed9e359eb60ed1fc717f9 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Mon, 18 May 2026 23:10:46 -0700 Subject: [PATCH 5/6] test(PSResourceGet): add Pester 5 type tests for PSResourceGet dependency 13 tests across 10 contexts covering: version handling (omit vs explicit), Name/DependencyName fallback, Test action ($false/$true), Test+Install short-circuit, latest version comparison, Save-PSResource path target, credential pass-through, repository validation, availability guard, and TrustRepository always-on. Stubs declare realistic parameter signatures so PSResourceGet.ps1's param- stripping loop retains all params, keeping ParameterFilter assertions valid on machines where Microsoft.PowerShell.PSResourceGet is not installed. Co-Authored-By: Claude Sonnet 4.6 --- Tests/PSResourceGet.Type.Tests.ps1 | 230 +++++++++++++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 Tests/PSResourceGet.Type.Tests.ps1 diff --git a/Tests/PSResourceGet.Type.Tests.ps1 b/Tests/PSResourceGet.Type.Tests.ps1 new file mode 100644 index 0000000..2204ae2 --- /dev/null +++ b/Tests/PSResourceGet.Type.Tests.ps1 @@ -0,0 +1,230 @@ +#requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } + +BeforeAll { + if (-not $env:BHProjectPath) { + & "$PSScriptRoot\..\build.ps1" -Task 'Build' + } + Remove-Module $env:BHProjectName -ErrorAction SilentlyContinue + Import-Module (Join-Path $env:BHProjectPath $env:BHProjectName) -Force + + Import-Module (Join-Path $PSScriptRoot 'Shared/TestHelpers.psm1') -Force + + $script:ScriptPath = Join-Path $env:BHProjectPath 'PSDepend/PSDependScripts/PSResourceGet.ps1' + $script:TestCred = New-TestCredential + $script:OrigPSModulePath = $env:PSModulePath +} + +AfterAll { + if ($script:OrigPSModulePath) { + $env:PSModulePath = $script:OrigPSModulePath + } +} + +Describe 'PSResourceGet script' { + + BeforeAll { + InModuleScope PSDepend { + # Stubs for PSResourceGet cmdlets — needed so Pester can mock them on machines where + # Microsoft.PowerShell.PSResourceGet is not installed. Parameter declarations must + # match what PSResourceGet.ps1 passes so the param-stripping loop keeps them intact. + function Get-PSResourceRepository { + [CmdletBinding()] param([string]$Name) + } + function Find-PSResource { + [CmdletBinding()] param( + [string]$Name, [string]$Repository, + [PSCredential]$Credential, [switch]$Prerelease + ) + } + function Install-PSResource { + [CmdletBinding()] param( + [string]$Name, [string]$Version, [string]$Repository, + [switch]$TrustRepository, [switch]$NoClobber, + [switch]$AcceptLicense, [switch]$Prerelease, + [PSCredential]$Credential, [string]$Scope + ) + } + function Save-PSResource { + [CmdletBinding()] param( + [string]$Name, [string]$Version, [string]$Repository, + [switch]$TrustRepository, [switch]$NoClobber, + [switch]$AcceptLicense, [switch]$Prerelease, + [PSCredential]$Credential, [string]$Path + ) + } + + Mock Get-PSResourceRepository { [PSCustomObject]@{ Name = 'PSGallery'; Trusted = $true } } + Mock Get-Module { } -ParameterFilter { $ListAvailable } + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.0.0' } } + Mock Install-PSResource { } + Mock Save-PSResource { } + Mock Import-PSDependModule { } + Mock Add-ToPsModulePathIfRequired { } + } + } + + Context 'Contract: default Version handling' { + It 'Omits -Version when Version is not supplied (installs latest)' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { -not $PSBoundParameters.ContainsKey('Version') } + } + + It 'Passes -Version when an explicit version is supplied' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version '1.2.3' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Version -eq '1.2.3' } + } + } + + Context 'Contract: Name falls back to DependencyName' { + It 'Uses DependencyName as the module name when Name is not set' { + $dep = New-PSDependFixture -DependencyName 'FallbackModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Name -eq 'FallbackModule' } + } + + It 'Prefers Name over DependencyName when both are set' { + $dep = New-PSDependFixture -DependencyName 'IgnoredKey' -Name 'RealModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Name -eq 'RealModule' } + } + } + + Context 'PSDependAction = Test only' { + It 'Returns $false when module is not installed' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $false + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + + It 'Returns $true when installed version matches requested version' { + InModuleScope PSDepend { + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'1.2.3' } } ` + -ParameterFilter { $ListAvailable } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version '1.2.3' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $true + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'PSDependAction = Test,Install short-circuits when satisfied' { + BeforeAll { + InModuleScope PSDepend { + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.0.0' } } ` + -ParameterFilter { $ListAvailable } + } + } + + It 'Skips Install-PSResource but still calls Import-PSDependModule' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version 'latest' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test, Install + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + Should -Invoke -CommandName Import-PSDependModule -ModuleName PSDepend -Times 1 + } + } + + Context 'Latest version comparison' { + It 'Installs when installed version is behind the repository version' { + InModuleScope PSDepend { + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.8.0' } } ` + -ParameterFilter { $ListAvailable } + Mock Find-PSResource { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.10.0' } } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version 'latest' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 + } + } + + Context 'Target as path uses Save-PSResource instead of Install-PSResource' { + It 'Calls Save-PSResource with -Path and skips Install-PSResource' { + $savePath = (New-Item 'TestDrive:/psresourceget-save' -ItemType Directory -Force).FullName + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Target $savePath + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Save-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Path -eq $savePath } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'Credential pass-through' { + It 'Forwards Credential to Install-PSResource' { + $dep = New-PSDependFixture -DependencyName 'PrivateModule' -DependencyType 'PSResourceGet' ` + -Credential $script:TestCred + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Credential -and $Credential.UserName -eq 'testUser' } + } + } + + Context 'Repository validation' { + BeforeAll { + InModuleScope PSDepend { + Mock Get-PSResourceRepository { } -ParameterFilter { $Name -eq 'BogusRepo' } + } + } + + It 'Writes an error and skips install when the repository is not registered' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' ` + -Parameters @{ Repository = 'BogusRepo' } + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -Repository 'BogusRepo' -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'PSResourceGet availability guard' { + It 'Returns early without installing when Install-PSResource is not available' { + InModuleScope PSDepend { + # Intercept the guard check: Get-Command is safe to mock inside an It block + # (Pester's own mock setup finished in BeforeAll; this only affects the script's call) + Mock Get-Command { } -ParameterFilter { $Name -eq 'Install-PSResource' } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + + Context 'TrustRepository' { + It 'Always passes TrustRepository to Install-PSResource for unattended use' { + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $TrustRepository -eq $true } + } + } +} From ce583527a6e8e3852672a476b57008e734d8e917 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Tue, 19 May 2026 09:59:44 -0700 Subject: [PATCH 6/6] fix(PSResourceGet): address Copilot review findings - Fix multi-version short-circuit: compare requested version against all installed versions, not just the maximum, so a lower installed version is correctly detected as satisfied - Fix SemVer comparison: compare parsed types on both sides instead of mixing parsed SemanticVersion with unparsed string - Fix System.Version fallback: parse gallery version before comparing, add else { $false } so existingIsUpToDate is never $null on failure - Fix NuGet range import: resolve range strings to the concrete installed version before passing to Import-PSDependModule - Add tests: NuGet range version syntax and multi-version installed scenario Co-Authored-By: Claude Sonnet 4.6 --- PSDepend/PSDependScripts/PSResourceGet.ps1 | 36 +++++++++++++++------ Tests/PSResourceGet.Type.Tests.ps1 | 37 ++++++++++++++++++++++ 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/PSDepend/PSDependScripts/PSResourceGet.ps1 b/PSDepend/PSDependScripts/PSResourceGet.ps1 index 4c09b86..484da3a 100644 --- a/PSDepend/PSDependScripts/PSResourceGet.ps1 +++ b/PSDepend/PSDependScripts/PSResourceGet.ps1 @@ -274,11 +274,15 @@ if ($Existing) $FindModuleParams.Add('Prerelease', $true) } - # Version string, and equal to current - if ($Version -and $Version -ne 'latest' -and $Version -eq $ExistingVersion) + # Version string, and that version is already installed (may not be the maximum) + $matchedExisting = if ($Version -and $Version -ne 'latest') + { + $Existing | Where-Object { [string]$_.Version -eq $Version } | Select-Object -First 1 + } + if ($matchedExisting) { Write-Verbose "You have the requested version [$Version] of [$Name]" - Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $ExistingVersion + Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $matchedExisting.Version if ($PSDependAction -contains 'Test') { @@ -290,18 +294,26 @@ if ($Existing) $GalleryVersion = Find-PSResource @FindModuleParams | Measure-Object -Property Version -Maximum | Select-Object -ExpandProperty Maximum # Compare using SemanticVersion first (PSResourceGet uses SemVer); fall back to System.Version [System.Version]$parsedVersion = $null + [System.Version]$parsedGalleryVersion = $null [System.Management.Automation.SemanticVersion]$parsedSemanticVersion = $null [System.Management.Automation.SemanticVersion]$parsedTempSemanticVersion = $null $existingIsUpToDate = if ( - [System.Management.Automation.SemanticVersion]::TryParse($ExistingVersion, [ref]$parsedSemanticVersion) -and - [System.Management.Automation.SemanticVersion]::TryParse($GalleryVersion, [ref]$parsedTempSemanticVersion) + [System.Management.Automation.SemanticVersion]::TryParse([string]$ExistingVersion, [ref]$parsedSemanticVersion) -and + [System.Management.Automation.SemanticVersion]::TryParse([string]$GalleryVersion, [ref]$parsedTempSemanticVersion) ) { - $GalleryVersion -le $parsedSemanticVersion + $parsedTempSemanticVersion -le $parsedSemanticVersion } - elseif ([System.Version]::TryParse($ExistingVersion, [ref]$parsedVersion)) + elseif ( + [System.Version]::TryParse([string]$ExistingVersion, [ref]$parsedVersion) -and + [System.Version]::TryParse([string]$GalleryVersion, [ref]$parsedGalleryVersion) + ) { - $GalleryVersion -le $parsedVersion + $parsedGalleryVersion -le $parsedVersion + } + else + { + $false } # latest, and we have latest @@ -344,6 +356,12 @@ if ($PSDependAction -contains 'Install') } } -# Conditional import +# Conditional import — params['Version'] may be a NuGet range; resolve to a concrete installed version $importVs = $params['Version'] +if ($importVs -and $importVs -match '[\[\](,]') +{ + $importVs = Get-Module -ListAvailable -Name $ModuleName -ErrorAction SilentlyContinue | + Measure-Object -Property Version -Maximum | + Select-Object -ExpandProperty Maximum +} Import-PSDependModule -Name $ModuleName -Action $PSDependAction -Version $importVs diff --git a/Tests/PSResourceGet.Type.Tests.ps1 b/Tests/PSResourceGet.Type.Tests.ps1 index 2204ae2..d4b7abe 100644 --- a/Tests/PSResourceGet.Type.Tests.ps1 +++ b/Tests/PSResourceGet.Type.Tests.ps1 @@ -145,6 +145,24 @@ Describe 'PSResourceGet script' { } } + Context 'NuGet range version syntax' { + It 'Passes the range string to Install-PSResource and resolves a concrete version for Import' { + InModuleScope PSDepend { + Mock Get-Module { } -ParameterFilter { $ListAvailable } + Mock Get-Module { [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'1.5.0' } } ` + -ParameterFilter { -not $ListAvailable } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version '[1.0.0, )' + InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Install, Import + } + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Version -eq '[1.0.0, )' } + Should -Invoke -CommandName Import-PSDependModule -ModuleName PSDepend -Times 1 -Exactly ` + -ParameterFilter { $Version -ne '[1.0.0, )' } + } + } + Context 'Latest version comparison' { It 'Installs when installed version is behind the repository version' { InModuleScope PSDepend { @@ -160,6 +178,25 @@ Describe 'PSResourceGet script' { } } + Context 'Multiple installed versions — requested version present but not the maximum' { + It 'Returns $true and skips Install when the requested version is installed (even if a higher version also exists)' { + InModuleScope PSDepend { + Mock Get-Module { + @( + [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'1.2.3' } + [PSCustomObject]@{ Name = 'TestModule'; Version = [version]'2.0.0' } + ) + } -ParameterFilter { $ListAvailable } + } + $dep = New-PSDependFixture -DependencyName 'TestModule' -DependencyType 'PSResourceGet' -Version '1.2.3' + $result = InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { + & $ScriptPath -Dependency $Dep -PSDependAction Test + } + $result | Should -Be $true + Should -Invoke -CommandName Install-PSResource -ModuleName PSDepend -Times 0 + } + } + Context 'Target as path uses Save-PSResource instead of Install-PSResource' { It 'Calls Save-PSResource with -Path and skips Install-PSResource' { $savePath = (New-Item 'TestDrive:/psresourceget-save' -ItemType Directory -Force).FullName