Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.6.0] - 2026-09-30

### Added

- NuGet version-range support for Chocolatey dependencies. Ranges resolve to
the highest matching package version before installation.

### Fixed

- Build initialization now explicitly loads the pinned Pester 5 version before
importing PowerShellBuild and uses a Pester 5-compatible test task, preventing
a newer installed Pester version from being loaded.

## [0.5.0] - 2026-08-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion PSDepend/PSDepend.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
RootModule = 'PSDepend.psm1'

# Version number of this module.
ModuleVersion = '0.5.0'
ModuleVersion = '0.6.0'

# ID used to uniquely identify this module
GUID = '63ea9e2a-320d-43ff-a11a-4930ca03cce6'
Expand Down
78 changes: 65 additions & 13 deletions PSDepend/PSDependScripts/Chocolatey.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

Relevant Dependency metadata:
Name: The name of the package
Version: Used to identify existing installs meeting this criteria. Defaults to 'latest'
Version: Used to identify existing installs meeting this criteria. Defaults to 'latest'.
Also accepts a NuGet version range (e.g. '[2.2.3,3.0)', '[2.0,)', '(,3.0)').
A bare version remains an exact version. Ranges resolve to the highest matching version.
Source: Source Uri. Defaults to https://community.chocolatey.org/api/v2/

.PARAMETER Dependency
Expand Down Expand Up @@ -127,19 +129,24 @@ function Get-ChocoInstalledPackage {
Invoke-ExternalCommand @invokeExternalCommandSplat | ConvertFrom-Csv @convertFromCsvSplat
}

