Skip to content

DSC v3 resource for PSResourceGet#1852

Open
adityapatwardhan wants to merge 25 commits intomasterfrom
DscResource
Open

DSC v3 resource for PSResourceGet#1852
adityapatwardhan wants to merge 25 commits intomasterfrom
DscResource

Conversation

@adityapatwardhan
Copy link
Member

PR Summary

The initial implementation of DSC v3 resource for PSResourceGet. It include two resources, Repository and PSResources.

PR Context

PR Checklist

@adityapatwardhan adityapatwardhan changed the title DSC v3 resource for PSResourceGet WIP: DSC v3 resource for PSResourceGet Jul 28, 2025
@adityapatwardhan adityapatwardhan marked this pull request as draft July 28, 2025 20:56
Copy link

@michaeltlombardi michaeltlombardi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick review pass on the manifests, looking at resource implementation next.

Copy link

@michaeltlombardi michaeltlombardi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few more comments on the schema and implementation.

@ThomasNieto
Copy link
Contributor

Is the plan to add a discover extension in DSC to be able to find these resource manifests? If so, is there an issue/pr for that?

"type": "Microsoft.PowerShell.PSResourceGet/PSResources",
"version": "0.0.1",
"get": {
"executable": "pwsh",
Copy link
Contributor

@ThomasNieto ThomasNieto Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is Windows PowerShell handled? PSResourceGet supports both. For PSScript DSC v3 resource there are actually two different resources to account for different versions of PowerShell.

"type": "Microsoft.PowerShell.PSResourceGet/Repository",
"version": "0.0.1",
"get": {
"executable": "pwsh",
Copy link
Contributor

@ThomasNieto ThomasNieto Jul 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is Windows PowerShell handled? PSResourceGet supports both. For PSScript DSC v3 resource there are actually two different resources to account for different versions of PowerShell.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's a need, we could have a Microsoft.WindowsPowerShell.PSResourceGet type

@michaeltlombardi
Copy link

Is the plan to add a discover extension in DSC to be able to find these resource manifests? If so, is there an issue/pr for that?

@ThomasNieto yup! PowerShell/DSC#913 proposes a discovery extension for finding resource and extension manifests through the PSModulePath.

@ThomasNieto
Copy link
Contributor

I logged PowerShell/DSC#1024 issue. PSResourceGet will also have the same issue since it uses NuGet version range syntax containing square brackets.

@Gijsreyn
Copy link

Gijsreyn commented Nov 8, 2025

@adityapatwardhan - I think you've followed the news, but you should be able to define a single resource manifest now e.g.,:

// psresourceget.dsc.manifests.json
{
  "resources": [
    {
      "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
      "description": "Manage PowerShell resources using PSResourceGet.",
      "tags": [
        "linux",
        "windows",
        "macos",
        "powershell",
        "nuget"
      ],
      "type": "Microsoft.PowerShell.PSResourceGet/PSResourceList",
      "version": "0.0.1",
      "get": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation 'get'"
        ],
        "input": "stdin"
      },
      "set": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation set"
        ],
        "input": "stdin",
        "return": "stateAndDiff"
      },
      "export": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "./psresourceget.ps1 -resourcetype 'psresourcelist' -operation export"
        ],
        "input": "stdin"
      },
      "test": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "$Input | ./psresourceget.ps1 -resourcetype 'psresourcelist' -operation test"
        ],
        "input": "stdin",
        "return": "stateAndDiff"
      },
      "schema": {
        "embedded": {
          "$schema": "https://json-schema.org/draft/2020-12/schema",
          "title": "PSResourceList",
          "type": "object",
          "additionalProperties": false,
          "required": [
            "repositoryName"
          ],
          "properties": {
            "repositoryName": {
              "title": "Repository Name",
              "description": "The name of the repository from where the resources are acquired.",
              "type": "string"
            },
            "resources": {
              "title": "Resources",
              "description": "The list of resources to manage.",
              "type": "array",
              "items": {
                "$ref": "#/$defs/PSResource"
              },
              "minItems": 0
            },
            "_exist": {
              "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json"
            },
            "_inDesiredState": {
              "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json"
            }
          },
          "$defs": {
            "Scope": {
              "type": "string",
              "title": "Scope",
              "description": "Scope of the resource installation.",
              "enum": [
                "CurrentUser",
                "AllUsers"
              ]
            },
            "PSResource": {
              "type": "object",
              "additionalProperties": false,
              "required": [
                "name"
              ],
              "properties": {
                "name": {
                  "title": "Name",
                  "description": "The name of the resource.",
                  "type": "string"
                },
                "version": {
                  "title": "Version",
                  "description": "The version range of the resource.",
                  "type": "string",
                  "pattern": "^(\\[|\\()\\s*\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?\\s*(,\\s*(\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?)?\\s*(\\]|\\)))?$"
                },
                "scope": {
                  "title": "Scope",
                  "description": "The scope of the resource. Can be 'CurrentUser' or 'AllUsers'.",
                  "$ref": "#/$defs/Scope"
                },
                "repositoryName": {
                  "title": "Repository Name",
                  "description": "The name of the repository from where the resource is acquired.",
                  "type": "string"
                },
                "preRelease": {
                  "title": "Pre-Release version",
                  "description": "Indicates whether to include pre-release versions of the resource.",
                  "type": "boolean",
                  "default": false
                },
                "_exist": {
                  "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json"
                },
                "_inDesiredState": {
                  "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json"
                }
              }
            },
            "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json",
              "title": "Instance should exist",
              "description": "Indicates whether the DSC resource instance should exist.",
              "type": "boolean",
              "default": true,
              "enum": [
                false,
                true
              ]
            },
            "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json",
              "title": "Instance is in desired state",
              "description": "Indicates whether the DSC resource instance is in the desired state.",
              "type": "boolean",
              "default": true,
              "enum": [
                false,
                true
              ]
            }
          }
        }
      }
    },
    {
      "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json",
      "description": "Manage PowerShell repositories using PSResourceGet.",
      "tags": [
        "linux",
        "windows",
        "macos",
        "powershell",
        "nuget"
      ],
      "type": "Microsoft.PowerShell.PSResourceGet/Repository",
      "version": "0.0.1",
      "get": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation 'get'"
        ],
        "input": "stdin"
      },
      "set": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation set"
        ],
        "input": "stdin"
      },
      "delete": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "$Input | ./psresourceget.ps1 -resourcetype 'repository' -operation delete"
        ],
        "input": "stdin"
      },
      "export": {
        "executable": "pwsh",
        "args": [
          "-NoLogo",
          "-NonInteractive",
          "-NoProfile",
          "-ExecutionPolicy",
          "Bypass",
          "-Command",
          "./psresourceget.ps1 -resourcetype 'repository' -operation export"
        ],
        "input": "stdin"
      },
      "schema": {
        "embedded": {
          "$schema": "https://json-schema.org/draft/2020-12/schema",
          "title": "Repository",
          "description": "A PowerShell Resource repository from where to acquire the resources.",
          "type": "object",
          "additionalProperties": false,
          "allOf": [
            {
              "if": {
                "properties": {
                  "_exist": {
                    "const": false
                  }
                }
              },
              "then": {
                "required": [
                  "name"
                ]
              },
              "else": {
                "required": [
                  "name",
                  "uri"
                ]
              }
            }
          ],
          "properties": {
            "name": {
              "title": "Name",
              "description": "The name of the repository.",
              "type": "string"
            },
            "uri": {
              "title": "URI",
              "description": "The URI of the repository.",
              "type": "string",
              "format": "uri"
            },
            "trusted": {
              "title": "Trusted",
              "description": "Indicates whether the repository is trusted.",
              "type": "boolean"
            },
            "priority": {
              "title": "Priority",
              "description": "The priority of the repository. Lower numbers indicate higher priority.",
              "type": "integer",
              "minimum": 0,
              "maximum": 100
            },
            "repositoryType": {
              "title": "Repository Type",
              "description": "The type of the repository.",
              "$ref": "#/$defs/RepositoryType"
            },
            "_exist": {
              "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json"
            }
          },
          "$defs": {
            "RepositoryType": {
              "type": "string",
              "title": "Repository Type",
              "description": "The type of the repository. Can be 'Unknown', 'V2', 'V3', 'Local', 'NugetServer', or 'ContainerRegistry'.",
              "enum": [
                "Unknown",
                "V2",
                "V3",
                "Local",
                "NugetServer",
                "ContainerRegistry"
              ]
            },
            "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json": {
              "$schema": "https://json-schema.org/draft/2020-12/schema",
              "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json",
              "title": "Instance should exist",
              "description": "Indicates whether the DSC resource instance should exist.",
              "type": "boolean",
              "default": true,
              "enum": [
                false,
                true
              ]
            }
          }
        }
      }
    }
  ]
}

