From 81dc9495636d0335b6f6632ecd4271c4b28e8886 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Fri, 12 Jun 2026 13:56:03 -0700 Subject: [PATCH 1/3] fix: modernize dependency scripts for current tool versions Chocolatey.ps1 (fixes #187): - Use 'choco search' for remote version queries; 'choco list' stopped querying remote sources in Chocolatey 2.0 and rejects URL sources - Version-gate --local-only via new Get-ChocoVersion helper (flag was removed in Chocolatey 2.0) - Default Source and install script to community.chocolatey.org - Fix undefined $scriptUrl in bootstrap catch block - Use Test-VersionEquality for explicit-version checks and TryParse for latest checks (raw [System.Version] casts throw on prerelease) GitHub.ps1: - Replace COM shell.application zip extraction with Expand-Archive (module floor is PS 5.1) - Suppress New-Item pipeline leak into the script output stream PSGalleryNuget.ps1: - Replace lexical string version comparison with typed SemanticVersion/Version TryParse ("10.0.0" -le "9.0.0" is true as strings, causing needless reinstalls) FileSystem.ps1: - Fix operator precedence in missing-source check (-not $array -like evaluated to always-false, error never surfaced) - Add documented Force/Mirror parameters missing from the param block (Parameters splat was a binding error) - Normalize action checks from -like to -contains Task.ps1: - Honor documented Target field (code only read Source, so documented usage silently ran zero tasks) Git.ps1: return after "git not found" error instead of invoking git anyway Command.ps1: fix loop variable in verbose output; document FailOnError Npm.ps1: remove dead $PackageListArguments; use Join-Path for target Tests updated for the new choco list/search contract with regression coverage for 1.x vs 2.x flag detection, and the new default source URL. Co-Authored-By: Claude Fable 5 --- PSDepend/PSDependScripts/Chocolatey.ps1 | 55 ++++++++++++++++----- PSDepend/PSDependScripts/Command.ps1 | 5 +- PSDepend/PSDependScripts/FileSystem.ps1 | 22 +++++---- PSDepend/PSDependScripts/Git.ps1 | 1 + PSDepend/PSDependScripts/GitHub.ps1 | 12 +---- PSDepend/PSDependScripts/Npm.ps1 | 3 +- PSDepend/PSDependScripts/PSGalleryNuget.ps1 | 40 +++++++++++---- PSDepend/PSDependScripts/Task.ps1 | 5 +- Tests/Chocolatey.Type.Tests.ps1 | 4 +- Tests/PSModuleGallery.Type.Tests.ps1 | 51 +++++++++++++------ 10 files changed, 134 insertions(+), 64 deletions(-) diff --git a/PSDepend/PSDependScripts/Chocolatey.ps1 b/PSDepend/PSDependScripts/Chocolatey.ps1 index 8d185b4..235e49f 100644 --- a/PSDepend/PSDependScripts/Chocolatey.ps1 +++ b/PSDepend/PSDependScripts/Chocolatey.ps1 @@ -8,7 +8,7 @@ Relevant Dependency metadata: Name: The name of the package Version: Used to identify existing installs meeting this criteria. Defaults to 'latest' - Source: Source Uri. Defaults to https://chocolatey.org/api/v2/ + Source: Source Uri. Defaults to https://community.chocolatey.org/api/v2/ .PARAMETER Force If specified and the package is already installed, force the install again. @@ -64,12 +64,33 @@ param( [switch]$Force, - [string]$ChocoInstallScriptUrl = 'https://chocolatey.org/install.ps1', + [string]$ChocoInstallScriptUrl = 'https://community.chocolatey.org/install.ps1', [ValidateSet('Test', 'Install')] [string[]]$PSDependAction = @('Install') ) +function Get-ChocoVersion { + [CmdletBinding()] + param () + + $invokeExternalCommandSplat = @{ + Command = 'choco.exe' + Arguments = @('--version') + PassThru = $true + } + $rawVersion = [string](Invoke-ExternalCommand @invokeExternalCommandSplat | Select-Object -First 1) + [System.Version]$parsedVersion = $null + # Strip prerelease/build metadata (e.g. 2.2.2-beta) before parsing + if ([System.Version]::TryParse(($rawVersion -replace '[-+].*$'), [ref]$parsedVersion)) { + $parsedVersion + } + else { + # Assume a modern CLI when the version cannot be determined + [System.Version]'2.0' + } +} + function Get-ChocoInstalledPackage { [CmdletBinding()] param ( @@ -80,9 +101,13 @@ function Get-ChocoInstalledPackage { 'list', "$Name", '--limit-output', - '--exact', - '--local-only' + '--exact' ) + # Chocolatey 2.0 removed --local-only ('choco list' is now local-only by default); + # before 2.0, 'choco list' queried remote sources unless the flag was passed + if ((Get-ChocoVersion).Major -lt 2) { + $chocoParams += '--local-only' + } $invokeExternalCommandSplat = @{ Command = 'choco.exe' Arguments = $chocoParams @@ -105,7 +130,9 @@ function Get-ChocoLatestPackage { [Management.Automation.PSCredential]$Credential ) - $chocoParams = @('list', "$Name", '--limit-output', '--exact') + # '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 ($Source) { $chocoParams += "--source='$Source'" } @@ -191,7 +218,7 @@ if (-not $Dependency.Version -or $Version -eq '') { $Source = $Dependency.Source if (-not $Dependency.Source -or $Source -eq '') { - $Source = 'https://chocolatey.org/api/v2/' + $Source = 'https://community.chocolatey.org/api/v2/' } $Credential = $Dependency.Credential @@ -211,7 +238,7 @@ if (-not (Get-Command -Name 'choco.exe' -ErrorAction SilentlyContinue)) { & $scriptPath } catch { - throw "Unable to install Chocolatey from '$scriptUrl'." + throw "Unable to install Chocolatey from '$ChocoInstallScriptUrl'." } } @@ -245,8 +272,8 @@ else { Write-Verbose "Package [$Name] not installed." } -# Version latest requested, and equal to current -if ($Version -ne 'latest' -and $Version -eq $existingVersion) { +# Specific version requested, and equal to current +if ($Version -ne 'latest' -and (Test-VersionEquality -ReferenceVersion $Version -DifferenceVersion $existingVersion)) { Write-Verbose "You have the requested version [$Version] of [$Name]" if ($PSDependAction -contains 'Test') { return $true @@ -275,10 +302,12 @@ else { } # If the version in the remote repository is less than or equal to the version installed, then we have the latest already -if ( - $Version -eq 'latest' -and - ([System.Version]$repositoryVersion -le [System.Version]$existingVersion) -) { +[System.Version]$parsedRepositoryVersion = $null +[System.Version]$parsedExistingVersion = $null +$haveLatest = [System.Version]::TryParse($repositoryVersion, [ref]$parsedRepositoryVersion) -and + [System.Version]::TryParse($existingVersion, [ref]$parsedExistingVersion) -and + $parsedRepositoryVersion -le $parsedExistingVersion +if ($Version -eq 'latest' -and $haveLatest) { Write-Verbose "You have the latest version of [$Name], with installed version [$existingVersion] and Source version [$repositoryVersion]" if ($PSDependAction -contains 'Test') { return $true diff --git a/PSDepend/PSDependScripts/Command.ps1 b/PSDepend/PSDependScripts/Command.ps1 index 5d49d14..c1a4d40 100644 --- a/PSDepend/PSDependScripts/Command.ps1 +++ b/PSDepend/PSDependScripts/Command.ps1 @@ -22,6 +22,9 @@ .PARAMETER Dependency Dependency to process + .PARAMETER FailOnError + If specified, throw a terminating error if the command errors out + .EXAMPLE @{ ExampleCommand = @{ @@ -48,7 +51,7 @@ Write-Verbose "Executing $($Dependency.count) commands" foreach ($Depend in $Dependency) { foreach ($Command in $Depend.Source) { - Write-Verbose "Invoking command [$($Dependency.DependencyName)]:`n$Command" + Write-Verbose "Invoking command [$($Depend.DependencyName)]:`n$Command" $ScriptBlock = [ScriptBlock]::Create($Command) Try { . $ScriptBlock diff --git a/PSDepend/PSDependScripts/FileSystem.ps1 b/PSDepend/PSDependScripts/FileSystem.ps1 index 7ef8617..0992bca 100644 --- a/PSDepend/PSDependScripts/FileSystem.ps1 +++ b/PSDepend/PSDependScripts/FileSystem.ps1 @@ -68,7 +68,11 @@ param ( [ValidateSet('Test', 'Install', 'Import')] [string[]]$PSDependAction = @('Install'), - [string]$ImportPath + [string]$ImportPath, + + [switch]$Force, + + [switch]$Mirror ) # Extract data from Dependency @@ -80,8 +84,8 @@ $Sources = @($Dependency.Source) $TestOutput = @() foreach ($Source in @($Sources)) { if (-not (Test-Path $Source)) { - if (-not $PSDependAction -like 'Test') { - Write-Error "Skipping $DependencyName, could not find source [$Sources] due to error:" + if ($PSDependAction -notcontains 'Test') { + Write-Error "Skipping $DependencyName, could not find source [$Sources]" } continue } @@ -102,7 +106,7 @@ foreach ($Source in @($Sources)) { else { $TestOutput += $false } - if ($PSDependAction -like 'Install') { + if ($PSDependAction -contains 'Install') { # TODO: Add non Windows equivalent... [string[]]$Arguments = "/XO" $Arguments += "/E" @@ -132,24 +136,24 @@ foreach ($Source in @($Sources)) { if ($TargetHash -ne $SourceHash) { Write-Verbose "Hashes do not match" - if ($PSDependAction -like 'Install') { + if ($PSDependAction -contains 'Install') { Write-Verbose "Copying file [$Source] to [$Target]" Copy-Item -Path $Source -Destination $Target -Force } - if ($PSDependAction -like 'Test' -and $PSDependAction.count -eq 1) { + if ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) { $TestOutput += $false } } else { Write-Verbose "Matching hash: [$Source] = [$TargetFile]" - if ($PSDependAction -like 'Test' -and $PSDependAction.count -eq 1) { + if ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) { $TestOutput += $True } } } } -if ($PSDependAction -like 'Test' -and $PSDependAction.count -eq 1) { +if ($PSDependAction -contains 'Test' -and $PSDependAction.count -eq 1) { if (@($TestOutput) -contains $false -or @($TestOutput) -notcontains $true) { return $false } @@ -158,7 +162,7 @@ if ($PSDependAction -like 'Test' -and $PSDependAction.count -eq 1) { } } -if ($PSDependAction -like 'Import') { +if ($PSDependAction -contains 'Import') { if (-not $ImportPath) { if (Test-Path $Target -PathType Leaf) { $ImportPath = Split-Path $Target -Parent diff --git a/PSDepend/PSDependScripts/Git.ps1 b/PSDepend/PSDependScripts/Git.ps1 index f541393..64c4e7a 100644 --- a/PSDepend/PSDependScripts/Git.ps1 +++ b/PSDepend/PSDependScripts/Git.ps1 @@ -118,6 +118,7 @@ else { # Target exists if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Write-Error "Git dependency type requires git. Ensure this is in your path, or explicitly specified in $ModuleRoot\PSDepend.Config's GitPath. Skipping [$DependencyName]" + return } $Version = $Dependency.Version diff --git a/PSDepend/PSDependScripts/GitHub.ps1 b/PSDepend/PSDependScripts/GitHub.ps1 index a6408a0..b431646 100644 --- a/PSDepend/PSDependScripts/GitHub.ps1 +++ b/PSDepend/PSDependScripts/GitHub.ps1 @@ -412,15 +412,7 @@ if (($PSDependAction -contains 'Install') -and $ShouldInstall) { } # Extract the zip file - if ($script:IsWindows) { - $ZipFile = (New-Object -com shell.application).NameSpace($OutFile) - $ZipDestination = (New-Object -com shell.application).NameSpace($OutPath) - $ZipDestination.CopyHere($ZipFile.Items()) - } - else { - # If not on Windows "Expand-Archive" should be available as PS version 6 is considered minimum. - Expand-Archive $OutFile -DestinationPath $OutPath - } + Expand-Archive -Path $OutFile -DestinationPath $OutPath # Remove the zip file Remove-Item $OutFile -Force -Confirm:$false @@ -454,7 +446,7 @@ if (($PSDependAction -contains 'Install') -and $ShouldInstall) { # Copy the contents to their target if (-not (Test-Path $TargetPath)) { - New-Item $TargetPath -ItemType "directory" -Force + $null = New-Item $TargetPath -ItemType "directory" -Force } $Destination = $null diff --git a/PSDepend/PSDependScripts/Npm.ps1 b/PSDepend/PSDependScripts/Npm.ps1 index 5aa4c04..5ddfa12 100644 --- a/PSDepend/PSDependScripts/Npm.ps1 +++ b/PSDepend/PSDependScripts/Npm.ps1 @@ -73,7 +73,7 @@ If (-not [string]::IsNullOrEmpty($Target) -and $Target -ne 'global') { # Otherwise, assume that its a folder _in the current directory_. # If no target is specified, it will install to the current directory. If ($Target -notmatch '(^/|:|\\\\)') { - $Target = "$PWD\$Target" + $Target = Join-Path $PWD $Target } If (-not (Test-Path $Target) -and $PSDependAction -contains 'Install') { Write-Verbose "Creating folder [$Target] for node module dependency [$Name]" @@ -83,7 +83,6 @@ If (-not [string]::IsNullOrEmpty($Target) -and $Target -ne 'global') { #endregion Extract Dependency Data #region Test Action If ($PSDependAction -contains 'Test') { - $PackageListArguments = 'ls --json --silent' If ([string]::IsNullOrEmpty($Target)) { $InstalledNodeModules = Get-NodeModule } diff --git a/PSDepend/PSDependScripts/PSGalleryNuget.ps1 b/PSDepend/PSDependScripts/PSGalleryNuget.ps1 index 6c65433..4bc9a96 100644 --- a/PSDepend/PSDependScripts/PSGalleryNuget.ps1 +++ b/PSDepend/PSDependScripts/PSGalleryNuget.ps1 @@ -137,17 +137,37 @@ if (Test-Path $ModulePath) { } # latest, and we have latest - if ( $Version -and - ($Version -eq 'latest' -or $Version -like '') -and - ($GalleryVersion = (& $GetGalleryVersion)) -le $ExistingVersion - ) { - Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and PSGallery version [$GalleryVersion]" - # Conditional import - Import-PSDependModule -Name $ModulePath -Action $PSDependAction -Version $ExistingVersion - if ($PSDependAction -contains 'Test') { - return $True + if ($Version -and ($Version -eq 'latest' -or $Version -like '')) { + $GalleryVersion = & $GetGalleryVersion + [System.Version]$parsedExistingVersion = $null + [System.Version]$parsedGalleryVersion = $null + [System.Management.Automation.SemanticVersion]$parsedExistingSemanticVersion = $null + [System.Management.Automation.SemanticVersion]$parsedGallerySemanticVersion = $null + $isGalleryVersionLessEquals = if ( + [System.Management.Automation.SemanticVersion]::TryParse([string]$ExistingVersion, [ref]$parsedExistingSemanticVersion) -and + [System.Management.Automation.SemanticVersion]::TryParse([string]$GalleryVersion, [ref]$parsedGallerySemanticVersion) + ) { + $parsedGallerySemanticVersion -le $parsedExistingSemanticVersion + } + elseif ( + [System.Version]::TryParse([string]$ExistingVersion, [ref]$parsedExistingVersion) -and + [System.Version]::TryParse([string]$GalleryVersion, [ref]$parsedGalleryVersion) + ) { + $parsedGalleryVersion -le $parsedExistingVersion + } + else { + $false + } + + if ($isGalleryVersionLessEquals) { + Write-Verbose "You have the latest version of [$Name], with installed version [$ExistingVersion] and PSGallery version [$GalleryVersion]" + # Conditional import + Import-PSDependModule -Name $ModulePath -Action $PSDependAction -Version $ExistingVersion + if ($PSDependAction -contains 'Test') { + return $True + } + return $null } - return $null } Write-Verbose "Removing existing [$ModulePath]`nContinuing to install [$Name]: Requested version [$version], existing version [$ExistingVersion]" diff --git a/PSDepend/PSDependScripts/Task.ps1 b/PSDepend/PSDependScripts/Task.ps1 index a6c67a4..770fccb 100644 --- a/PSDepend/PSDependScripts/Task.ps1 +++ b/PSDepend/PSDependScripts/Task.ps1 @@ -6,7 +6,7 @@ Support dependencies by handling simple tasks. Relevant Dependency metadata: - Target: One or more scripts to run for this task + Target: One or more scripts to run for this task (Source is honored as an alias) Parameters: Parameters to call against the task scripts .PARAMETER PSDependAction @@ -57,7 +57,8 @@ param ( Write-Verbose "Executing $($Dependency.count) tasks" foreach ($Depend in $Dependency) { - foreach ($Task in $Depend.Source) { + $Tasks = if ($Depend.Source) { $Depend.Source } else { $Depend.Target } + foreach ($Task in $Tasks) { if (Test-Path $Task -PathType Leaf) { $params = @{} if ($Depend.Parameters) { diff --git a/Tests/Chocolatey.Type.Tests.ps1 b/Tests/Chocolatey.Type.Tests.ps1 index a1957cb..19e3424 100644 --- a/Tests/Chocolatey.Type.Tests.ps1 +++ b/Tests/Chocolatey.Type.Tests.ps1 @@ -29,13 +29,13 @@ Describe 'Chocolatey script' -Tag 'WindowsOnly' -Skip:$SkipUnsupported { } } - It 'Defaults Source to https://chocolatey.org/api/v2/ when not supplied' { + It 'Defaults Source to https://community.chocolatey.org/api/v2/ when not supplied' { $dep = New-PSDependFixture -DependencyName 'git' -DependencyType 'Chocolatey' InModuleScope PSDepend -Parameters @{ Dep = $dep; ScriptPath = $script:ScriptPath } { & $ScriptPath -Dependency $Dep -Force } Should -Invoke -CommandName Invoke-ExternalCommand -ModuleName PSDepend -Times 1 -Exactly -ParameterFilter { - ($Arguments -join ' ') -match "--source='https://chocolatey\.org/api/v2/'" + ($Arguments -join ' ') -match "--source='https://community\.chocolatey\.org/api/v2/'" } } diff --git a/Tests/PSModuleGallery.Type.Tests.ps1 b/Tests/PSModuleGallery.Type.Tests.ps1 index f4665cf..391d378 100644 --- a/Tests/PSModuleGallery.Type.Tests.ps1 +++ b/Tests/PSModuleGallery.Type.Tests.ps1 @@ -1279,12 +1279,12 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Package version installed is what is requested' { It 'skips installing the package' { - Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments -contains '--local-only' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments[0] -eq 'list' } -ModuleName PSDepend Invoke-PSDepend @Verbose -Path "$TestDepends\chocolatey.specificversionrequested.depend.psd1" -Force -ErrorAction Stop - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments -contains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -Times 0 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' } -Times 1 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'search' } -Times 0 -Exactly -ModuleName PSDepend Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'upgrade' } -Times 0 -Exactly -ModuleName PSDepend } } @@ -1292,13 +1292,13 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Package version installed is latest' { It 'skips installing the package' { - Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments -contains '--local-only' } -ModuleName PSDepend - Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments[0] -eq 'list' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments[0] -eq 'search' } -ModuleName PSDepend Invoke-PSDepend @Verbose -Path "$TestDepends\chocolatey.latestversionrequested.depend.psd1" -Force -ErrorAction Stop - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments -contains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' } -Times 1 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'search' } -Times 1 -Exactly -ModuleName PSDepend Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'upgrade' } -Times 0 -Exactly -ModuleName PSDepend } } @@ -1306,13 +1306,13 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Package requested is latest and version installed is newer than available in source' { It 'skips installing the package' { - Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments -contains '--local-only' } -ModuleName PSDepend - Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments[0] -eq 'list' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments[0] -eq 'search' } -ModuleName PSDepend Invoke-PSDepend @Verbose -Path "$TestDepends\chocolatey.latestversionrequested.depend.psd1" -Force -ErrorAction Stop - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments -contains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' } -Times 1 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'search' } -Times 1 -Exactly -ModuleName PSDepend Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'upgrade' } -Times 0 -Exactly -ModuleName PSDepend } } @@ -1320,15 +1320,36 @@ Describe "PSModuleGallery Type" -Tag 'Integration' { Context 'Package requested is latest and version installed is older than available in source' { It 'installs the package' { - Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments -contains '--local-only' } -ModuleName PSDepend - Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments[0] -eq 'list' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|2.0" } -ParameterFilter { $Arguments[0] -eq 'search' } -ModuleName PSDepend Invoke-PSDepend @Verbose -Path "$TestDepends\chocolatey.latestversionrequested.depend.psd1" -Force -ErrorAction Stop - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments -contains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend - Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' } -Times 1 -Exactly -ModuleName PSDepend + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'search' } -Times 1 -Exactly -ModuleName PSDepend Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'upgrade' } -Times 1 -Exactly -ModuleName PSDepend } } + + Context 'Chocolatey CLI version detection' { + + It 'passes --local-only to choco list on Chocolatey 1.x' { + Mock Invoke-ExternalCommand { '1.4.0' } -ParameterFilter { $Arguments -contains '--version' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments[0] -eq 'list' } -ModuleName PSDepend + + Invoke-PSDepend @Verbose -Path "$TestDepends\chocolatey.specificversionrequested.depend.psd1" -Force -ErrorAction Stop + + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -contains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend + } + + It 'omits --local-only from choco list on Chocolatey 2.x' { + Mock Invoke-ExternalCommand { '2.2.2' } -ParameterFilter { $Arguments -contains '--version' } -ModuleName PSDepend + Mock Invoke-ExternalCommand { "7zip|1.0" } -ParameterFilter { $Arguments[0] -eq 'list' } -ModuleName PSDepend + + Invoke-PSDepend @Verbose -Path "$TestDepends\chocolatey.specificversionrequested.depend.psd1" -Force -ErrorAction Stop + + Should -Invoke Invoke-ExternalCommand -ParameterFilter { $Arguments[0] -eq 'list' -and $Arguments -notcontains '--local-only' } -Times 1 -Exactly -ModuleName PSDepend + } + } } } From 6e55c5dc021ce71640d49b9a0fe5cbbda0ed2498 Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez Date: Fri, 12 Jun 2026 13:56:16 -0700 Subject: [PATCH 2/3] docs: add tool-currency, secrets-in-argv, and stream-hygiene checklist items The reviewer checklist verified invocation mechanics but not whether the wrapped tool's CLI contract is still valid - the class of breakage in issue #187 (choco list changed semantics in Chocolatey 2.0) passed every existing item. Add: - External tool and endpoint currency: version-gate CLI flags across supported tool majors, prefer canonical endpoint URLs, honor Credential for rate-limited APIs, flag deprecated upstream providers - Docs-vs-code drift: documented Dependency fields must be read by the code and documented parameters must exist in the param block - Output-stream hygiene: object-emitting cmdlets must not pollute the stream that carries Test booleans - Secrets on process command lines, not just in Write-Verbose output Co-Authored-By: Claude Fable 5 --- docs/PSDependScripts-ReviewerChecklist.md | 53 +++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/docs/PSDependScripts-ReviewerChecklist.md b/docs/PSDependScripts-ReviewerChecklist.md index 223facb..e0fe8b3 100644 --- a/docs/PSDependScripts-ReviewerChecklist.md +++ b/docs/PSDependScripts-ReviewerChecklist.md @@ -86,6 +86,32 @@ and is consistent with the principle that PSDepend should be idempotent. - `Write-Error` (not `throw`) for recoverable failures. - `Write-Warning` for skip-and-continue cases (see `Task.ps1`). +### 9. External tool and endpoint currency + +Wrapper scripts break when the thing they wrap moves, not just when the +PowerShell is wrong. `choco list` silently changed meaning in Chocolatey 2.0 +(local-only by default, `--local-only` removed, remote queries moved to +`choco search`) and the script kept passing review because every *mechanics* +check still passed — see issue #187. + +- Know the CLI contract for every major version of the tool the script + supports, and version-gate flags that changed (see `Get-ChocoVersion` in + `Chocolatey.ps1`). +- Default endpoint URLs go stale: prefer the current canonical host + (e.g. `community.chocolatey.org`, not the legacy `chocolatey.org` redirect) + and note known-legacy defaults (the nuget.exe scripts still default to + v2 OData feeds). +- If the underlying provider is deprecated upstream (PowerShellGet v2, + PackageManagement), the script's help must say so and point at the + supported alternative (see `PSResourceGet.ps1`). + +### 10. Output-stream hygiene + +A dependency script's return value *is* its output stream — `Test` returns +booleans through it. Any cmdlet that emits objects (`New-Item`, `Copy-Item +-PassThru`, `Install-Package`) must be assigned to `$null` or piped to +`Out-Null`, or it corrupts the Test result seen by the engine. + ## Reviewer checklist Use this as a PR review checklist when adding or modifying a script under @@ -103,6 +129,11 @@ Use this as a PR review checklist when adding or modifying a script under - [ ] `.SYNOPSIS` and `.DESCRIPTION` are present. - [ ] "Relevant Dependency metadata" block enumerates **every** `$Dependency.*` field the script reads. +- [ ] Every field the help documents is **actually read by the code** — and + every documented parameter exists in the param block. Drift in either + direction ships a silent no-op or a binding error (`Task.ps1` documented + `Target` while the code read only `Source`; `FileSystem.ps1` documented + `Force`/`Mirror` with no matching parameters). - [ ] Every parameter has a `.PARAMETER` entry. - [ ] At least one `.EXAMPLE` with a runnable `@{ }` hashtable. @@ -130,9 +161,26 @@ Use this as a PR review checklist when adding or modifying a script under - [ ] Failures use `Write-Error` (not `throw`) unless terminating is intended (e.g. `FailOnError`). - [ ] No `Out-Null` / `2>$null` swallowing of error streams. +- [ ] No pipeline pollution: object-emitting cmdlets (`New-Item`, + `Install-Package`, etc.) are assigned to `$null` so stray objects don't + corrupt the `Test` boolean in the output stream. - [ ] Verbose messages on each decision branch. - [ ] Cross-platform paths use `Join-Path` (not string concat with `\`). +### External tool currency + +- [ ] The script's CLI calls are valid on **every major version of the + external tool it claims to support** — flags and subcommands that + changed between majors are version-gated (Chocolatey 2.0 removed + `--local-only` and made `choco list` local-only; see issue #187 and + `Get-ChocoVersion` in `Chocolatey.ps1`). +- [ ] Default registry/feed URLs are the current canonical endpoints, not + legacy redirects. +- [ ] Unauthenticated calls to rate-limited public APIs (e.g. + `api.github.com`) honor `Credential` so CI runs don't hit limits. +- [ ] If the wrapped provider is deprecated upstream, the help says so and + names the supported successor. + ### Version comparison (for installers) - [ ] When an explicit version is requested, **all locally installed versions @@ -147,6 +195,11 @@ Use this as a PR review checklist when adding or modifying a script under ### Security / hygiene - [ ] No plaintext credentials emitted in `Write-Verbose`. +- [ ] No secrets passed as external-process **command-line arguments** where + avoidable — argv is visible to any process lister and to verbose + command echoing (`Chocolatey.ps1` passes `--password=''` + to choco; prefer the tool's config/env mechanism when one exists, and + flag any new occurrence). - [ ] No `Invoke-Expression` on dependency data. `Command.ps1` uses `[ScriptBlock]::Create` — that's the documented trust boundary; any new script doing this needs an explicit opt-in From be83442a4cf134dd85b20b33670e6706606fbfdf Mon Sep 17 00:00:00 2001 From: Gilbert Sanchez <me@gilbertsanchez.com> Date: Fri, 12 Jun 2026 14:55:34 -0700 Subject: [PATCH 3/3] fix: address PR review feedback Chocolatey.ps1: - Fix synopsis grammar and replace stale Chocolatey.org references with the Chocolatey community repository - Document the Dependency and ChocoInstallScriptUrl parameters - Compare "have latest" versions SemanticVersion-first so prerelease versions (e.g. 2.2.2-beta) do not fall through to a reinstall FileSystem.ps1: - Report the specific missing $Source in the error, not the whole $Sources collection - Wire the documented Force switch: drop robocopy /XO so the target is overwritten even where its copy is newer Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --- PSDepend/PSDependScripts/Chocolatey.ps1 | 33 ++++++++++++++++++++----- PSDepend/PSDependScripts/FileSystem.ps1 | 9 ++++--- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/PSDepend/PSDependScripts/Chocolatey.ps1 b/PSDepend/PSDependScripts/Chocolatey.ps1 index 235e49f..633667c 100644 --- a/PSDepend/PSDependScripts/Chocolatey.ps1 +++ b/PSDepend/PSDependScripts/Chocolatey.ps1 @@ -1,18 +1,25 @@ <# .SYNOPSIS - Installs a Chocolatey package a repository. + Installs a package from a Chocolatey repository. .DESCRIPTION - Installs a package from a Chocolatey repository like Chocolatey.org. + Installs a package from a Chocolatey repository like the Chocolatey community repository. Relevant Dependency metadata: Name: The name of the package Version: Used to identify existing installs meeting this criteria. Defaults to 'latest' Source: Source Uri. Defaults to https://community.chocolatey.org/api/v2/ + .PARAMETER Dependency + Dependency to process + .PARAMETER Force If specified and the package is already installed, force the install again. + .PARAMETER ChocoInstallScriptUrl + Url to the script used to bootstrap Chocolatey when choco.exe is not found. + Defaults to https://community.chocolatey.org/install.ps1 + .PARAMETER PSDependAction Test, or Install the package. Defaults to Install @@ -27,7 +34,7 @@ } } - # Install version 2.0.2 of git via Chocolatey.org + # Install version 2.0.2 of git from the Chocolatey community repository .EXAMPLE @{ @@ -54,7 +61,7 @@ 'putty' = 'latest' } - # Installs the list of Chocolatey packages from Chocolatey.org using the Global PSDependOptions to limit repetition. + # Installs the list of Chocolatey packages from the Chocolatey community repository using the Global PSDependOptions to limit repetition. #> [CmdletBinding()] @@ -304,9 +311,23 @@ else { # If the version in the remote repository is less than or equal to the version installed, then we have the latest already [System.Version]$parsedRepositoryVersion = $null [System.Version]$parsedExistingVersion = $null -$haveLatest = [System.Version]::TryParse($repositoryVersion, [ref]$parsedRepositoryVersion) -and - [System.Version]::TryParse($existingVersion, [ref]$parsedExistingVersion) -and +[System.Management.Automation.SemanticVersion]$parsedRepositorySemanticVersion = $null +[System.Management.Automation.SemanticVersion]$parsedExistingSemanticVersion = $null +$haveLatest = if ( + [System.Management.Automation.SemanticVersion]::TryParse([string]$repositoryVersion, [ref]$parsedRepositorySemanticVersion) -and + [System.Management.Automation.SemanticVersion]::TryParse([string]$existingVersion, [ref]$parsedExistingSemanticVersion) +) { + $parsedRepositorySemanticVersion -le $parsedExistingSemanticVersion +} +elseif ( + [System.Version]::TryParse([string]$repositoryVersion, [ref]$parsedRepositoryVersion) -and + [System.Version]::TryParse([string]$existingVersion, [ref]$parsedExistingVersion) +) { $parsedRepositoryVersion -le $parsedExistingVersion +} +else { + $false +} if ($Version -eq 'latest' -and $haveLatest) { Write-Verbose "You have the latest version of [$Name], with installed version [$existingVersion] and Source version [$repositoryVersion]" if ($PSDependAction -contains 'Test') { diff --git a/PSDepend/PSDependScripts/FileSystem.ps1 b/PSDepend/PSDependScripts/FileSystem.ps1 index 0992bca..68271e1 100644 --- a/PSDepend/PSDependScripts/FileSystem.ps1 +++ b/PSDepend/PSDependScripts/FileSystem.ps1 @@ -85,7 +85,7 @@ $TestOutput = @() foreach ($Source in @($Sources)) { if (-not (Test-Path $Source)) { if ($PSDependAction -notcontains 'Test') { - Write-Error "Skipping $DependencyName, could not find source [$Sources]" + Write-Error "Skipping $DependencyName, could not find source [$Source]" } continue } @@ -108,8 +108,11 @@ foreach ($Source in @($Sources)) { } if ($PSDependAction -contains 'Install') { # TODO: Add non Windows equivalent... - [string[]]$Arguments = "/XO" - $Arguments += "/E" + [string[]]$Arguments = "/E" + if (-not ($Dependency.Parameters.Force -eq $True -or $Force)) { + # Without -Force, skip files where the target copy is newer + $Arguments += "/XO" + } if ($Dependency.Parameters.Mirror -eq $True -or $Mirror) { $Arguments += "/PURGE" }