-
Notifications
You must be signed in to change notification settings - Fork 8.3k
[release/v7.6] DSC v3 resource for Powershell Profile #26447
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -45,6 +45,48 @@ runs: | |||||
| with: | ||||||
| global-json-file: ./global.json | ||||||
|
|
||||||
| - name: Set Package Name by Platform | ||||||
| id: set_package_name | ||||||
| shell: pwsh | ||||||
| run: |- | ||||||
| Import-Module ./.github/workflows/GHWorkflowHelper/GHWorkflowHelper.psm1 | ||||||
| $platform = $env:RUNNER_OS | ||||||
| Write-Host "Runner platform: $platform" | ||||||
| if ($platform -eq 'Linux') { | ||||||
| $packageName = 'DSC-*-x86_64-linux.tar.gz' | ||||||
| } elseif ($platform -eq 'macOS') { | ||||||
| $packageName = 'DSC-*-x86_64-apple-darwin.tar.gz' | ||||||
| } else { | ||||||
| throw "Unsupported platform: $platform" | ||||||
| } | ||||||
|
|
||||||
| Set-GWVariable -Name "DSC_PACKAGE_NAME" -Value $packageName | ||||||
|
|
||||||
| - name: Get Latest DSC Package Version | ||||||
| shell: pwsh | ||||||
| run: |- | ||||||
| Import-Module ./.github/workflows/GHWorkflowHelper/GHWorkflowHelper.psm1 | ||||||
| $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" | ||||||
| $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 | ||||||
| $latestVersion = $latestRelease.tag_name.TrimStart("v") | ||||||
| Write-Host "Latest DSC Version: $latestVersion" | ||||||
|
|
||||||
| $packageName = "$env:DSC_PACKAGE_NAME" | ||||||
|
|
||||||
| Write-Host "Package Name: $packageName" | ||||||
|
|
||||||
| $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "*$packageName*" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url | ||||||
|
TravisEz13 marked this conversation as resolved.
|
||||||
| $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "*$packageName*" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url | |
| $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like $packageName } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url |
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -45,6 +45,26 @@ runs: | |||||||||||
| with: | ||||||||||||
| global-json-file: .\global.json | ||||||||||||
|
|
||||||||||||
| - name: Get Latest DSC Package Version | ||||||||||||
| shell: pwsh | ||||||||||||
| run: |- | ||||||||||||
| Import-Module .\.github\workflows\GHWorkflowHelper\GHWorkflowHelper.psm1 | ||||||||||||
| $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" | ||||||||||||
| $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 | ||||||||||||
|
||||||||||||
| $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 | |
| $latestRelease = $releases | Where-Object { $v = $_.name.trim("v"); $semVer = [System.Management.Automation.SemanticVersion]::new($v); if ($semVer.Major -eq 3 -and $semVer.Minor -ge 2) { $_ } } | Select-Object -First 1 | |
| if (-not $latestRelease) { | |
| throw "No DSC v3.2+ release found" | |
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If no asset matches the filter pattern, $downloadUrl will be $null, causing Invoke-RestMethod to fail with an unclear error. Add validation to handle this case.
Add error handling:
$downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "DSC-*-x86_64-pc-windows-msvc.zip" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url
if (-not $downloadUrl) {
throw "No matching DSC package found for Windows x86_64"
}
Write-Host "Download URL: $downloadUrl"This provides a clear error message if the expected package isn't found in the release.
| $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "DSC-*-x86_64-pc-windows-msvc.zip" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url | |
| $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "DSC-*-x86_64-pc-windows-msvc.zip" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url | |
| if (-not $downloadUrl) { | |
| throw "No matching DSC package found for Windows x86_64" | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| { | ||
| "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", | ||
| "description": "Manage PowerShell profiles.", | ||
| "tags": [ | ||
| "Linux", | ||
| "Windows", | ||
| "macOS", | ||
| "PowerShell" | ||
| ], | ||
| "type": "Microsoft.PowerShell/Profile", | ||
| "version": "0.1.0", | ||
| "get": { | ||
| "executable": "pwsh", | ||
| "args": [ | ||
| "-NoLogo", | ||
| "-NonInteractive", | ||
| "-NoProfile", | ||
| "-ExecutionPolicy", | ||
| "Bypass", | ||
| "-File", | ||
| "./pwsh.profile.resource.ps1", | ||
| "-operation", | ||
| "get" | ||
| ], | ||
| "input": "stdin" | ||
| }, | ||
| "set": { | ||
| "executable": "pwsh", | ||
| "args": [ | ||
| "-NoLogo", | ||
| "-NonInteractive", | ||
| "-NoProfile", | ||
| "-ExecutionPolicy", | ||
| "Bypass", | ||
| "-File", | ||
| "./pwsh.profile.resource.ps1", | ||
| "-operation", | ||
| "set" | ||
| ], | ||
| "input": "stdin" | ||
| }, | ||
| "export": { | ||
| "executable": "pwsh", | ||
| "args": [ | ||
| "-NoLogo", | ||
| "-NonInteractive", | ||
| "-NoProfile", | ||
| "-ExecutionPolicy", | ||
| "Bypass", | ||
| "-File", | ||
| "./pwsh.profile.resource.ps1", | ||
| "-operation", | ||
| "export" | ||
| ], | ||
| "input": "stdin" | ||
| }, | ||
| "exitCodes": { | ||
| "0": "Success", | ||
| "1": "Error", | ||
| "2": "Input not supported for export operation" | ||
| }, | ||
| "schema": { | ||
| "embedded": { | ||
| "$schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "title": "Profile", | ||
| "description": "Manage PowerShell profiles.", | ||
| "type": "object", | ||
| "unevaluatedProperties": false, | ||
| "required": [ | ||
| "profileType" | ||
| ], | ||
| "properties": { | ||
| "profileType": { | ||
| "type": "string", | ||
| "title": "Profile Type", | ||
| "description": "Defines which profile to manage. Valid values are: 'AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserAllHosts', and 'CurrentUserCurrentHost'.", | ||
| "enum": [ | ||
| "AllUsersCurrentHost", | ||
| "AllUsersAllHosts", | ||
| "CurrentUserAllHosts", | ||
| "CurrentUserCurrentHost" | ||
| ] | ||
| }, | ||
| "profilePath": { | ||
| "title": "Profile Path", | ||
| "description": "The full path to the profile file.", | ||
| "type": "string", | ||
| "readOnly": true | ||
| }, | ||
| "content": { | ||
| "title": "Content", | ||
| "description": "Defines the content of the profile. If you don't specify this property, the resource doesn't manage the file contents. If you specify this property as an empty string, the resource removes all content from the file. If you specify this property as a non-empty string, the resource sets the file contents to the specified string. The resources retains newlines from this property without any modification.", | ||
| "type": "string" | ||
| }, | ||
| "_exist": { | ||
| "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json" | ||
| }, | ||
| "_name": { | ||
| "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/name.json" | ||
| } | ||
| }, | ||
| "$defs": { | ||
| "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/name.json": { | ||
| "$schema": "https://json-schema.org/draft/2020-12/schema", | ||
| "$id": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/name.json", | ||
| "title": "Exported instance name", | ||
| "description": "Returns a generated name for the resource instance from an export operation.", | ||
| "readOnly": true, | ||
| "type": "string" | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,179 @@ | ||||||||||||||||||||||||||||||
| ## Copyright (c) Microsoft Corporation. All rights reserved. | ||||||||||||||||||||||||||||||
| ## Licensed under the MIT License. | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [CmdletBinding()] | ||||||||||||||||||||||||||||||
| param( | ||||||||||||||||||||||||||||||
| [Parameter(Mandatory = $true)] | ||||||||||||||||||||||||||||||
| [ValidateSet('get', 'set', 'export')] | ||||||||||||||||||||||||||||||
| [string]$Operation, | ||||||||||||||||||||||||||||||
| [Parameter(ValueFromPipeline)] | ||||||||||||||||||||||||||||||
| [string[]]$UserInput | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| Begin { | ||||||||||||||||||||||||||||||
| enum ProfileType { | ||||||||||||||||||||||||||||||
| AllUsersCurrentHost | ||||||||||||||||||||||||||||||
| AllUsersAllHosts | ||||||||||||||||||||||||||||||
| CurrentUserAllHosts | ||||||||||||||||||||||||||||||
| CurrentUserCurrentHost | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| function New-PwshResource { | ||||||||||||||||||||||||||||||
| param( | ||||||||||||||||||||||||||||||
| [Parameter(Mandatory = $true)] | ||||||||||||||||||||||||||||||
| [ProfileType] $ProfileType, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [Parameter(ParameterSetName = 'WithContent')] | ||||||||||||||||||||||||||||||
| [string] $Content, | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| [Parameter(ParameterSetName = 'WithContent')] | ||||||||||||||||||||||||||||||
| [bool] $Exist | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Create the PSCustomObject with properties | ||||||||||||||||||||||||||||||
| $resource = [PSCustomObject]@{ | ||||||||||||||||||||||||||||||
| profileType = $ProfileType | ||||||||||||||||||||||||||||||
| content = $null | ||||||||||||||||||||||||||||||
| profilePath = GetProfilePath -profileType $ProfileType | ||||||||||||||||||||||||||||||
| _exist = $false | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Add ToJson method | ||||||||||||||||||||||||||||||
| $resource | Add-Member -MemberType ScriptMethod -Name 'ToJson' -Value { | ||||||||||||||||||||||||||||||
| return ([ordered] @{ | ||||||||||||||||||||||||||||||
| profileType = $this.profileType | ||||||||||||||||||||||||||||||
| content = $this.content | ||||||||||||||||||||||||||||||
| profilePath = $this.profilePath | ||||||||||||||||||||||||||||||
| _exist = $this._exist | ||||||||||||||||||||||||||||||
| }) | ConvertTo-Json -Compress -EnumsAsStrings | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Constructor logic - if Content and Exist parameters are provided (WithContent parameter set) | ||||||||||||||||||||||||||||||
| if ($PSCmdlet.ParameterSetName -eq 'WithContent') { | ||||||||||||||||||||||||||||||
| $resource.content = $Content | ||||||||||||||||||||||||||||||
| $resource._exist = $Exist | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| # Default constructor logic - read from file system | ||||||||||||||||||||||||||||||
| $fileExists = Test-Path $resource.profilePath | ||||||||||||||||||||||||||||||
| if ($fileExists) { | ||||||||||||||||||||||||||||||
| $resource.content = Get-Content -Path $resource.profilePath | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| $resource.content = Get-Content -Path $resource.profilePath | |
| $resource.content = Get-Content -Path $resource.profilePath -Raw |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Get-Content call returns an array of strings (one per line) by default, but should return the entire content as a single string to match the Set-Content behavior in line 147 and test expectations.
Add the -Raw parameter:
$content = Get-Content -Path $profilePath -RawWithout -Raw, this returns an array which could cause mismatches when comparing with expected string values.
| $content = Get-Content -Path $profilePath | |
| $actualState.Content = $content | |
| } elseif ($InputResource._exist -and -not $exists) { | |
| $actualState.Content = $null | |
| $actualState._exist = $false | |
| } elseif (-not $InputResource._exist -and $exists) { | |
| $actualState.Content = Get-Content -Path $profilePath | |
| $content = Get-Content -Path $profilePath -Raw | |
| $actualState.Content = $content | |
| } elseif ($InputResource._exist -and -not $exists) { | |
| $actualState.Content = $null | |
| $actualState._exist = $false | |
| } elseif (-not $InputResource._exist -and $exists) { | |
| $actualState.Content = Get-Content -Path $profilePath -Raw |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The 'set' operation should return the resource state after making changes. While DSC v3 can call 'get' automatically after 'set' if no output is provided, returning the state directly is more efficient and provides immediate feedback.
Update the 'set' case to return the final state:
'set' {
SetOperation -InputResource $InputResource
GetOperation -InputResource $InputResource -AsJson
}This aligns with DSC v3 best practices and ensures the test expectations in lines 78-80 are properly supported without requiring an additional 'get' operation.
| SetOperation -InputResource $InputResource | |
| SetOperation -InputResource $InputResource | |
| GetOperation -InputResource $InputResource -AsJson |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If no releases match the filter criteria (Major = 3, Minor >= 2),
$latestReleasewill be$null, causing subsequent property accesses to fail. Add validation to handle this case gracefully.Add error handling:
This prevents cryptic errors when the expected DSC version isn't available.