@Gijsreyn
Copy link

Gijsreyn commented Nov 8, 2025

I think the pattern for version should also slightly change to:

"pattern": "^((\\[|\\()[ \\t]*(>=|>|<=|<)?[ \\t]*\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?[ \\t]*(,[ \\t]*(>=|>|<=|<)?[ \\t]*(\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?)?[ \\t]*)?(\\]|\\))|\\d+(\\.\\d+){0,2}(-[0-9A-Za-z-.]+)?)$"

@adityapatwardhan
Copy link
Member Author

@Gijsreyn - thanks for your comments. I would still maybe want to keep it separate files for two resources. Easier to understand. What do others think?

Also, I removed the pattern validation for the NuGet version ranges. It can get really unwieldy and difficult to maintain.

@adityapatwardhan
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@adityapatwardhan adityapatwardhan marked this pull request as ready for review March 2, 2026 17:54
@adityapatwardhan adityapatwardhan changed the title WIP: DSC v3 resource for PSResourceGet DSC v3 resource for PSResourceGet Mar 2, 2026
@SteveL-MSFT SteveL-MSFT requested a review from Copilot March 2, 2026 21:39
@adityapatwardhan
Copy link
Member Author

Close / Re-Open to re-kick off pipelines.

@adityapatwardhan
Copy link
Member Author

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