function Get-ChocoLatestPackage {
function Get-ChocoPackage {
[CmdletBinding()]
param (
[string]$Name,

[string]$Source,

[Management.Automation.PSCredential]$Credential
[Management.Automation.PSCredential]$Credential,

[switch]$AllVersions
)

# 'choco search' queries remote sources on both 1.x and 2.x; 'choco list' stopped
# querying remote sources in Chocolatey 2.0 and rejects URL sources (issue #187)
$chocoParams = @('search', "$Name", '--limit-output', '--exact')
if ($AllVersions.IsPresent) {
$chocoParams += '--all-versions'
}
if ($Source) {
$chocoParams += "--source='$Source'"
}
Expand Down Expand Up @@ -230,6 +237,23 @@ if (-not $Dependency.Source -or $Source -eq '') {

$Credential = $Dependency.Credential

$versionRange = $null
if ($Version -ne 'latest') {
$versionRange = ConvertFrom-VersionRange -Version $Version
if (-not $versionRange) {
Write-Error "Could not parse version [$Version] for [$Name]; expected an exact version or a valid NuGet range."
if ($PSDependAction -contains 'Test') {
return $false
}
return
}
}
$installVersion = if ($versionRange -and $versionRange.IsExact) {
$versionRange.Exact
} else {
$Version
}

if (-not (Get-Command -Name 'choco.exe' -ErrorAction SilentlyContinue)) {
Write-Verbose "Chocolatey is not installed. Installing from [$ChocoInstallScriptUrl]"
# download and run the Chocolatey script
Expand All @@ -251,9 +275,22 @@ if (-not (Get-Command -Name 'choco.exe' -ErrorAction SilentlyContinue)) {
# If this is a forced install we don't need to check anything,
# just install the package version requested
if ($Force.IsPresent -and $PSDependAction -contains 'Install') {
if ($versionRange -and -not $versionRange.IsExact) {
$availableVersions = (Get-ChocoPackage -Name $Name -Source $Source -Credential $Credential -AllVersions).Version
$installVersion = Resolve-VersionInRange -Candidate $availableVersions -Required $Version
if (-not $installVersion) {
Write-Error "No version of [$Name] at source [$Source] satisfies range [$Version]"
if ($PSDependAction -contains 'Test') {
return $false
}
return
}
Write-Verbose "Resolved range [$Version] to version [$installVersion] for [$Name]"
}

$params = @{
Name = $Name
Version = $Version
Version = $installVersion
Source = $Source
Force = $Force.IsPresent
}
Expand All @@ -278,7 +315,7 @@ if ($existingVersion) {
}

# Specific version requested, and equal to current
if ($Version -ne 'latest' -and (Test-VersionEquality -ReferenceVersion $Version -DifferenceVersion $existingVersion)) {
if ($Version -ne 'latest' -and (Test-VersionInRange -Version $existingVersion -Required $Version)) {
Write-Verbose "You have the requested version [$Version] of [$Name]"
if ($PSDependAction -contains 'Test') {
return $true
Expand All @@ -287,7 +324,8 @@ if ($Version -ne 'latest' -and (Test-VersionEquality -ReferenceVersion $Version
return
}

# get the latest version from the source
# Resolve a range to the highest available version. Chocolatey requires an exact
# version for --version.
$repoParams = @{
Name = $Name
Source = $Source
Expand All @@ -296,13 +334,27 @@ if ($Credential) {
$repoParams.Credential = $Credential
}

Write-Verbose "Getting latest package [$Name] version from source [$Source]."
$repositoryVersion = (Get-ChocoLatestPackage @repoParams).Version
if ($repositoryVersion) {
Write-Verbose "Found package [$Name] version [$repositoryVersion] on source [$Source]."
if ($versionRange -and -not $versionRange.IsExact) {
Write-Verbose "Getting available package [$Name] versions from source [$Source]."
$availableVersions = (Get-ChocoPackage @repoParams -AllVersions).Version
$installVersion = Resolve-VersionInRange -Candidate $availableVersions -Required $Version
if (-not $installVersion) {
Write-Error "No version of [$Name] at source [$Source] satisfies range [$Version]"
if ($PSDependAction -contains 'Test') {
return $false
}
return
}
Write-Verbose "Resolved range [$Version] to version [$installVersion] for [$Name]"
} else {
Write-Verbose "Package [$Name] not found on source [$Source]. Nothing more can be done."
return # cannot continue
Write-Verbose "Getting latest package [$Name] version from source [$Source]."
$repositoryVersion = (Get-ChocoPackage @repoParams).Version
if ($repositoryVersion) {
Write-Verbose "Found package [$Name] version [$repositoryVersion] on source [$Source]."
} else {
Write-Verbose "Package [$Name] not found on source [$Source]. Nothing more can be done."
return # cannot continue
}
}

# If the version in the remote repository is less than or equal to the version installed, then we have the latest already
Expand Down Expand Up @@ -338,7 +390,7 @@ Write-Verbose "You do not have the version requested of [$Name]: Requested versi
if ($PSDependAction -contains 'Install') {
$params = @{
Name = $Name
Version = $Version
Version = $installVersion
Source = $Source
Force = $Force.IsPresent
}
Expand Down
79 changes: 79 additions & 0 deletions Tests/Chocolatey.Type.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,83 @@ Describe 'Chocolatey script' -Tag 'WindowsOnly' -Skip:$SkipUnsupported {
($Arguments -join ' ') -match "--username='feeduser'" -and ($Arguments -join ' ') -match "--password='feedpass'"
}
}

Context 'NuGet version ranges' {
It 'Installs the highest available version that satisfies the range' {
$dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '[2.0.0,3.0.0)'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
Mock Invoke-ExternalCommand {
'git|1.9.0'
'git|2.5.0'
'git|3.0.0'
} -ParameterFilter { $Arguments -contains '--all-versions' }

& $ScriptPath -Dependency $Dep
}
Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter {
$Arguments -contains 'upgrade' -and $Arguments -contains "--version='2.5.0'"
}
}

It 'Resolves a range before a forced installation' {
$dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '[2.0.0,3.0.0)'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
Mock Invoke-ExternalCommand {
'git|2.4.0'
'git|2.8.0'
} -ParameterFilter { $Arguments -contains '--all-versions' }

& $ScriptPath -Dependency $Dep -Force
}
Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter {
$Arguments -contains 'upgrade' -and
$Arguments -contains "--version='2.8.0'" -and
$Arguments -contains '--force'
}
}

It 'Passes a bracketed exact range as a concrete version' {
$dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '[2.0.0]'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -Force
}
Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter {
$Arguments -contains 'upgrade' -and $Arguments -contains "--version='2.0.0'"
}
}

It 'Accepts an installed version that satisfies the range' {
$dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '[2.0.0,3.0.0)'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
Mock Invoke-ExternalCommand { 'git|2.4.0' } -ParameterFilter { $Arguments -contains 'list' }

& $ScriptPath -Dependency $Dep -PSDependAction Test
} | Should -BeTrue
Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 0 -Exactly -ParameterFilter {
$Arguments -contains 'search'
}
}

It 'Returns false when no available version satisfies the range during testing' {
$dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '[2.0.0,3.0.0)'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
Mock Invoke-ExternalCommand { 'git|1.9.0' } -ParameterFilter { $Arguments -contains '--all-versions' }

& $ScriptPath -Dependency $Dep -PSDependAction Test -ErrorAction SilentlyContinue
} | Should -BeFalse
Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 0 -Exactly -ParameterFilter {
$Arguments -contains 'upgrade'
}
}

It 'Skips installation for a malformed range' {
$dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' -Version '[2.0.0,3.0.0'
InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } {
& $ScriptPath -Dependency $Dep -ErrorAction SilentlyContinue
}
Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 0 -Exactly -ParameterFilter {
$Arguments -contains 'upgrade'
}
}
}
}
20 changes: 15 additions & 5 deletions build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ param(
)

$ErrorActionPreference = 'Stop'
$psakeFile = './psakeFile.ps1'
$requirementsFile = Join-Path $PSScriptRoot 'requirements.psd1'
$psakeFile = Join-Path $PSScriptRoot 'psakeFile.ps1'

$requirements = Import-PowerShellDataFile -Path $requirementsFile
$pesterRequirement = $requirements.Pester
$requirements.Remove('Pester')

if ($Bootstrap) {
if (-not (Get-PackageProvider -Name NuGet -ErrorAction SilentlyContinue)) {
Expand All @@ -49,13 +54,18 @@ if ($Bootstrap) {
if (-not (Get-Module -Name PSDepend -ListAvailable)) {
Install-Module -Name PSDepend -Repository PSGallery -Scope CurrentUser -Force -RequiredVersion '0.3.8'
}
Import-Module -Name PSDepend -Verbose:$false
Invoke-PSDepend -Path './requirements.psd1' -Install -Import -Force -WarningAction SilentlyContinue
}
else {
Invoke-PSDepend -Path './requirements.psd1' -Import -Force -WarningAction SilentlyContinue

Import-Module -Name PSDepend -Verbose:$false
if ($Bootstrap) {
Invoke-PSDepend -Path $requirementsFile -Install -Force -WarningAction SilentlyContinue
}

# PowerShellBuild requires Pester without an upper bound. Load the pinned version
# first, then import the remaining dependencies without re-importing Pester.
Import-Module -Name Pester -RequiredVersion $pesterRequirement.Version
Invoke-PSDepend -InputObject $requirements -Import -Force -WarningAction SilentlyContinue

if ($PSCmdlet.ParameterSetName -eq 'Help') {
Get-PSakeScriptTasks -BuildFile $psakeFile |
Format-Table -Property Name, Description, Alias, DependsOn
Expand Down
25 changes: 25 additions & 0 deletions psakeFile.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,31 @@ Properties {
# Skips BuildHelp (GenerateMarkdown) — doc generation is not needed in the test pipeline
# and Build-PSBuildMarkdown has a Remove-Module scope bug specific to PSDepend.
$PSBBuildDependency = @('StageFiles')
$PSBTestDependency = @('Pester5', 'Analyze')

Task Pester5 -Depends $PSBBuildDependency {
$moduleManifest = Join-Path $PSBPreference.Build.ModuleOutDir "$($PSBPreference.General.ModuleName).psd1"

Import-Module -Name $moduleManifest -Force
Push-Location -LiteralPath $PSBPreference.Test.RootDir
try {
$configuration = [PesterConfiguration]::Default
$configuration.Output.Verbosity = 'Detailed'
$configuration.Run.PassThru = $true
$configuration.TestResult.Enabled = $true
$configuration.TestResult.OutputPath = $PSBPreference.Test.OutputFile
$configuration.TestResult.OutputFormat = $PSBPreference.Test.OutputFormat

$testResult = Invoke-Pester -Configuration $configuration
if ($testResult.Result -eq 'Failed') {
throw 'One or more Pester tests failed.'
}
}
finally {
Pop-Location
Remove-Module -Name $PSBPreference.General.ModuleName -ErrorAction SilentlyContinue
}
} -Description 'Executes Pester 5 tests'

# Override with: .\build.ps1 InstallLocal -Properties @{PreReleaseLabel='rc1'}
Task InstallLocal -Depends StageFiles {
Expand Down
8 changes: 4 additions & 4 deletions requirements.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
'psake' = @{
Version = '4.9.1'
}
'PowerShellBuild' = @{
Version = '0.7.3'
}
'Pester' = @{
Version = '6.1.0'
Version = '5.9.0'
Parameters = @{
SkipPublisherCheck = $true
}
}
'PowerShellBuild' = @{
Version = '0.8.2'
}
'PSScriptAnalyzer' = @{
Version = '1.19.1'
}
Expand Down
Loading