_cmdletPassedIn);
}

_cmdletPassedIn.WriteVerbose("Exiting ContainerRegistryServerAPICalls::GetHttpResponseJObjectUsingDefaultHeaders() with NULL");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to keep this? If so, I would suggest changing to a debug message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a rebasing issue. This should not part of this PR.

Comment on lines +138 to +144
[string] ToJson() {
$resourceJson = if ($this.resources) { ($this.resources | ForEach-Object { $_.ToJson() }) -join ',' } else { '' }
$resourceJson = "[$resourceJson]"
$jsonString = "{'repositoryName': '$($this.repositoryName)','resources': $resourceJson}"
$jsonString = $jsonString -replace "'", '"'
return $jsonString | ConvertFrom-Json | ConvertTo-Json -Compress
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be better to have everything kept as a PSObject or HashTable and just use ConvertTo-Json -Compress once?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes in much easier in the implementation of other operation if it is a strongly type object.

"type": "Microsoft.PowerShell.PSResourceGet/Repository",
"version": "0.0.1",
"get": {
"executable": "pwsh",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's a need, we could have a Microsoft.WindowsPowerShell.PSResourceGet type

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds an initial DSC v3 integration for PSResourceGet by introducing two DSC resources (Repository and PSResourceList), along with test configurations and Pester coverage to exercise schema and basic end-to-end scenarios.

Changes:

  • Added DSC v3 resource manifests for Microsoft.PowerShell.PSResourceGet/Repository and Microsoft.PowerShell.PSResourceGet/PSResourceList.
  • Added the PowerShell implementation script (src/dsc/psresourceget.ps1) backing the DSC resource operations.
  • Added Pester tests and DSC YAML configs for schema validation, E2E behavior, and error-code scenarios; updated build packaging to include the DSC assets.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
test/DscResource/configs/repository.unregister.dsc.yaml Adds DSC config to ensure a repository is absent.
test/DscResource/configs/repository.register.dsc.yaml Adds DSC config to register a repository with expected properties.
test/DscResource/configs/repository.get.dsc.yaml Adds DSC config to assert repository settings.
test/DscResource/configs/repository.export.dsc.yaml Adds DSC config used for repository export assertions.
test/DscResource/configs/psresourcegetlist.uninstall.dsc.yaml Adds DSC config to uninstall a PSResource via PSResourceList.
test/DscResource/configs/psresourcegetlist.prerelease.install.dsc.yaml Adds DSC config to install stable + prerelease variants.
test/DscResource/configs/psresourcegetlist.oneexisting.install.dsc.yaml Adds DSC config for mixed “already installed + missing” scenario.
test/DscResource/configs/psresourcegetlist.moddeps.install.dsc.yaml Adds DSC config for installing a module with dependencies.
test/DscResource/configs/psresourcegetlist.install.dsc.yaml Adds DSC config to install a single resource.
test/DscResource/configs/psresourcegetlist.export.dsc.yaml Adds DSC config used for PSResourceList export assertions.
test/DscResource/configs/psresourcegetlist.error.noresource.dsc.yaml Adds DSC config used to validate “resource not found” errors.
test/DscResource/configs/psresourcegetlist.error.norepo.dsc.yaml Adds DSC config used to validate “repo not found” errors.
test/DscResource/configs/psresourcegetlist.error.install.untrustedrepo.dsc.yaml Adds DSC config used to validate “repo not trusted” errors.
test/DscResource/PSResourceGetDSCResource.Tests.ps1 Adds schema, unit-style, and E2E Pester tests for the new resources.
src/dsc/repository.dsc.resource.json Adds the Repository DSC v3 resource manifest and embedded schema.
src/dsc/psresourcelist.dsc.resource.json Adds the PSResourceList DSC v3 resource manifest, schema, and exit code mapping.
src/dsc/psresourceget.ps1 Implements operations (get/set/test/export/delete) for the DSC resources.
src/code/ContainerRegistryServerAPICalls.cs Adds a verbose line before returning null from a helper method.
doBuild.ps1 Updates build packaging to copy DSC scripts/manifests into the build output.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +354 to +373
$ret = if ($err.FullyQualifiedErrorId -eq 'ErrorGettingSpecifiedRepo,Microsoft.PowerShell.PSResourceGet.Cmdlets.GetPSResourceRepository') {
Write-Trace -message "Repository not found: $($inputRepository.Name). Returning _exist = false"
[Repository]::new(
$InputRepository.Name,
$false
)
}
else {
[Repository]::new(
$rep.Name,
$rep.Uri,
$rep.Trusted,
$rep.Priority,
$rep.ApiVersion
)

Write-Trace -message "Returning repository object for: $($ret.Name)"
}

return ( $ret.ToJson() )
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Repository get path, the else branch of the $ret = if (...) { ... } else { ... } expression doesn't output the newly created Repository object as the branch value (the last statement is Write-Trace). This makes $ret end up $null, and return ($ret.ToJson()) will fail. Assign the new object to $ret (or make it the last expression) before calling Write-Trace.

Copilot uses AI. Check for mistakes.
Comment on lines +515 to +547
$currentState = GetPSResourceList -inputObj $inputObj

$inputObj.resources | ForEach-Object {
$resourceDesiredState = ConvertInputToPSResource -inputObj $_ -repositoryName $repositoryName
$name = $resourceDesiredState.name
$version = $resourceDesiredState.version
$scope = if ($resourceDesiredState.scope) { $resourceDesiredState.scope } else { "CurrentUser" }

# Resource should not exist - uninstall if it does
$currentState.resources | ForEach-Object {

$isInDesiredState = $_.IsInDesiredState($resourceDesiredState)

# Uninstall if resource should not exist but does
if (-not $resourceDesiredState._exist -and $_._exist) {
Write-Trace -message "Resource $($resourceDesiredState.name) exists but _exist is false. Adding to uninstall list." -level info
$resourcesToUninstall += $_
}
# Install if resource should exist but doesn't, or exists but not in desired state
elseif ($resourceDesiredState._exist -and (-not $_._exist -or -not $isInDesiredState)) {
Write-Trace -message "Resource $($resourceDesiredState.name) needs to be installed." -level info
$versionStr = if ($version) { $resourceDesiredState.version } else { 'latest' }
$key = $name.ToLowerInvariant() + '-' + $versionStr.ToLowerInvariant()
if (-not $resourcesToInstall.ContainsKey($key)) {
$resourcesToInstall[$key] = $resourceDesiredState
}
}
# Otherwise resource is in desired state, no action needed
else {
Write-Trace -message "Resource $($resourceDesiredState.name) is in desired state." -level info
}
}
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetPSResourceList only iterates over $currentState.resources when deciding what to install. But GetPSResourceList returns only the resources that exist when at least one match is found (it omits placeholders for missing desired resources). In that situation, a desired resource with a different name that isn't currently installed will never be added to $resourcesToInstall because there is no corresponding entry in $currentState.resources to compare against. Consider building a lookup by name and explicitly handling the 'not found in current state' case as needing install.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +14
version: '[[0.0.93,)'
_exist: true
preRelease: false
- name: testmodule99
version: '[[100.0.99,)'
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version range strings here start with [[ (e.g. [[0.0.93,)), which is not valid NuGet version-range syntax and will fail parsing (VersionRange.Parse expects forms like [0.0.93,)). Update these version ranges to valid NuGet range notation so the 'one existing, one missing' scenario behaves as intended.

Suggested change
version: '[[0.0.93,)'
_exist: true
preRelease: false
- name: testmodule99
version: '[[100.0.99,)'
version: '[0.0.93,)'
_exist: true
preRelease: false
- name: testmodule99
version: '[100.0.99,)'

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,7 @@
$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json
resources:
- name: register PSGallery
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This config is intended to unregister the repository (_exist: false), but the resource instance name is "register PSGallery". Renaming it to reflect the action (e.g., "unregister …") will make test output/logs easier to interpret.

Suggested change
- name: register PSGallery
- name: unregister PSGallery

Copilot uses AI. Check for mistakes.
Comment on lines +632 to +639
if ($null -eq $rep) {
Register-PSResourceRepository @splatt
}
else {
if ($inputObj._exist -eq $false) {
Write-Trace -message "Repository $($inputObj.Name) exists and _exist is false. Deleting it." -level info
Unregister-PSResourceRepository -Name $inputObj.Name
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repository SetOperation registers the repository whenever $rep is $null, even if the desired state is _exist = false. For an 'ensure absent' request, this should be a no-op (or at most verify it's absent), not an attempted register (which will also fail if uri isn't provided, as in the unregister config). Add a branch that skips registration when _exist is explicitly false.

Copilot uses AI. Check for mistakes.
Comment on lines +404 to +413
if (-not $repositoryState) {
Write-Trace -message "Repository not found: $($inputObj.repositoryName). Returning PSResourceList with _inDesiredState = false." -level info
$retValue = [PSResourceList]::new($inputObj.repositoryName, $inputResources, $false)
$retValue._inDesiredState = $false
$retValue.ToJsonForTest()
'["repositoryName", "resources"]'
}

$inputPSResourceList = [PSResourceList]::new($inputObj.repositoryName, $inputResources, $repositoryState.Trusted)

Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TestPSResourceList doesn't return after handling the 'repository not found' case. After emitting output, it continues and then dereferences $repositoryState.Trusted even though $repositoryState is $null, which will throw. Add an explicit return (or exit) after writing the state/diff for this branch.

Copilot uses AI. Check for mistakes.
Comment on lines +517 to +553
$inputObj.resources | ForEach-Object {
$resourceDesiredState = ConvertInputToPSResource -inputObj $_ -repositoryName $repositoryName
$name = $resourceDesiredState.name
$version = $resourceDesiredState.version
$scope = if ($resourceDesiredState.scope) { $resourceDesiredState.scope } else { "CurrentUser" }

# Resource should not exist - uninstall if it does
$currentState.resources | ForEach-Object {

$isInDesiredState = $_.IsInDesiredState($resourceDesiredState)

# Uninstall if resource should not exist but does
if (-not $resourceDesiredState._exist -and $_._exist) {
Write-Trace -message "Resource $($resourceDesiredState.name) exists but _exist is false. Adding to uninstall list." -level info
$resourcesToUninstall += $_
}
# Install if resource should exist but doesn't, or exists but not in desired state
elseif ($resourceDesiredState._exist -and (-not $_._exist -or -not $isInDesiredState)) {
Write-Trace -message "Resource $($resourceDesiredState.name) needs to be installed." -level info
$versionStr = if ($version) { $resourceDesiredState.version } else { 'latest' }
$key = $name.ToLowerInvariant() + '-' + $versionStr.ToLowerInvariant()
if (-not $resourcesToInstall.ContainsKey($key)) {
$resourcesToInstall[$key] = $resourceDesiredState
}
}
# Otherwise resource is in desired state, no action needed
else {
Write-Trace -message "Resource $($resourceDesiredState.name) is in desired state." -level info
}
}
}

if ($resourcesToUninstall.Count -gt 0) {
Write-Trace -message "Uninstalling resources: $($resourcesToUninstall | ForEach-Object { "$($_.Name) - $($_.Version)" })"
$resourcesToUninstall | ForEach-Object {
Uninstall-PSResource -Name $_.Name -Scope $scope -ErrorAction Stop
}
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetPSResourceList computes $scope per desired resource, but later uses that single (last-assigned) $scope for all uninstall and install operations. This can uninstall/install into the wrong scope when the input contains mixed scopes. Use the scope from the specific resource being installed/uninstalled (e.g., store it on the queued item and pass it through).

Copilot uses AI. Check for mistakes.
Comment on lines +724 to +731
$resources += $resourcesExist | ForEach-Object {
[PSResource]::new(
$_.Name,
$_.Version.PreRelease ? $_.Version.ToString() + "-" + $_.PreRelease : $_.Version.ToString(),
$_.Scope,
$_.RepositoryName,
$_.PreRelease ? $true : $false
)
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PopulatePSResourceListObjectByRepository treats $_ .Version as if it has a .PreRelease property ($_.Version.PreRelease), but in this code path Version is a string (see PSResource definition and how $allPSResources is populated). This will throw at runtime. Build the version string without accessing .PreRelease on it, or store version/prerelease as separate fields.

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +14
version: '[[0.0.93,)'
_exist: true
preRelease: false
- name: testmodule99
version: '[[100.0.99,)'
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version range strings here start with [[ (e.g. [[0.0.93,)), which is not valid NuGet version-range syntax and will fail parsing (VersionRange.Parse expects forms like [0.0.93,)). Update these version ranges to valid NuGet range notation so prerelease/stable selection works.

Suggested change
version: '[[0.0.93,)'
_exist: true
preRelease: false
- name: testmodule99
version: '[[100.0.99,)'
version: '[0.0.93,)'
_exist: true
preRelease: false
- name: testmodule99
version: '[100.0.99,)'

Copilot uses AI. Check for mistakes.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to escape the [


if ($found) {
Write-Trace -message "Resource match found for: $($otherResource.name)" -level trace
break
Copy link

Copilot AI Mar 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PSResourceList.IsInDesiredState breaks out of the outer loop on the first matching resource, so it can return true even when later desired resources do not match. Replace the break in the $found branch with continue (or remove it) so every desired resource is validated.

Suggested change
break
continue

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants