From b0833190e743c612fefed549b5eba175eeeb8824 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 3 Sep 2025 10:35:32 -0700 Subject: [PATCH 01/20] Initial PS Profile resource --- dsc/pwsh.profile.dsc.resource.json | 103 ++++++++++++ dsc/pwsh.profile.resource.ps1 | 149 ++++++++++++++++++ .../powershell-win-core.csproj | 4 + 3 files changed, 256 insertions(+) create mode 100644 dsc/pwsh.profile.dsc.resource.json create mode 100644 dsc/pwsh.profile.resource.ps1 diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json new file mode 100644 index 00000000000..33f2f0c4c96 --- /dev/null +++ b/dsc/pwsh.profile.dsc.resource.json @@ -0,0 +1,103 @@ +{ + "$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.0.1", + "get": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./pwsh.profile.resource.ps1 -operation 'get'" + ], + "input": "stdin" + }, + "set": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./pwsh.profile.resource.ps1 -operation 'set'" + ], + "input": "stdin" + }, + "export": { + "executable": "pwsh", + "args": [ + "-NoLogo", + "-NonInteractive", + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-Command", + "$Input | ./pwsh.profile.resource.ps1 -operation 'export'" + ], + "input": "stdin" + }, + "schema": { + "embedded": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Profile", + "description": "A resource for managing PowerShell profiles.", + "type": "object", + "additionalProperties": false, + "required": [ + "profileType" + ], + "properties": { + "profileType": { + "title": "Name", + "description": "The name of the repository.", + "type": "string" + }, + "content": { + "title": "Content", + "description": "The content of the profile.", + "type": "string" + }, + "_exist": { + "$ref": "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json" + } + }, + "$defs": { + "ProfileType": { + "type": "string", + "title": "Profile Type", + "description": "The type of the profile. Can be 'AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserAllHosts', or 'CurrentUserCurrentHost'.", + "enum": [ + "AllUsersCurrentHost", + "AllUsersAllHosts", + "CurrentUserAllHosts", + "CurrentUserCurrentHost" + ] + }, + "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 + ] + } + } + } + } +} diff --git a/dsc/pwsh.profile.resource.ps1 b/dsc/pwsh.profile.resource.ps1 new file mode 100644 index 00000000000..d935e14504c --- /dev/null +++ b/dsc/pwsh.profile.resource.ps1 @@ -0,0 +1,149 @@ +## 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)] + $stdinput +) + +enum ProfileType { + AllUsersCurrentHost + AllUsersAllHosts + CurrentUserAllHosts + CurrentUserCurrentHost +} + +class PwshResource { + [ProfileType] $profileType + [string] $content + [bool] $_exist + + [string] ToJson() { + return ([ordered] @{ + profileType = $this.profileType + content = $this.content + }) | ConvertTo-Json -Compress -EnumsAsStrings + } +} + +function PopulatePwshResource { + param ( + [ProfileType] $profileType + ) + + $profilePath = GetProfilePath -profileType $profileType + $fileExists = Test-Path $profilePath + + $resource = [PwshResource]::new() + $resource.profileType = $profileType + $resource.content = $fileExists ? (Get-Content -Path $profilePath -Raw) : $null + $resource._exist = $fileExists + + return $resource +} + +function GetProfilePath { + param ( + [ProfileType] $profileType + ) + + $path = switch ($profileType) { + 'AllUsersCurrentHost' { $PROFILE.AllUsersCurrentHost} + 'AllUsersAllHosts' { $PROFILE.AllUsersAllHosts} + 'CurrentUserAllHosts' { $PROFILE.CurrentUserAllHosts} + 'CurrentUserCurrentHost' { $PROFILE.CurrentUserCurrentHost} + } + + return $path +} + +function ExportOperation { + $allUserCurrentHost = PopulatePwshResource -profileType 'AllUsersCurrentHost' + $allUsersAllHost = PopulatePwshResource -profileType 'AllUsersAllHosts' + $currentUserAllHost = PopulatePwshResource -profileType 'CurrentUserAllHosts' + $currentUserCurrentHost = PopulatePwshResource -profileType 'CurrentUserCurrentHost' + + # Cannot use the ToJson() method here as we are adding a note property + $allUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $allUsersAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $currentUserAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $currentUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings +} + +function GetOperation { + param ( + [PwshResource] $InputResource + ) + + $profilePath = GetProfilePath -profileType $InputResource.profileType.ToString() + + $exists = Test-Path $profilePath + + if ($InputResource._exist -and $exists) { + $content = Get-Content -Path $profilePath + $InputResource.Content = $content + } + elseif ($InputResource._exist -and -not $exists) { + $InputResource.Content = $null + $InputResource._exist = $false + } + elseif (-not $InputResource._exist -and $exists) { + $InputResource.Content = Get-Content -Path $profilePath + $InputResource._exist = $true + } + else { + $InputResource.Content = $null + $InputResource._exist = $false + } + + $InputResource.ToJson() +} + +function SetOperation { + param ( + [PwshResource] $InputResource + ) + + $profilePath = GetProfilePath -profileType $InputResource.profileType.ToString() + $profileExists = Test-Path $profilePath + + if ($InputResource._exist) { + if ($InputResource.content) { + Set-Content -Path $profilePath -Value $InputResource.content + } + else { + Remove-Item -Path $profilePath -Force + } + } + elseif (-not $InputResource._exist -and $profileExists) { + Remove-Item -Path $profilePath -Force + } + elseif (-not $InputResource._exist -and -not $profileExists) { + # Do nothing + } +} + +$inputJson = $input | ConvertFrom-Json + +if ($inputJson) { + $InputResource = [PwshResource]::new() + $InputResource.profileType = $inputJson.profileType + $InputResource.content = $inputJson.content + $InputResource._exist = $inputJson._exist +} + +switch ($Operation) { + 'get' { + GetOperation -InputResource $InputResource + } + 'set' { + SetOperation -InputResource $InputResource + } + 'export' { + ExportOperation + } +} diff --git a/src/powershell-win-core/powershell-win-core.csproj b/src/powershell-win-core/powershell-win-core.csproj index 5368518dd3c..e6efeac10f0 100644 --- a/src/powershell-win-core/powershell-win-core.csproj +++ b/src/powershell-win-core/powershell-win-core.csproj @@ -29,6 +29,10 @@ PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + PreserveNewest PreserveNewest From c74f8bc9cfd53732e4e7480fa1afde15d8f9bae1 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Thu, 18 Sep 2025 15:58:46 -0700 Subject: [PATCH 02/20] dsc ci --- .vsts-ci/dsc-resource.yml | 178 ++++++++++++++++++ dsc/pwsh.profile.dsc.resource.json | 6 +- .../dsc/dsc.profileresource.Tests.ps1 | 146 ++++++++++++++ .../dsc/psprofile_alluser_allhost.dsc.yaml | 9 + .../psprofile_allusers_currenthost.dsc.yaml | 9 + .../psprofile_currentuser_allhosts.dsc.yaml | 9 + ...psprofile_currentuser_currenthost.dsc.yaml | 9 + test/powershell/dsc/psprofile_export.dsc.yaml | 9 + 8 files changed, 371 insertions(+), 4 deletions(-) create mode 100644 .vsts-ci/dsc-resource.yml create mode 100644 test/powershell/dsc/dsc.profileresource.Tests.ps1 create mode 100644 test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml create mode 100644 test/powershell/dsc/psprofile_export.dsc.yaml diff --git a/.vsts-ci/dsc-resource.yml b/.vsts-ci/dsc-resource.yml new file mode 100644 index 00000000000..e68120de131 --- /dev/null +++ b/.vsts-ci/dsc-resource.yml @@ -0,0 +1,178 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +name: PR-$(System.PullRequest.PullRequestNumber)-$(Date:yyyyMMdd)$(Rev:.rr) +trigger: + # Batch merge builds together while a merge build is running + batch: true + branches: + include: + - master + - release* + - feature* + paths: + include: + - 'src' +pr: + branches: + include: + - master + - release* + - feature* + paths: + include: + - 'dsc' + +parameters: + - name: dscVersion + type: string + default: 'v3.2.0-preview.4' + +variables: + GIT_CONFIG_PARAMETERS: "'core.autocrlf=false'" + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + POWERSHELL_TELEMETRY_OPTOUT: 1 + DOTNET_NOLOGO: 1 + __SuppressAnsiEscapeSequences: 1 + NugetSecurityAnalysisWarningLevel: none + nugetMultiFeedWarnLevel: none + +resources: +- repo: self + clean: true + +stages: +- stage: AcquireDsc + displayName: Install DSC + jobs: + - job: acquire_dsc + displayName: Acquire DSC Resources + pool: + vmImage: 'windows-latest' + + steps: + - pwsh: | + Get-ChildItem -Path env: + displayName: Capture Environment + condition: succeededOrFailed() + + - pwsh: | + # Download DSC from GitHub and install it locally for use in the build. + $dscVersion = '${{ parameters.dscVersion }}'.TrimStart('v') + Write-Verbose -Verbose "Downloading DSC version $dscVersion from GitHub..." + $dscZipUrl = https://github.com/PowerShell/DSC/releases/download/v$dscVersion/DSC-$dscVersion-x86_64-pc-windows-msvc.zip + Write-Verbose -Verbose "Downloading DSC from $dscZipUrl" + $destinationPath = "$(System.ArtifactsDirectory)\dsc\$dscVersion" + $zipFilePath = Join-Path -Path $destinationPath -ChildPath "DSC-$dscVersion.zip" + New-Item -ItemType Directory -Path $destinationPath -Force | Out-Null + Invoke-WebRequest -Uri $dscZipUrl -OutFile $zipFilePath -UseBasicParsing + Write-Verbose -Verbose "Extracting DSC to $destinationPath" + Expand-Archive -Path $zipFilePath -DestinationPath $destinationPath -Force + Remove-Item -Path $zipFilePath -Force + + Get-ChildItem -Path $destinationPath -Recurse | ForEach-Object { + Write-Verbose -Verbose "Extracted file: $($_.FullName)" + } + displayName: 'Download and Install DSC' + + + - pwsh: | + Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse + displayName: 'Capture Artifacts Directory' + continueOnError: true + + - pwsh: | + Import-Module .\tools\ci.psm1 + Invoke-CIInstall + displayName: Bootstrap + +- stage: TestWin + displayName: Test PSResourceGetACR + jobs: + - job: win_test_ACR + displayName: PSResourceGet ACR Tests + pool: + vmImage: 'windows-latest' + + steps: + - pwsh: | + Get-ChildItem -Path env: + displayName: Capture Environment + condition: succeededOrFailed() + + - task: DownloadBuildArtifacts@0 + displayName: 'Download Build Artifacts' + inputs: + downloadType: specific + itemPattern: | + build/**/* + downloadPath: '$(System.ArtifactsDirectory)' + + - pwsh: | + Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse + displayName: 'Capture Artifacts Directory' + continueOnError: true + + - pwsh: | + # Remove "Program Files\dotnet" from the env variable PATH, so old SDKs won't affect us. + Write-Host "Old Path:" + Write-Host $env:Path + + $dotnetPath = Join-Path $env:SystemDrive 'Program Files\dotnet' + $paths = $env:Path -split ";" | Where-Object { -not $_.StartsWith($dotnetPath) } + $env:Path = $paths -join ";" + + Write-Host "New Path:" + Write-Host $env:Path + + # Bootstrap + Import-Module .\tools\ci.psm1 + Invoke-CIInstall + displayName: Bootstrap + + - pwsh: | + Install-Module -Name 'Microsoft.PowerShell.SecretManagement' -force -SkipPublisherCheck -AllowClobber + Install-Module -Name 'Microsoft.PowerShell.SecretStore' -force -SkipPublisherCheck -AllowClobber + $vaultPassword = ConvertTo-SecureString $("a!!"+ (Get-Random -Maximum ([int]::MaxValue))) -AsPlainText -Force + Set-SecretStoreConfiguration -Authentication None -Interaction None -Confirm:$false -Password $vaultPassword + Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault + displayName: 'Install Secret store' + + - task: AzurePowerShell@5 + inputs: + azureSubscription: PSResourceGetACR + azurePowerShellVersion: LatestVersion + ScriptType: InlineScript + pwsh: true + inline: | + Write-Verbose -Verbose "Getting Azure Container Registry" + Get-AzContainerRegistry -ResourceGroupName 'PSResourceGet' -Name 'psresourcegettest' | Select-Object -Property * + Write-Verbose -Verbose "Setting up secret for Azure Container Registry" + $azt = Get-AzAccessToken + $tenantId = $azt.TenantID + Set-Secret -Name $tenantId -Secret $azt.Token -Verbose + $vstsCommandString = "vso[task.setvariable variable=TenantId]$tenantId" + Write-Host "sending " + $vstsCommandString + Write-Host "##$vstsCommandString" + displayName: 'Setup Azure Container Registry secret' + + - pwsh: | + Import-Module .\build.psm1 -force + Import-Module .\tools\ci.psm1 + Restore-PSOptions -PSOptionsPath '$(System.ArtifactsDirectory)\build\psoptions.json' + $options = (Get-PSOptions) + $path = split-path -path $options.Output + $rootPath = split-Path -path $path + Expand-Archive -Path '$(System.ArtifactsDirectory)\build\build.zip' -DestinationPath $rootPath -Force + + $pwshExe = Get-ChildItem -Path $rootPath -Recurse -Filter pwsh.exe | Select-Object -First 1 + + $outputFilePath = "$(Build.SourcesDirectory)\test\powershell\Modules\Microsoft.PowerShell.PSResourceGet\ACRTests.xml" + $cmdline = "`$env:ACRTESTS = 'true'; Invoke-Pester -Path '$(Build.SourcesDirectory)\test\powershell\Modules\Microsoft.PowerShell.PSResourceGet\Microsoft.PowerShell.PSResourceGet.Tests.ps1' -TestName 'PSResourceGet - ACR tests' -OutputFile $outputFilePath -OutputFormat NUnitXml" + Write-Verbose -Verbose "Running $cmdline" + + & $pwshExe -Command $cmdline + + Publish-TestResults -Title "PSResourceGet - ACR tests" -Path $outputFilePath -Type NUnit + displayName: 'PSResourceGet ACR functional tests using AzAuth' + diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json index 33f2f0c4c96..bfaa2237519 100644 --- a/dsc/pwsh.profile.dsc.resource.json +++ b/dsc/pwsh.profile.dsc.resource.json @@ -54,15 +54,13 @@ "title": "Profile", "description": "A resource for managing PowerShell profiles.", "type": "object", - "additionalProperties": false, + "unevaluatedProperties": false, "required": [ "profileType" ], "properties": { "profileType": { - "title": "Name", - "description": "The name of the repository.", - "type": "string" + "$ref": "#/$defs/ProfileType" }, "content": { "title": "Content", diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 new file mode 100644 index 00000000000..bbcdeb3f233 --- /dev/null +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -0,0 +1,146 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { + BeforeAll { + # Ensure DSC v3 is available + if (-not (Get-Command -name dsc -CommandType Application -ErrorAction SilentlyContinue)) { + throw "DSC v3 is not installed" + } + + $dscExe = Get-Command -name dsc -CommandType Application | Select-Object -First 1 + + $testProfileContent = "# Test profile content currentuser currenthost" + $testProfilePathCurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost + Copy-Item -Path $testProfilePathCurrentUserCurrentHost -Destination "$TestDrive/currentuser-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue + Set-Content -Path $testProfilePathCurrentUserCurrentHost -Value $testProfileContent -Force + + $testProfileContent = "# Test profile content currentuser allhosts" + $testProfilePathCurrentUserAllHosts = $PROFILE.CurrentUserAllHosts + Copy-Item -Path $testProfilePathCurrentUserAllHosts -Destination "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + Set-Content -Path $testProfilePathCurrentUserAllHosts -Value $testProfileContent -Force + + $originalPath = $env:PATH + $env:PATH += ";$PSHome" + } + AfterAll { + # Restore original profile + $testProfilePathCurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost + Copy-Item -Path "$TestDrive/currentuser-currenthost-profile.bak" -Destination $testProfilePathCurrentUserCurrentHost -Force -ErrorAction SilentlyContinue + + $testProfilePathCurrentUserAllHosts = $PROFILE.CurrentUserAllHosts + Copy-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Destination $testProfilePathCurrentUserAllHosts -Force -ErrorAction SilentlyContinue + + $env:PATH = $originalPath + Remove-Item -Path "$TestDrive/currentuser-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + } + + It 'DSC resource is located at $PSHome' { + $resourceFile = Join-Path -Path $PSHome -ChildPath 'pwsh.profile.resource.ps1' + $resourceFile | Should -Exist + + $resourceManifest = Join-Path -Path $PSHome -ChildPath 'pwsh.profile.dsc.resource.json' + $resourceManifest | Should -Exist + } + + It 'DSC resource can be found' { + (& $dscExe resource list -o json | ConvertFrom-Json | Select-Object -Property type).type | Should -Contain 'Microsoft.PowerShell/Profile' + } + + It 'DSC resource can set current user current host profile' { + $setOutput = (& $dscExe config set --file .\psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get current user current host profile' { + $getOutput = (& $dscExe config get --file .\psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can set current user all hosts profile' { + $setOutput = (& $dscExe config set --file .\psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get current user all hosts profile' { + $getOutput = (& $dscExe config get --file .\psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can export all profiles' { + $exportOutput = (& $dscExe config export --file .\psprofile_export.dsc.yaml -o json) | ConvertFrom-Json + + $exportOutput.resources | Should -HaveCount 4 + + $exportOutput.resources | ForEach-Object { + $_.type | Should -Be 'Microsoft.PowerShell/Profile' + $_.name | Should -BeIn @('AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserCurrentHost', 'CurrentUserAllHosts') + } + } +} + +Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdminOnWindows', 'RequireSudoOnUnix' { + BeforeAll { + # Ensure DSC v3 is available + if (-not (Get-Command -name dsc -CommandType Application -ErrorAction SilentlyContinue)) { + throw "DSC v3 is not installed" + } + + $dscExe = Get-Command -name dsc -CommandType Application | Select-Object -First 1 + + $testProfileContent = "# Test profile content allusers currenthost" + $testProfilePathAllUsersCurrentHost = $PROFILE.AllUsersCurrentHost + Copy-Item -Path $testProfilePathAllUsersCurrentHost -Destination "$TestDrive/allusers-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue + Set-Content -Path $testProfilePathAllUsersCurrentHost -Value $testProfileContent -Force + + $testProfileContent = "# Test profile content allusers allhosts" + $testProfilePathAllUsersAllHosts = $PROFILE.AllUsersAllHosts + Copy-Item -Path $testProfilePathAllUsersAllHosts -Destination "$TestDrive/allusers-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + Set-Content -Path $testProfilePathAllUsersAllHosts -Value $testProfileContent -Force + + + $originalPath = $env:PATH + $env:PATH += ";$PSHome" + } + AfterAll { + $env:PATH = $originalPath + + $testProfilePathAllUsersCurrentHost = $PROFILE.AllUsersCurrentHost + Copy-Item -Path "$TestDrive/allusers-currenthost-profile.bak" -Destination $testProfilePathAllUsersCurrentHost -Force -ErrorAction SilentlyContinue + + $testProfilePathAllUsersAllHosts = $PROFILE.AllUsersAllHosts + Copy-Item -Path "$TestDrive/allusers-allhosts-profile.bak" -Destination $testProfilePathAllUsersAllHosts -Force -ErrorAction SilentlyContinue + + Remove-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + Remove-Item -Path "$TestDrive/allusers-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue + } + + It 'DSC resource can set all users all hosts profile' { + $setOutput = (& $dscExe config set --file .\psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get all users all hosts profile' { + $getOutput = (& $dscExe config get --file .\psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can set all users current hosts profile' { + $setOutput = (& $dscExe config set --file .\psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" + $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent + } + + It 'DSC resource can get all users current hosts profile' { + $getOutput = (& $dscExe config get --file .\psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" + $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent + } +} diff --git a/test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml b/test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml new file mode 100644 index 00000000000..356119826c3 --- /dev/null +++ b/test/powershell/dsc/psprofile_alluser_allhost.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: AllUsersAllHosts + content: "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml b/test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml new file mode 100644 index 00000000000..bc51f0a4392 --- /dev/null +++ b/test/powershell/dsc/psprofile_allusers_currenthost.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: AllUsersCurrentHost + content: "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml b/test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml new file mode 100644 index 00000000000..8ed8d98c3ab --- /dev/null +++ b/test/powershell/dsc/psprofile_currentuser_allhosts.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: CurrentUserAllHosts + content: "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml b/test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml new file mode 100644 index 00000000000..5a42c28eb96 --- /dev/null +++ b/test/powershell/dsc/psprofile_currentuser_currenthost.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: CurrentUserCurrentHost + content: "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" + _exist: true diff --git a/test/powershell/dsc/psprofile_export.dsc.yaml b/test/powershell/dsc/psprofile_export.dsc.yaml new file mode 100644 index 00000000000..356119826c3 --- /dev/null +++ b/test/powershell/dsc/psprofile_export.dsc.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: AllUsersAllHosts + content: "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" + _exist: true From 8b2b85e12e3eeef8e3dbe6ac85d0924bb88a3361 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 3 Oct 2025 15:42:35 -0700 Subject: [PATCH 03/20] Add experimental feature --- experimental-feature-windows.json | 1 + .../engine/ExperimentalFeature/ExperimentalFeature.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/experimental-feature-windows.json b/experimental-feature-windows.json index ca5b49878a4..31f7b965a5b 100644 --- a/experimental-feature-windows.json +++ b/experimental-feature-windows.json @@ -2,6 +2,7 @@ "PSFeedbackProvider", "PSLoadAssemblyFromNativeCode", "PSNativeWindowsTildeExpansion", + "PSProfileDSCResource", "PSSerializeJSONLongEnumAsNumber", "PSRedirectToVariable", "PSSubsystemPluginModel" diff --git a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs index 1c17a22ae9a..7e17ec43137 100644 --- a/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs +++ b/src/System.Management.Automation/engine/ExperimentalFeature/ExperimentalFeature.cs @@ -21,6 +21,7 @@ public class ExperimentalFeature internal const string EngineSource = "PSEngine"; internal const string PSSerializeJSONLongEnumAsNumber = nameof(PSSerializeJSONLongEnumAsNumber); + internal const string PSProfileDSCResource = "PSProfileDSCResource"; #endregion @@ -109,6 +110,10 @@ static ExperimentalFeature() new ExperimentalFeature( name: PSSerializeJSONLongEnumAsNumber, description: "Serialize enums based on long or ulong as an numeric value rather than the string representation when using ConvertTo-Json." + ), + new ExperimentalFeature( + name: PSProfileDSCResource, + description: "DSC v3 resources for managing PowerShell profile." ) }; From 522b77d4583b2b40e5be8ebf270d77d1df900aea Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 6 Oct 2025 15:43:12 -0700 Subject: [PATCH 04/20] Add experimental feature for linux --- experimental-feature-linux.json | 1 + 1 file changed, 1 insertion(+) diff --git a/experimental-feature-linux.json b/experimental-feature-linux.json index ca5b49878a4..31f7b965a5b 100644 --- a/experimental-feature-linux.json +++ b/experimental-feature-linux.json @@ -2,6 +2,7 @@ "PSFeedbackProvider", "PSLoadAssemblyFromNativeCode", "PSNativeWindowsTildeExpansion", + "PSProfileDSCResource", "PSSerializeJSONLongEnumAsNumber", "PSRedirectToVariable", "PSSubsystemPluginModel" From d91d6e41991e7f40bf1a5f5cfc5752c61ed4db17 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Tue, 14 Oct 2025 20:06:14 -0700 Subject: [PATCH 05/20] Fixes --- .../install-azure-devops-package/README.md | 134 +++++++++++++++ .../install-azure-devops-package/action.yml | 153 ++++++++++++++++++ .github/actions/test/nix/action.yml | 54 +++++++ .github/actions/test/windows/action.yml | 30 +++- .github/workflows/linux-ci.yml | 12 ++ .github/workflows/macos-ci.yml | 12 ++ .github/workflows/windows-ci.yml | 12 ++ dsc/pwsh.profile.dsc.resource.json | 2 +- dsc/pwsh.profile.resource.ps1 | 4 +- src/powershell-unix/powershell-unix.csproj | 4 + .../powershell-win-core.csproj | 2 +- .../dsc/dsc.profileresource.Tests.ps1 | 67 ++++++-- 12 files changed, 464 insertions(+), 22 deletions(-) create mode 100644 .github/actions/install-azure-devops-package/README.md create mode 100644 .github/actions/install-azure-devops-package/action.yml diff --git a/.github/actions/install-azure-devops-package/README.md b/.github/actions/install-azure-devops-package/README.md new file mode 100644 index 00000000000..0582bb0f173 --- /dev/null +++ b/.github/actions/install-azure-devops-package/README.md @@ -0,0 +1,134 @@ +# Install Azure DevOps Universal Package Action + +This composite GitHub Action downloads and optionally extracts Universal Packages from Azure DevOps Artifacts feeds. + +## Features + +- Downloads Universal Packages from Azure DevOps Artifacts feeds +- Supports both organization and project scoped feeds +- Optional package extraction with customizable paths +- Environment variable setting for integration with subsequent steps +- Flexible package file name patterns with version substitution + +## Inputs + +| Input | Description | Required | Default | +|-------|-------------|----------|---------| +| `feed` | Azure DevOps Artifacts feed name | ✅ | - | +| `package` | Package name to download | ✅ | - | +| `version` | Package version to download | ✅ | - | +| `organization` | Azure DevOps organization URL | ✅ | `https://dev.azure.com/powershell/` | +| `project` | Azure DevOps project ID | ✅ | - | +| `scope` | Package scope (project or organization) | ❌ | `project` | +| `destination` | Destination path for package download | ❌ | `${{ github.workspace }}\packages` | +| `packageFileName` | Expected package file name pattern (supports `{version}` substitution) | ❌ | - | +| `extractSubFolder` | Subfolder name to create for extraction | ❌ | - | +| `environmentVariable` | Environment variable name to set with extraction path | ❌ | - | + +## Outputs + +| Output | Description | +|--------|-------------| +| `packagePath` | Path where the package was downloaded | +| `extractionPath` | Path where the package was extracted | + +## Prerequisites + +- The `AZURE_DEVOPS_EXT_PAT` secret must be configured in your repository +- The Azure DevOps Personal Access Token must have permissions to read from the specified artifacts feed + +## Usage Examples + +### Basic Package Download + +```yaml +- name: Download Package + uses: ./.github/actions/install-azure-devops-package + with: + feed: 'MyFeed' + package: 'my-package' + version: '1.0.0' + project: 'my-project-id' +``` + +### Download and Extract with Environment Variable + +```yaml +- name: Install DSC Package + uses: ./.github/actions/install-azure-devops-package + with: + feed: 'PowerShell-Universal' + package: 'microsoft.dsc-windows' + version: '3.2.0-preview.6' + project: '2972bb5c-f20c-4a60-8bd9-00ffe9987edc' + packageFileName: 'DSC-{version}-x86_64-pc-windows-msvc.zip' + extractSubFolder: 'DSC' + environmentVariable: 'DSC_ROOT' +``` + +### Using Custom Organization and Destination + +```yaml +- name: Download Package + uses: ./.github/actions/install-azure-devops-package + with: + feed: 'MyFeed' + package: 'my-package' + version: '2.1.0' + organization: 'https://dev.azure.com/myorg/' + project: 'my-project-id' + destination: '${{ github.workspace }}\custom-packages' + scope: 'organization' +``` + +## Environment Setup + +Make sure to set up the Azure DevOps Personal Access Token as a repository secret: + +1. Go to your repository's Settings → Secrets and variables → Actions +1. Add a new secret named `AZURE_DEVOPS_EXT_PAT` +1. Set the value to your Azure DevOps Personal Access Token with appropriate permissions + +## Original Use Case Migration + +The original step from the Windows test action: + +```yaml +- name: Install Universal Package from Azure DevOps Feed + shell: pwsh + env: + AZURE_DEVOPS_EXT_PAT: ${{ secrets.AZURE_DEVOPS_EXT_PAT }} + run: |- + # ... PowerShell script ... +``` + +Can be replaced with: + +```yaml +- name: Install Universal Package from Azure DevOps Feed + uses: ./.github/actions/install-azure-devops-package + with: + feed: 'PowerShell-Universal' + package: 'microsoft.dsc-windows' + version: '3.2.0-preview.6' + project: '2972bb5c-f20c-4a60-8bd9-00ffe9987edc' + packageFileName: 'DSC-{version}-x86_64-pc-windows-msvc.zip' + extractSubFolder: 'DSC' + environmentVariable: 'DSC_ROOT' +``` + +## Error Handling + +The action includes error handling for: + +- Missing Azure DevOps CLI installation +- Failed package downloads +- Missing expected package files +- Directory creation failures +- Archive extraction errors + +## Security Considerations + +- The `AZURE_DEVOPS_EXT_PAT` environment variable is used for authentication +- Ensure your Personal Access Token has minimal required permissions +- Consider using fine-grained tokens scoped to specific feeds and projects diff --git a/.github/actions/install-azure-devops-package/action.yml b/.github/actions/install-azure-devops-package/action.yml new file mode 100644 index 00000000000..23af19c4a77 --- /dev/null +++ b/.github/actions/install-azure-devops-package/action.yml @@ -0,0 +1,153 @@ +name: 'Install Azure DevOps Universal Package' +description: 'Downloads and extracts a Universal Package from Azure DevOps Artifacts feed' + +inputs: + feed: + description: 'Azure DevOps Artifacts feed name' + required: true + package: + description: 'Package name to download' + required: true + version: + description: 'Package version to download' + required: true + organization: + description: 'Azure DevOps organization URL' + required: true + default: 'https://dev.azure.com/powershell/' + project: + description: 'Azure DevOps project ID' + required: true + scope: + description: 'Package scope (project or organization)' + required: false + default: 'project' + client-id: + description: 'Azure AD Application (Client) ID for authentication' + required: true + default: '' + tenant-id: + description: 'Azure AD Tenant ID for authentication' + required: true + default: '' + subscription-id: + description: 'Azure Subscription ID for authentication' + required: true + default: '' + destination: + description: 'Destination path for package download' + required: false + default: '${{ github.workspace }}\packages' + packageFileName: + description: 'Expected package file name pattern (supports variable substitution with {version})' + required: false + default: '' + extractSubFolder: + description: 'Subfolder name to create for extraction' + required: false + default: '' + environmentVariable: + description: 'Environment variable name to set with extraction path' + required: false + default: '' + +outputs: + packagePath: + description: 'Path where the package was downloaded' + value: ${{ steps.download.outputs.packagePath }} + extractionPath: + description: 'Path where the package was extracted' + value: ${{ steps.download.outputs.extractionPath }} + +runs: + using: composite + steps: + - name: Azure CLI Login + uses: azure/login@v2 + with: + client-id: ${{ inputs.client-id }} + tenant-id: ${{ inputs.tenant-id }} + subscription-id: ${{ inputs.subscription-id }} + + - name: Download and Extract Azure DevOps Universal Package + id: download + shell: pwsh + env: + AZURE_DEVOPS_EXT_PAT: ${{ env.AZURE_DEVOPS_EXT_PAT }} + run: | + $feed = "${{ inputs.feed }}" + $package = "${{ inputs.package }}" + $version = "${{ inputs.version }}" + $org = "${{ inputs.organization }}" + $project = "${{ inputs.project }}" + $scope = "${{ inputs.scope }}" + $destination = "${{ inputs.destination }}" + $packageFileName = "${{ inputs.packageFileName }}" + $extractSubFolder = "${{ inputs.extractSubFolder }}" + $environmentVariable = "${{ inputs.environmentVariable }}" + + Import-Module ./build.psm1 + Switch-PSNugetConfig -Source Public + + Write-Host "Installing Azure DevOps CLI..." + az extension add --name azure-devops + + Write-Host "Configuring Azure DevOps defaults..." + az devops configure --defaults organization=$org project=$project + + Write-Host "Downloading package: $package version $version from feed: $feed" + az artifacts universal download --feed $feed --name $package --version $version --path $destination --scope $scope + + Write-Host "Downloaded $package version $version to $destination" + + # Set output for package path + echo "packagePath=$destination" >> $env:GITHUB_OUTPUT + + # Handle package extraction if packageFileName is provided + if ($packageFileName) { + # Replace {version} placeholder with actual version + $actualPackageFileName = $packageFileName -replace '\{version\}', $version + $packagePath = Join-Path $destination $actualPackageFileName + + Write-Host "Looking for package file: $actualPackageFileName" + + if (-not (Test-Path -Path $packagePath)) { + throw "Package $actualPackageFileName not found in $destination" + } + + # Determine extraction path + if ($extractSubFolder) { + $extractPath = Join-Path $destination $extractSubFolder + } else { + $extractPath = $destination + } + + # Create extraction directory if it doesn't exist + if (-not (Test-Path -Path $extractPath)) { + New-Item -Path $extractPath -ItemType Directory | Out-Null + } + + if ($packageFileName -like "*.tar.gz") { + Write-Host "Extracting tar.gz package $packagePath to $extractPath" + tar -xvzf $packagePath -C $extractPath + Write-Host "Extracted package to $extractPath" + } + else { + Write-Host "Extracting package $packagePath to $extractPath" + Expand-Archive -Path $packagePath -DestinationPath $extractPath -Force + Write-Host "Extracted package to $extractPath" + } + + # Set output for extraction path + echo "extractionPath=$extractPath" >> $env:GITHUB_OUTPUT + + # Set environment variable if specified + if ($environmentVariable) { + Write-Host "Setting environment variable $environmentVariable to $extractPath" + echo "$environmentVariable=$extractPath" | Out-File -FilePath $env:GITHUB_ENV -Append + } + } else { + echo "extractionPath=$destination" >> $env:GITHUB_OUTPUT + } + + Write-Host "Azure DevOps Universal Package installation completed successfully" diff --git a/.github/actions/test/nix/action.yml b/.github/actions/test/nix/action.yml index ef943bfce78..b328ba90cd3 100644 --- a/.github/actions/test/nix/action.yml +++ b/.github/actions/test/nix/action.yml @@ -14,6 +14,18 @@ inputs: required: false default: ctrf type: string + client-id: + description: 'Azure AD Application (Client) ID for authentication' + required: true + default: '' + tenant-id: + description: 'Azure AD Tenant ID for authentication' + required: true + default: '' + subscription-id: + description: 'Azure Subscription ID for authentication' + required: true + default: '' runs: using: composite @@ -43,6 +55,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 | 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 + Write-Host "Download URL: $downloadUrl" + + $tempPath = Get-GWTempPath + + Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath/DSC.tar.gz" -Verbose + New-Item -ItemType Directory -Path "$tempPath/DSC" -Force -Verbose + tar xvf "$tempPath/DSC.tar.gz" -C "$tempPath/DSC" + $dscRoot = "$tempPath/DSC" + Write-Host "DSC Root: $dscRoot" + Set-GWVariable -Name "DSC_ROOT" -Value $dscRoot + - name: Bootstrap shell: pwsh run: |- diff --git a/.github/actions/test/windows/action.yml b/.github/actions/test/windows/action.yml index 3b3ce0cafe8..75a0985bbe1 100644 --- a/.github/actions/test/windows/action.yml +++ b/.github/actions/test/windows/action.yml @@ -5,7 +5,6 @@ inputs: purpose: required: false default: '' - type: string tagSet: required: false default: CI @@ -14,6 +13,15 @@ inputs: required: false default: ctrf type: string + client-id: + required: true + type: string + tenant-id: + required: true + type: string + subscription-id: + required: true + type: string runs: using: composite @@ -43,6 +51,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 | Select-Object -First 1 + $latestVersion = $latestRelease.tag_name.TrimStart("v") + Write-Host "Latest DSC Version: $latestVersion" + + $downloadUrl = $latestRelease.assets | Where-Object { $_.name -like "DSC-*-x86_64-pc-windows-msvc.zip" } | Select-Object -First 1 | Select-Object -ExpandProperty browser_download_url + Write-Host "Download URL: $downloadUrl" + $tempPath = Get-GWTempPath + Invoke-RestMethod -Uri $downloadUrl -OutFile "$tempPath\DSC.zip" + + $null = New-Item -ItemType Directory -Path "$tempPath\DSC" -Force + Expand-Archive -Path "$tempPath\DSC.zip" -DestinationPath "$tempPath\DSC" -Force + $dscRoot = "$tempPath\DSC" + Write-Host "DSC Root: $dscRoot" + Set-GWVariable -Name "DSC_ROOT" -Value $dscRoot + - name: Bootstrap shell: powershell run: |- diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index 2058bd61568..a6a83bef436 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -111,6 +111,9 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} linux_test_elevated_ci: name: Linux Elevated CI needs: @@ -128,6 +131,9 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} linux_test_unelevated_others: name: Linux Unelevated Others needs: @@ -145,6 +151,9 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} linux_test_elevated_others: name: Linux Elevated Others needs: @@ -162,6 +171,9 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} xunit_tests: name: xUnit Tests needs: diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index 2ee96079049..a06f12f53c0 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -93,6 +93,9 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} macos_test_elevated_ci: name: macOS Elevated CI needs: @@ -110,6 +113,9 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} macos_test_unelevated_others: name: macOS Unelevated Others needs: @@ -127,6 +133,9 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} macos_test_elevated_others: name: macOS Elevated Others needs: @@ -144,6 +153,9 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} xunit_tests: name: xUnit Tests needs: diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 582860de34c..8e2e4016f99 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -97,6 +97,9 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} windows_test_elevated_ci: name: Windows Elevated CI needs: @@ -114,6 +117,9 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} windows_test_unelevated_others: name: Windows Unelevated Others needs: @@ -131,6 +137,9 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} windows_test_elevated_others: name: Windows Elevated Others needs: @@ -148,6 +157,9 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} xunit_tests: name: xUnit Tests needs: diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json index bfaa2237519..5fb9e514732 100644 --- a/dsc/pwsh.profile.dsc.resource.json +++ b/dsc/pwsh.profile.dsc.resource.json @@ -8,7 +8,7 @@ "powershell" ], "type": "Microsoft.PowerShell/Profile", - "version": "0.0.1", + "version": "0.1.0", "get": { "executable": "pwsh", "args": [ diff --git a/dsc/pwsh.profile.resource.ps1 b/dsc/pwsh.profile.resource.ps1 index d935e14504c..3c07f774bc9 100644 --- a/dsc/pwsh.profile.resource.ps1 +++ b/dsc/pwsh.profile.resource.ps1 @@ -115,8 +115,8 @@ function SetOperation { if ($InputResource.content) { Set-Content -Path $profilePath -Value $InputResource.content } - else { - Remove-Item -Path $profilePath -Force + else { + ## Do nothing if content is not specified } } elseif (-not $InputResource._exist -and $profileExists) { diff --git a/src/powershell-unix/powershell-unix.csproj b/src/powershell-unix/powershell-unix.csproj index 802acf05e3a..20c61247d24 100644 --- a/src/powershell-unix/powershell-unix.csproj +++ b/src/powershell-unix/powershell-unix.csproj @@ -37,6 +37,10 @@ PreserveNewest PreserveNewest + + PreserveNewest + PreserveNewest + diff --git a/src/powershell-win-core/powershell-win-core.csproj b/src/powershell-win-core/powershell-win-core.csproj index e6efeac10f0..04ecff48690 100644 --- a/src/powershell-win-core/powershell-win-core.csproj +++ b/src/powershell-win-core/powershell-win-core.csproj @@ -33,7 +33,7 @@ PreserveNewest PreserveNewest - + PreserveNewest PreserveNewest diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 index bbcdeb3f233..dcd20c72ccf 100644 --- a/test/powershell/dsc/dsc.profileresource.Tests.ps1 +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -3,8 +3,27 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { BeforeAll { + $DSC_ROOT = $env:DSC_ROOT + + if (-not (Test-Path -Path $DSC_ROOT)) { + throw "DSC_ROOT environment variable is not set or path does not exist." + } + + Write-Verbose "DSC_ROOT is set to $DSC_ROOT" -Verbose + + $originalPath = $env:PATH + + $pathSeparator = [System.IO.Path]::PathSeparator + $env:PATH += "$pathSeparator$DSC_ROOT" + $env:PATH += "$pathSeparator$PSHome" + + Write-Verbose "Updated PATH to include DSC_ROOT: $env:PATH" -Verbose + # Ensure DSC v3 is available if (-not (Get-Command -name dsc -CommandType Application -ErrorAction SilentlyContinue)) { + Get-ChildItem $DSC_ROOT -Recurse 'dsc' | ForEach-Object { + Write-Verbose "Found DSC executable at $($_.FullName)" -Verbose + } throw "DSC v3 is not installed" } @@ -13,15 +32,12 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { $testProfileContent = "# Test profile content currentuser currenthost" $testProfilePathCurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost Copy-Item -Path $testProfilePathCurrentUserCurrentHost -Destination "$TestDrive/currentuser-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue - Set-Content -Path $testProfilePathCurrentUserCurrentHost -Value $testProfileContent -Force + New-Item -Path $testProfilePathCurrentUserCurrentHost -Value $testProfileContent -Force -ItemType File $testProfileContent = "# Test profile content currentuser allhosts" $testProfilePathCurrentUserAllHosts = $PROFILE.CurrentUserAllHosts Copy-Item -Path $testProfilePathCurrentUserAllHosts -Destination "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue - Set-Content -Path $testProfilePathCurrentUserAllHosts -Value $testProfileContent -Force - - $originalPath = $env:PATH - $env:PATH += ";$PSHome" + New-Item -Path $testProfilePathCurrentUserAllHosts -Value $testProfileContent -Force -ItemType File } AfterAll { # Restore original profile @@ -49,31 +65,31 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { } It 'DSC resource can set current user current host profile' { - $setOutput = (& $dscExe config set --file .\psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent } It 'DSC resource can get current user current host profile' { - $getOutput = (& $dscExe config get --file .\psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_currentuser_currenthost.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserCurrentHost!'" $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent } It 'DSC resource can set current user all hosts profile' { - $setOutput = (& $dscExe config set --file .\psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent } It 'DSC resource can get current user all hosts profile' { - $getOutput = (& $dscExe config get --file .\psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent } It 'DSC resource can export all profiles' { - $exportOutput = (& $dscExe config export --file .\psprofile_export.dsc.yaml -o json) | ConvertFrom-Json + $exportOutput = (& $dscExe config export --file $PSScriptRoot/psprofile_export.dsc.yaml -o json) | ConvertFrom-Json $exportOutput.resources | Should -HaveCount 4 @@ -86,8 +102,26 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdminOnWindows', 'RequireSudoOnUnix' { BeforeAll { + $DSC_ROOT = $env:DSC_ROOT + + if (-not (Test-Path -Path $DSC_ROOT)) { + throw "DSC_ROOT environment variable is not set or path does not exist." + } + + Write-Verbose "DSC_ROOT is set to $DSC_ROOT" -Verbose + $pathSeparator = [System.IO.Path]::PathSeparator + + $env:PATH += "$pathSeparator$DSC_ROOT" + + $env:PATH += "$pathSeparator$PSHome" + + Write-Verbose "Updated PATH to include DSC_ROOT: $env:PATH" -Verbose + # Ensure DSC v3 is available if (-not (Get-Command -name dsc -CommandType Application -ErrorAction SilentlyContinue)) { + Get-ChildItem $DSC_ROOT -Recurse 'dsc' | ForEach-Object { + Write-Verbose "Found DSC executable at $($_.FullName)" -Verbose + } throw "DSC v3 is not installed" } @@ -96,13 +130,12 @@ Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdm $testProfileContent = "# Test profile content allusers currenthost" $testProfilePathAllUsersCurrentHost = $PROFILE.AllUsersCurrentHost Copy-Item -Path $testProfilePathAllUsersCurrentHost -Destination "$TestDrive/allusers-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue - Set-Content -Path $testProfilePathAllUsersCurrentHost -Value $testProfileContent -Force + New-Item -Path $testProfilePathAllUsersCurrentHost -Value $testProfileContent -Force -ItemType File $testProfileContent = "# Test profile content allusers allhosts" $testProfilePathAllUsersAllHosts = $PROFILE.AllUsersAllHosts Copy-Item -Path $testProfilePathAllUsersAllHosts -Destination "$TestDrive/allusers-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue - Set-Content -Path $testProfilePathAllUsersAllHosts -Value $testProfileContent -Force - + New-Item -Path $testProfilePathAllUsersAllHosts -Value $testProfileContent -Force -ItemType File $originalPath = $env:PATH $env:PATH += ";$PSHome" @@ -121,25 +154,25 @@ Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdm } It 'DSC resource can set all users all hosts profile' { - $setOutput = (& $dscExe config set --file .\psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent } It 'DSC resource can get all users all hosts profile' { - $getOutput = (& $dscExe config get --file .\psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_alluser_allhost.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent } It 'DSC resource can set all users current hosts profile' { - $setOutput = (& $dscExe config set --file .\psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" $setOutput.results.result.afterState.content | Should -BeExactly $expectedContent } It 'DSC resource can get all users current hosts profile' { - $getOutput = (& $dscExe config get --file .\psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json + $getOutput = (& $dscExe config get --file $PSScriptRoot/psprofile_allusers_currenthost.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - AllUsersCurrentHost!'" $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent } From 9afb9e477647e3e4e88ee27935de9ca61ca4b5fb Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 12:18:12 -0800 Subject: [PATCH 06/20] Update boms --- tools/packaging/boms/linux.json | 10 ++++++++++ tools/packaging/boms/mac.json | 10 ++++++++++ tools/packaging/boms/windows.json | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/tools/packaging/boms/linux.json b/tools/packaging/boms/linux.json index 9f5f886a4ca..db29e47a290 100644 --- a/tools/packaging/boms/linux.json +++ b/tools/packaging/boms/linux.json @@ -2442,5 +2442,15 @@ { "Pattern": "System.Management.Automation.dll", "FileType": "Product" + }, + { + "Pattern": "pwsh.profile.dsc.resource.json", + "FileType": "Product", + "Architecture": null + }, + { + "Pattern": "pwsh.profile.resource.ps1", + "FileType": "Product", + "Architecture": null } ] diff --git a/tools/packaging/boms/mac.json b/tools/packaging/boms/mac.json index a4b5bc2bffb..af4fcb0c967 100644 --- a/tools/packaging/boms/mac.json +++ b/tools/packaging/boms/mac.json @@ -2218,5 +2218,15 @@ { "Pattern": "System.Management.Automation.dll", "FileType": "Product" + }, + { + "Pattern": "pwsh.profile.dsc.resource.json", + "FileType": "Product", + "Architecture": null + }, + { + "Pattern": "pwsh.profile.resource.ps1", + "FileType": "Product", + "Architecture": null } ] diff --git a/tools/packaging/boms/windows.json b/tools/packaging/boms/windows.json index d8857c71786..af4d1736ed8 100644 --- a/tools/packaging/boms/windows.json +++ b/tools/packaging/boms/windows.json @@ -4425,5 +4425,15 @@ "Pattern": "System.Management.Automation.dll", "FileType": "Product", "Architecture": null + }, + { + "Pattern": "pwsh.profile.dsc.resource.json", + "FileType": "Product", + "Architecture": null + }, + { + "Pattern": "pwsh.profile.resource.ps1", + "FileType": "Product", + "Architecture": null } ] From d55c775ca214e29d4ddb31b60f6627d5fe18fa85 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 13:31:20 -0800 Subject: [PATCH 07/20] Cleanup profiles properly --- .../dsc/dsc.profileresource.Tests.ps1 | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 index dcd20c72ccf..b4b4b579c0d 100644 --- a/test/powershell/dsc/dsc.profileresource.Tests.ps1 +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -42,10 +42,20 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { AfterAll { # Restore original profile $testProfilePathCurrentUserCurrentHost = $PROFILE.CurrentUserCurrentHost - Copy-Item -Path "$TestDrive/currentuser-currenthost-profile.bak" -Destination $testProfilePathCurrentUserCurrentHost -Force -ErrorAction SilentlyContinue + if (Test-Path "$TestDrive/currentuser-currenthost-profile.bak") { + Copy-Item -Path "$TestDrive/currentuser-currenthost-profile.bak" -Destination $testProfilePathCurrentUserCurrentHost -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathCurrentUserCurrentHost -Force -ErrorAction SilentlyContinue + } $testProfilePathCurrentUserAllHosts = $PROFILE.CurrentUserAllHosts - Copy-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Destination $testProfilePathCurrentUserAllHosts -Force -ErrorAction SilentlyContinue + if (Test-Path "$TestDrive/currentuser-allhosts-profile.bak") { + Copy-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Destination $testProfilePathCurrentUserAllHosts -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathCurrentUserAllHosts -Force -ErrorAction SilentlyContinue + } $env:PATH = $originalPath Remove-Item -Path "$TestDrive/currentuser-currenthost-profile.bak" -Force -ErrorAction SilentlyContinue @@ -144,10 +154,20 @@ Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdm $env:PATH = $originalPath $testProfilePathAllUsersCurrentHost = $PROFILE.AllUsersCurrentHost - Copy-Item -Path "$TestDrive/allusers-currenthost-profile.bak" -Destination $testProfilePathAllUsersCurrentHost -Force -ErrorAction SilentlyContinue + if (Test-Path "$TestDrive/allusers-currenthost-profile.bak") { + Copy-Item -Path "$TestDrive/allusers-currenthost-profile.bak" -Destination $testProfilePathAllUsersCurrentHost -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathAllUsersCurrentHost -Force -ErrorAction SilentlyContinue + } $testProfilePathAllUsersAllHosts = $PROFILE.AllUsersAllHosts - Copy-Item -Path "$TestDrive/allusers-allhosts-profile.bak" -Destination $testProfilePathAllUsersAllHosts -Force -ErrorAction SilentlyContinue + if (Test-Path "$TestDrive/allusers-allhosts-profile.bak") { + Copy-Item -Path "$TestDrive/allusers-allhosts-profile.bak" -Destination $testProfilePathAllUsersAllHosts -Force -ErrorAction SilentlyContinue + } + else { + Remove-Item $testProfilePathAllUsersAllHosts -Force -ErrorAction SilentlyContinue + } Remove-Item -Path "$TestDrive/currentuser-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue Remove-Item -Path "$TestDrive/allusers-allhosts-profile.bak" -Force -ErrorAction SilentlyContinue From 492ddb16b8aa5b49fbac99bd4585ae4629a98177 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 14:04:11 -0800 Subject: [PATCH 08/20] Remove unused CI --- .vsts-ci/dsc-resource.yml | 178 -------------------------------------- 1 file changed, 178 deletions(-) delete mode 100644 .vsts-ci/dsc-resource.yml diff --git a/.vsts-ci/dsc-resource.yml b/.vsts-ci/dsc-resource.yml deleted file mode 100644 index e68120de131..00000000000 --- a/.vsts-ci/dsc-resource.yml +++ /dev/null @@ -1,178 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -name: PR-$(System.PullRequest.PullRequestNumber)-$(Date:yyyyMMdd)$(Rev:.rr) -trigger: - # Batch merge builds together while a merge build is running - batch: true - branches: - include: - - master - - release* - - feature* - paths: - include: - - 'src' -pr: - branches: - include: - - master - - release* - - feature* - paths: - include: - - 'dsc' - -parameters: - - name: dscVersion - type: string - default: 'v3.2.0-preview.4' - -variables: - GIT_CONFIG_PARAMETERS: "'core.autocrlf=false'" - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - POWERSHELL_TELEMETRY_OPTOUT: 1 - DOTNET_NOLOGO: 1 - __SuppressAnsiEscapeSequences: 1 - NugetSecurityAnalysisWarningLevel: none - nugetMultiFeedWarnLevel: none - -resources: -- repo: self - clean: true - -stages: -- stage: AcquireDsc - displayName: Install DSC - jobs: - - job: acquire_dsc - displayName: Acquire DSC Resources - pool: - vmImage: 'windows-latest' - - steps: - - pwsh: | - Get-ChildItem -Path env: - displayName: Capture Environment - condition: succeededOrFailed() - - - pwsh: | - # Download DSC from GitHub and install it locally for use in the build. - $dscVersion = '${{ parameters.dscVersion }}'.TrimStart('v') - Write-Verbose -Verbose "Downloading DSC version $dscVersion from GitHub..." - $dscZipUrl = https://github.com/PowerShell/DSC/releases/download/v$dscVersion/DSC-$dscVersion-x86_64-pc-windows-msvc.zip - Write-Verbose -Verbose "Downloading DSC from $dscZipUrl" - $destinationPath = "$(System.ArtifactsDirectory)\dsc\$dscVersion" - $zipFilePath = Join-Path -Path $destinationPath -ChildPath "DSC-$dscVersion.zip" - New-Item -ItemType Directory -Path $destinationPath -Force | Out-Null - Invoke-WebRequest -Uri $dscZipUrl -OutFile $zipFilePath -UseBasicParsing - Write-Verbose -Verbose "Extracting DSC to $destinationPath" - Expand-Archive -Path $zipFilePath -DestinationPath $destinationPath -Force - Remove-Item -Path $zipFilePath -Force - - Get-ChildItem -Path $destinationPath -Recurse | ForEach-Object { - Write-Verbose -Verbose "Extracted file: $($_.FullName)" - } - displayName: 'Download and Install DSC' - - - - pwsh: | - Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse - displayName: 'Capture Artifacts Directory' - continueOnError: true - - - pwsh: | - Import-Module .\tools\ci.psm1 - Invoke-CIInstall - displayName: Bootstrap - -- stage: TestWin - displayName: Test PSResourceGetACR - jobs: - - job: win_test_ACR - displayName: PSResourceGet ACR Tests - pool: - vmImage: 'windows-latest' - - steps: - - pwsh: | - Get-ChildItem -Path env: - displayName: Capture Environment - condition: succeededOrFailed() - - - task: DownloadBuildArtifacts@0 - displayName: 'Download Build Artifacts' - inputs: - downloadType: specific - itemPattern: | - build/**/* - downloadPath: '$(System.ArtifactsDirectory)' - - - pwsh: | - Get-ChildItem "$(System.ArtifactsDirectory)\*" -Recurse - displayName: 'Capture Artifacts Directory' - continueOnError: true - - - pwsh: | - # Remove "Program Files\dotnet" from the env variable PATH, so old SDKs won't affect us. - Write-Host "Old Path:" - Write-Host $env:Path - - $dotnetPath = Join-Path $env:SystemDrive 'Program Files\dotnet' - $paths = $env:Path -split ";" | Where-Object { -not $_.StartsWith($dotnetPath) } - $env:Path = $paths -join ";" - - Write-Host "New Path:" - Write-Host $env:Path - - # Bootstrap - Import-Module .\tools\ci.psm1 - Invoke-CIInstall - displayName: Bootstrap - - - pwsh: | - Install-Module -Name 'Microsoft.PowerShell.SecretManagement' -force -SkipPublisherCheck -AllowClobber - Install-Module -Name 'Microsoft.PowerShell.SecretStore' -force -SkipPublisherCheck -AllowClobber - $vaultPassword = ConvertTo-SecureString $("a!!"+ (Get-Random -Maximum ([int]::MaxValue))) -AsPlainText -Force - Set-SecretStoreConfiguration -Authentication None -Interaction None -Confirm:$false -Password $vaultPassword - Register-SecretVault -Name SecretStore -ModuleName Microsoft.PowerShell.SecretStore -DefaultVault - displayName: 'Install Secret store' - - - task: AzurePowerShell@5 - inputs: - azureSubscription: PSResourceGetACR - azurePowerShellVersion: LatestVersion - ScriptType: InlineScript - pwsh: true - inline: | - Write-Verbose -Verbose "Getting Azure Container Registry" - Get-AzContainerRegistry -ResourceGroupName 'PSResourceGet' -Name 'psresourcegettest' | Select-Object -Property * - Write-Verbose -Verbose "Setting up secret for Azure Container Registry" - $azt = Get-AzAccessToken - $tenantId = $azt.TenantID - Set-Secret -Name $tenantId -Secret $azt.Token -Verbose - $vstsCommandString = "vso[task.setvariable variable=TenantId]$tenantId" - Write-Host "sending " + $vstsCommandString - Write-Host "##$vstsCommandString" - displayName: 'Setup Azure Container Registry secret' - - - pwsh: | - Import-Module .\build.psm1 -force - Import-Module .\tools\ci.psm1 - Restore-PSOptions -PSOptionsPath '$(System.ArtifactsDirectory)\build\psoptions.json' - $options = (Get-PSOptions) - $path = split-path -path $options.Output - $rootPath = split-Path -path $path - Expand-Archive -Path '$(System.ArtifactsDirectory)\build\build.zip' -DestinationPath $rootPath -Force - - $pwshExe = Get-ChildItem -Path $rootPath -Recurse -Filter pwsh.exe | Select-Object -First 1 - - $outputFilePath = "$(Build.SourcesDirectory)\test\powershell\Modules\Microsoft.PowerShell.PSResourceGet\ACRTests.xml" - $cmdline = "`$env:ACRTESTS = 'true'; Invoke-Pester -Path '$(Build.SourcesDirectory)\test\powershell\Modules\Microsoft.PowerShell.PSResourceGet\Microsoft.PowerShell.PSResourceGet.Tests.ps1' -TestName 'PSResourceGet - ACR tests' -OutputFile $outputFilePath -OutputFormat NUnitXml" - Write-Verbose -Verbose "Running $cmdline" - - & $pwshExe -Command $cmdline - - Publish-TestResults -Title "PSResourceGet - ACR tests" -Path $outputFilePath -Type NUnit - displayName: 'PSResourceGet ACR functional tests using AzAuth' - From ace86498e541db866632fd65c98ff7ddc92955e0 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 14:21:13 -0800 Subject: [PATCH 09/20] Cleanup workflows and actions --- .../install-azure-devops-package/README.md | 134 --------------- .../install-azure-devops-package/action.yml | 153 ------------------ .github/actions/test/nix/action.yml | 12 -- .github/actions/test/windows/action.yml | 9 -- .github/workflows/linux-ci.yml | 12 -- .github/workflows/macos-ci.yml | 12 -- .github/workflows/windows-ci.yml | 12 -- 7 files changed, 344 deletions(-) delete mode 100644 .github/actions/install-azure-devops-package/README.md delete mode 100644 .github/actions/install-azure-devops-package/action.yml diff --git a/.github/actions/install-azure-devops-package/README.md b/.github/actions/install-azure-devops-package/README.md deleted file mode 100644 index 0582bb0f173..00000000000 --- a/.github/actions/install-azure-devops-package/README.md +++ /dev/null @@ -1,134 +0,0 @@ -# Install Azure DevOps Universal Package Action - -This composite GitHub Action downloads and optionally extracts Universal Packages from Azure DevOps Artifacts feeds. - -## Features - -- Downloads Universal Packages from Azure DevOps Artifacts feeds -- Supports both organization and project scoped feeds -- Optional package extraction with customizable paths -- Environment variable setting for integration with subsequent steps -- Flexible package file name patterns with version substitution - -## Inputs - -| Input | Description | Required | Default | -|-------|-------------|----------|---------| -| `feed` | Azure DevOps Artifacts feed name | ✅ | - | -| `package` | Package name to download | ✅ | - | -| `version` | Package version to download | ✅ | - | -| `organization` | Azure DevOps organization URL | ✅ | `https://dev.azure.com/powershell/` | -| `project` | Azure DevOps project ID | ✅ | - | -| `scope` | Package scope (project or organization) | ❌ | `project` | -| `destination` | Destination path for package download | ❌ | `${{ github.workspace }}\packages` | -| `packageFileName` | Expected package file name pattern (supports `{version}` substitution) | ❌ | - | -| `extractSubFolder` | Subfolder name to create for extraction | ❌ | - | -| `environmentVariable` | Environment variable name to set with extraction path | ❌ | - | - -## Outputs - -| Output | Description | -|--------|-------------| -| `packagePath` | Path where the package was downloaded | -| `extractionPath` | Path where the package was extracted | - -## Prerequisites - -- The `AZURE_DEVOPS_EXT_PAT` secret must be configured in your repository -- The Azure DevOps Personal Access Token must have permissions to read from the specified artifacts feed - -## Usage Examples - -### Basic Package Download - -```yaml -- name: Download Package - uses: ./.github/actions/install-azure-devops-package - with: - feed: 'MyFeed' - package: 'my-package' - version: '1.0.0' - project: 'my-project-id' -``` - -### Download and Extract with Environment Variable - -```yaml -- name: Install DSC Package - uses: ./.github/actions/install-azure-devops-package - with: - feed: 'PowerShell-Universal' - package: 'microsoft.dsc-windows' - version: '3.2.0-preview.6' - project: '2972bb5c-f20c-4a60-8bd9-00ffe9987edc' - packageFileName: 'DSC-{version}-x86_64-pc-windows-msvc.zip' - extractSubFolder: 'DSC' - environmentVariable: 'DSC_ROOT' -``` - -### Using Custom Organization and Destination - -```yaml -- name: Download Package - uses: ./.github/actions/install-azure-devops-package - with: - feed: 'MyFeed' - package: 'my-package' - version: '2.1.0' - organization: 'https://dev.azure.com/myorg/' - project: 'my-project-id' - destination: '${{ github.workspace }}\custom-packages' - scope: 'organization' -``` - -## Environment Setup - -Make sure to set up the Azure DevOps Personal Access Token as a repository secret: - -1. Go to your repository's Settings → Secrets and variables → Actions -1. Add a new secret named `AZURE_DEVOPS_EXT_PAT` -1. Set the value to your Azure DevOps Personal Access Token with appropriate permissions - -## Original Use Case Migration - -The original step from the Windows test action: - -```yaml -- name: Install Universal Package from Azure DevOps Feed - shell: pwsh - env: - AZURE_DEVOPS_EXT_PAT: ${{ secrets.AZURE_DEVOPS_EXT_PAT }} - run: |- - # ... PowerShell script ... -``` - -Can be replaced with: - -```yaml -- name: Install Universal Package from Azure DevOps Feed - uses: ./.github/actions/install-azure-devops-package - with: - feed: 'PowerShell-Universal' - package: 'microsoft.dsc-windows' - version: '3.2.0-preview.6' - project: '2972bb5c-f20c-4a60-8bd9-00ffe9987edc' - packageFileName: 'DSC-{version}-x86_64-pc-windows-msvc.zip' - extractSubFolder: 'DSC' - environmentVariable: 'DSC_ROOT' -``` - -## Error Handling - -The action includes error handling for: - -- Missing Azure DevOps CLI installation -- Failed package downloads -- Missing expected package files -- Directory creation failures -- Archive extraction errors - -## Security Considerations - -- The `AZURE_DEVOPS_EXT_PAT` environment variable is used for authentication -- Ensure your Personal Access Token has minimal required permissions -- Consider using fine-grained tokens scoped to specific feeds and projects diff --git a/.github/actions/install-azure-devops-package/action.yml b/.github/actions/install-azure-devops-package/action.yml deleted file mode 100644 index 23af19c4a77..00000000000 --- a/.github/actions/install-azure-devops-package/action.yml +++ /dev/null @@ -1,153 +0,0 @@ -name: 'Install Azure DevOps Universal Package' -description: 'Downloads and extracts a Universal Package from Azure DevOps Artifacts feed' - -inputs: - feed: - description: 'Azure DevOps Artifacts feed name' - required: true - package: - description: 'Package name to download' - required: true - version: - description: 'Package version to download' - required: true - organization: - description: 'Azure DevOps organization URL' - required: true - default: 'https://dev.azure.com/powershell/' - project: - description: 'Azure DevOps project ID' - required: true - scope: - description: 'Package scope (project or organization)' - required: false - default: 'project' - client-id: - description: 'Azure AD Application (Client) ID for authentication' - required: true - default: '' - tenant-id: - description: 'Azure AD Tenant ID for authentication' - required: true - default: '' - subscription-id: - description: 'Azure Subscription ID for authentication' - required: true - default: '' - destination: - description: 'Destination path for package download' - required: false - default: '${{ github.workspace }}\packages' - packageFileName: - description: 'Expected package file name pattern (supports variable substitution with {version})' - required: false - default: '' - extractSubFolder: - description: 'Subfolder name to create for extraction' - required: false - default: '' - environmentVariable: - description: 'Environment variable name to set with extraction path' - required: false - default: '' - -outputs: - packagePath: - description: 'Path where the package was downloaded' - value: ${{ steps.download.outputs.packagePath }} - extractionPath: - description: 'Path where the package was extracted' - value: ${{ steps.download.outputs.extractionPath }} - -runs: - using: composite - steps: - - name: Azure CLI Login - uses: azure/login@v2 - with: - client-id: ${{ inputs.client-id }} - tenant-id: ${{ inputs.tenant-id }} - subscription-id: ${{ inputs.subscription-id }} - - - name: Download and Extract Azure DevOps Universal Package - id: download - shell: pwsh - env: - AZURE_DEVOPS_EXT_PAT: ${{ env.AZURE_DEVOPS_EXT_PAT }} - run: | - $feed = "${{ inputs.feed }}" - $package = "${{ inputs.package }}" - $version = "${{ inputs.version }}" - $org = "${{ inputs.organization }}" - $project = "${{ inputs.project }}" - $scope = "${{ inputs.scope }}" - $destination = "${{ inputs.destination }}" - $packageFileName = "${{ inputs.packageFileName }}" - $extractSubFolder = "${{ inputs.extractSubFolder }}" - $environmentVariable = "${{ inputs.environmentVariable }}" - - Import-Module ./build.psm1 - Switch-PSNugetConfig -Source Public - - Write-Host "Installing Azure DevOps CLI..." - az extension add --name azure-devops - - Write-Host "Configuring Azure DevOps defaults..." - az devops configure --defaults organization=$org project=$project - - Write-Host "Downloading package: $package version $version from feed: $feed" - az artifacts universal download --feed $feed --name $package --version $version --path $destination --scope $scope - - Write-Host "Downloaded $package version $version to $destination" - - # Set output for package path - echo "packagePath=$destination" >> $env:GITHUB_OUTPUT - - # Handle package extraction if packageFileName is provided - if ($packageFileName) { - # Replace {version} placeholder with actual version - $actualPackageFileName = $packageFileName -replace '\{version\}', $version - $packagePath = Join-Path $destination $actualPackageFileName - - Write-Host "Looking for package file: $actualPackageFileName" - - if (-not (Test-Path -Path $packagePath)) { - throw "Package $actualPackageFileName not found in $destination" - } - - # Determine extraction path - if ($extractSubFolder) { - $extractPath = Join-Path $destination $extractSubFolder - } else { - $extractPath = $destination - } - - # Create extraction directory if it doesn't exist - if (-not (Test-Path -Path $extractPath)) { - New-Item -Path $extractPath -ItemType Directory | Out-Null - } - - if ($packageFileName -like "*.tar.gz") { - Write-Host "Extracting tar.gz package $packagePath to $extractPath" - tar -xvzf $packagePath -C $extractPath - Write-Host "Extracted package to $extractPath" - } - else { - Write-Host "Extracting package $packagePath to $extractPath" - Expand-Archive -Path $packagePath -DestinationPath $extractPath -Force - Write-Host "Extracted package to $extractPath" - } - - # Set output for extraction path - echo "extractionPath=$extractPath" >> $env:GITHUB_OUTPUT - - # Set environment variable if specified - if ($environmentVariable) { - Write-Host "Setting environment variable $environmentVariable to $extractPath" - echo "$environmentVariable=$extractPath" | Out-File -FilePath $env:GITHUB_ENV -Append - } - } else { - echo "extractionPath=$destination" >> $env:GITHUB_OUTPUT - } - - Write-Host "Azure DevOps Universal Package installation completed successfully" diff --git a/.github/actions/test/nix/action.yml b/.github/actions/test/nix/action.yml index b328ba90cd3..d2a608ee2a4 100644 --- a/.github/actions/test/nix/action.yml +++ b/.github/actions/test/nix/action.yml @@ -14,18 +14,6 @@ inputs: required: false default: ctrf type: string - client-id: - description: 'Azure AD Application (Client) ID for authentication' - required: true - default: '' - tenant-id: - description: 'Azure AD Tenant ID for authentication' - required: true - default: '' - subscription-id: - description: 'Azure Subscription ID for authentication' - required: true - default: '' runs: using: composite diff --git a/.github/actions/test/windows/action.yml b/.github/actions/test/windows/action.yml index 75a0985bbe1..2aba39fe35c 100644 --- a/.github/actions/test/windows/action.yml +++ b/.github/actions/test/windows/action.yml @@ -13,15 +13,6 @@ inputs: required: false default: ctrf type: string - client-id: - required: true - type: string - tenant-id: - required: true - type: string - subscription-id: - required: true - type: string runs: using: composite diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index a6a83bef436..2058bd61568 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -111,9 +111,6 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} linux_test_elevated_ci: name: Linux Elevated CI needs: @@ -131,9 +128,6 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} linux_test_unelevated_others: name: Linux Unelevated Others needs: @@ -151,9 +145,6 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} linux_test_elevated_others: name: Linux Elevated Others needs: @@ -171,9 +162,6 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} xunit_tests: name: xUnit Tests needs: diff --git a/.github/workflows/macos-ci.yml b/.github/workflows/macos-ci.yml index a06f12f53c0..2ee96079049 100644 --- a/.github/workflows/macos-ci.yml +++ b/.github/workflows/macos-ci.yml @@ -93,9 +93,6 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} macos_test_elevated_ci: name: macOS Elevated CI needs: @@ -113,9 +110,6 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} macos_test_unelevated_others: name: macOS Unelevated Others needs: @@ -133,9 +127,6 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} macos_test_elevated_others: name: macOS Elevated Others needs: @@ -153,9 +144,6 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} xunit_tests: name: xUnit Tests needs: diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index 8e2e4016f99..582860de34c 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -97,9 +97,6 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: CI - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} windows_test_elevated_ci: name: Windows Elevated CI needs: @@ -117,9 +114,6 @@ jobs: with: purpose: ElevatedPesterTests tagSet: CI - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} windows_test_unelevated_others: name: Windows Unelevated Others needs: @@ -137,9 +131,6 @@ jobs: with: purpose: UnelevatedPesterTests tagSet: Others - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} windows_test_elevated_others: name: Windows Elevated Others needs: @@ -157,9 +148,6 @@ jobs: with: purpose: ElevatedPesterTests tagSet: Others - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} xunit_tests: name: xUnit Tests needs: From 9c8ce18085b0a8b3b907e0ecaf238a0934658f46 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 14:52:01 -0800 Subject: [PATCH 10/20] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/powershell-win-core/powershell-win-core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/powershell-win-core/powershell-win-core.csproj b/src/powershell-win-core/powershell-win-core.csproj index 04ecff48690..e6efeac10f0 100644 --- a/src/powershell-win-core/powershell-win-core.csproj +++ b/src/powershell-win-core/powershell-win-core.csproj @@ -33,7 +33,7 @@ PreserveNewest PreserveNewest - + PreserveNewest PreserveNewest From 350d1881572d4e294fb8d8698281047be4cfa762 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 14:56:25 -0800 Subject: [PATCH 11/20] Update test/powershell/dsc/dsc.profileresource.Tests.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/powershell/dsc/dsc.profileresource.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 index b4b4b579c0d..ee2f0bfce00 100644 --- a/test/powershell/dsc/dsc.profileresource.Tests.ps1 +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -148,7 +148,7 @@ Describe "DSC PowerShell Profile resource elevated tests" -Tag "CI", 'RequireAdm New-Item -Path $testProfilePathAllUsersAllHosts -Value $testProfileContent -Force -ItemType File $originalPath = $env:PATH - $env:PATH += ";$PSHome" + $env:PATH += "$pathSeparator$PSHome" } AfterAll { $env:PATH = $originalPath From e38d5b8618346e7d228dbe8eb5b7f34dcc53dc99 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 14:58:17 -0800 Subject: [PATCH 12/20] Remove -Raw --- dsc/pwsh.profile.resource.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsc/pwsh.profile.resource.ps1 b/dsc/pwsh.profile.resource.ps1 index 3c07f774bc9..c76f8087e20 100644 --- a/dsc/pwsh.profile.resource.ps1 +++ b/dsc/pwsh.profile.resource.ps1 @@ -40,7 +40,7 @@ function PopulatePwshResource { $resource = [PwshResource]::new() $resource.profileType = $profileType - $resource.content = $fileExists ? (Get-Content -Path $profilePath -Raw) : $null + $resource.content = $fileExists ? (Get-Content -Path $profilePath) : $null $resource._exist = $fileExists return $resource From 34fe66c80cdbb93b5137c6c0060f50703574ce10 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 5 Nov 2025 15:02:55 -0800 Subject: [PATCH 13/20] Remove inadvertent change --- .github/actions/test/windows/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/test/windows/action.yml b/.github/actions/test/windows/action.yml index 2aba39fe35c..d2a3bd2f73e 100644 --- a/.github/actions/test/windows/action.yml +++ b/.github/actions/test/windows/action.yml @@ -5,6 +5,7 @@ inputs: purpose: required: false default: '' + type: string tagSet: required: false default: CI From 2b7e95c794b00313310c5f55f239f4971cf946b1 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 7 Nov 2025 17:39:37 -0800 Subject: [PATCH 14/20] CR feedback from Mikey --- dsc/pwsh.profile.dsc.resource.json | 38 ++++--- dsc/pwsh.profile.resource.ps1 | 101 +++++++++++------- .../dsc/dsc.profileresource.Tests.ps1 | 5 + ..._currentuser_currenthost_emptycontent.yaml | 9 ++ 4 files changed, 100 insertions(+), 53 deletions(-) create mode 100644 test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json index 5fb9e514732..6a83b720050 100644 --- a/dsc/pwsh.profile.dsc.resource.json +++ b/dsc/pwsh.profile.dsc.resource.json @@ -60,7 +60,21 @@ ], "properties": { "profileType": { - "$ref": "#/$defs/ProfileType" + "type": "string", + "title": "Profile Type", + "description": "The type of the profile. Can be 'AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserAllHosts', or '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", @@ -69,20 +83,12 @@ }, "_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": { - "ProfileType": { - "type": "string", - "title": "Profile Type", - "description": "The type of the profile. Can be 'AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserAllHosts', or 'CurrentUserCurrentHost'.", - "enum": [ - "AllUsersCurrentHost", - "AllUsersAllHosts", - "CurrentUserAllHosts", - "CurrentUserCurrentHost" - ] - }, "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", @@ -94,6 +100,14 @@ 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" } } } diff --git a/dsc/pwsh.profile.resource.ps1 b/dsc/pwsh.profile.resource.ps1 index c76f8087e20..51e47607a02 100644 --- a/dsc/pwsh.profile.resource.ps1 +++ b/dsc/pwsh.profile.resource.ps1 @@ -20,30 +20,39 @@ enum ProfileType { class PwshResource { [ProfileType] $profileType [string] $content + [string] $profilePath [bool] $_exist [string] ToJson() { return ([ordered] @{ profileType = $this.profileType content = $this.content + profilePath = $this.profilePath + _exist = $this._exist }) | ConvertTo-Json -Compress -EnumsAsStrings } -} -function PopulatePwshResource { - param ( - [ProfileType] $profileType - ) + PwshResource([ProfileType] $profileType, [string] $content, [bool] $_exist) { + $this.profileType = $profileType + $this.content = $content + $this.profilePath = GetProfilePath -profileType $profileType + $this._exist = $_exist + } - $profilePath = GetProfilePath -profileType $profileType - $fileExists = Test-Path $profilePath + PwshResource([ProfileType] $profileType) { + $this.profileType = $profileType + $this.profilePath = GetProfilePath -profileType $profileType - $resource = [PwshResource]::new() - $resource.profileType = $profileType - $resource.content = $fileExists ? (Get-Content -Path $profilePath) : $null - $resource._exist = $fileExists + $fileExists = Test-Path $this.profilePath + if ($fileExists) { + $this.content = Get-Content -Path $this.profilePath + } + else { + $this.content = $null + } - return $resource + $this._exist = $fileExists + } } function GetProfilePath { @@ -62,10 +71,10 @@ function GetProfilePath { } function ExportOperation { - $allUserCurrentHost = PopulatePwshResource -profileType 'AllUsersCurrentHost' - $allUsersAllHost = PopulatePwshResource -profileType 'AllUsersAllHosts' - $currentUserAllHost = PopulatePwshResource -profileType 'CurrentUserAllHosts' - $currentUserCurrentHost = PopulatePwshResource -profileType 'CurrentUserCurrentHost' + $allUserCurrentHost = [PwshResource]::new('AllUsersCurrentHost') + $allUsersAllHost = [PwshResource]::new('AllUsersAllHosts') + $currentUserAllHost = [PwshResource]::new('CurrentUserAllHosts') + $currentUserCurrentHost = [PwshResource]::new('CurrentUserCurrentHost') # Cannot use the ToJson() method here as we are adding a note property $allUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings @@ -76,31 +85,43 @@ function ExportOperation { function GetOperation { param ( - [PwshResource] $InputResource + [Parameter(Mandatory = $true)] + [PwshResource] $InputResource, + [Parameter()] + [switch] $AsJson ) $profilePath = GetProfilePath -profileType $InputResource.profileType.ToString() + $actualState = [PwshResource]::new($InputResource.profileType) + + $actualState.profilePath = $profilePath + $exists = Test-Path $profilePath if ($InputResource._exist -and $exists) { $content = Get-Content -Path $profilePath - $InputResource.Content = $content + $actualState.Content = $content } elseif ($InputResource._exist -and -not $exists) { - $InputResource.Content = $null - $InputResource._exist = $false + $actualState.Content = $null + $actualState._exist = $false } elseif (-not $InputResource._exist -and $exists) { - $InputResource.Content = Get-Content -Path $profilePath - $InputResource._exist = $true + $actualState.Content = Get-Content -Path $profilePath + $actualState._exist = $true } else { - $InputResource.Content = $null - $InputResource._exist = $false + $actualState.Content = $null + $actualState._exist = $false } - $InputResource.ToJson() + if ($AsJson) { + return $actualState.ToJson() + } + else { + return $actualState + } } function SetOperation { @@ -108,42 +129,40 @@ function SetOperation { [PwshResource] $InputResource ) - $profilePath = GetProfilePath -profileType $InputResource.profileType.ToString() - $profileExists = Test-Path $profilePath + $actualState = GetOperation -InputResource $InputResource if ($InputResource._exist) { - if ($InputResource.content) { - Set-Content -Path $profilePath -Value $InputResource.content + if (-not $actualState._exist) { + $null = New-Item -Path $actualState.profilePath -ItemType File -Force } - else { - ## Do nothing if content is not specified + + if ($null -ne $InputResource.content) { + Set-Content -Path $actualState.profilePath -Value $InputResource.content } } - elseif (-not $InputResource._exist -and $profileExists) { - Remove-Item -Path $profilePath -Force - } - elseif (-not $InputResource._exist -and -not $profileExists) { - # Do nothing + elseif ($actualState._exist) { + Remove-Item -Path $actualState.profilePath -Force } } $inputJson = $input | ConvertFrom-Json if ($inputJson) { - $InputResource = [PwshResource]::new() - $InputResource.profileType = $inputJson.profileType - $InputResource.content = $inputJson.content - $InputResource._exist = $inputJson._exist + $InputResource = [PwshResource]::new( $inputJson.profileType, $inputJson.content, $inputJson._exist ) } switch ($Operation) { 'get' { - GetOperation -InputResource $InputResource + GetOperation -InputResource $InputResource -AsJson } 'set' { SetOperation -InputResource $InputResource } 'export' { + if ($inputJson) { + throw "Input is not expected for export operation." + } + ExportOperation } } diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 index ee2f0bfce00..b2839cb24f4 100644 --- a/test/powershell/dsc/dsc.profileresource.Tests.ps1 +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -86,6 +86,11 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent } + It 'DSC resource can set content as empty for current user current host profile' { + $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_currenthost_emptycontent.dsc.yaml -o json) | ConvertFrom-Json + $setOutput.results.result.afterState.content | Should -BeExactly '' + } + It 'DSC resource can set current user all hosts profile' { $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_allhosts.dsc.yaml -o json) | ConvertFrom-Json $expectedContent = "Write-Host 'Welcome to your PowerShell profile - CurrentUserAllHosts!'" diff --git a/test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml b/test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml new file mode 100644 index 00000000000..76439387d2e --- /dev/null +++ b/test/powershell/dsc/psprofile_currentuser_currenthost_emptycontent.yaml @@ -0,0 +1,9 @@ +# Set PowerShell profile content +$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: +- name: PSProfile + type: Microsoft.PowerShell/Profile + properties: + profileType: CurrentUserCurrentHost + content: '' + _exist: true From 3672cd8b25ebffc956d6923d7fb5a36b61319a1b Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 7 Nov 2025 17:40:10 -0800 Subject: [PATCH 15/20] Mark empty content test as pending --- test/powershell/dsc/dsc.profileresource.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/powershell/dsc/dsc.profileresource.Tests.ps1 b/test/powershell/dsc/dsc.profileresource.Tests.ps1 index b2839cb24f4..f361e67ea8e 100644 --- a/test/powershell/dsc/dsc.profileresource.Tests.ps1 +++ b/test/powershell/dsc/dsc.profileresource.Tests.ps1 @@ -86,7 +86,7 @@ Describe "DSC PowerShell Profile Resource Tests" -Tag "CI" { $getOutput.results.result.actualState.content | Should -BeExactly $expectedContent } - It 'DSC resource can set content as empty for current user current host profile' { + It 'DSC resource can set content as empty for current user current host profile' -Pending { $setOutput = (& $dscExe config set --file $PSScriptRoot/psprofile_currentuser_currenthost_emptycontent.dsc.yaml -o json) | ConvertFrom-Json $setOutput.results.result.afterState.content | Should -BeExactly '' } From 4104e3199fb3cd2a3dccaff89e455d8d9514f4d3 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Fri, 7 Nov 2025 17:44:56 -0800 Subject: [PATCH 16/20] Update descriptions --- dsc/pwsh.profile.dsc.resource.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json index 6a83b720050..7fa232ae7a4 100644 --- a/dsc/pwsh.profile.dsc.resource.json +++ b/dsc/pwsh.profile.dsc.resource.json @@ -2,10 +2,10 @@ "$schema": "https://aka.ms/dsc/schemas/v3/bundled/resource/manifest.json", "description": "Manage PowerShell profiles.", "tags": [ - "linux", - "windows", - "macos", - "powershell" + "Linux", + "Windows", + "macOS", + "PowerShell" ], "type": "Microsoft.PowerShell/Profile", "version": "0.1.0", @@ -52,7 +52,7 @@ "embedded": { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "Profile", - "description": "A resource for managing PowerShell profiles.", + "description": "Manage PowerShell profiles.", "type": "object", "unevaluatedProperties": false, "required": [ @@ -62,7 +62,7 @@ "profileType": { "type": "string", "title": "Profile Type", - "description": "The type of the profile. Can be 'AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserAllHosts', or 'CurrentUserCurrentHost'.", + "description": "Defines which profile to manage. Valid values are: 'AllUsersCurrentHost', 'AllUsersAllHosts', 'CurrentUserAllHosts', and 'CurrentUserCurrentHost'.", "enum": [ "AllUsersCurrentHost", "AllUsersAllHosts", @@ -78,7 +78,7 @@ }, "content": { "title": "Content", - "description": "The content of the profile.", + "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": { From cf207dbb47b1f60fed06a0c947ed3e33b0c0276a Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 10 Nov 2025 11:34:14 -0800 Subject: [PATCH 17/20] Update export test --- test/powershell/dsc/psprofile_export.dsc.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/powershell/dsc/psprofile_export.dsc.yaml b/test/powershell/dsc/psprofile_export.dsc.yaml index 356119826c3..cf7881b5df8 100644 --- a/test/powershell/dsc/psprofile_export.dsc.yaml +++ b/test/powershell/dsc/psprofile_export.dsc.yaml @@ -3,7 +3,3 @@ $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json resources: - name: PSProfile type: Microsoft.PowerShell/Profile - properties: - profileType: AllUsersAllHosts - content: "Write-Host 'Welcome to your PowerShell profile - AllUsersAllHosts!'" - _exist: true From 2555ff3adb5e04c4d5027ec2f909f51aa384de4a Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 10 Nov 2025 13:28:50 -0800 Subject: [PATCH 18/20] Ensure v3.2 DSC is installed --- .github/actions/test/nix/action.yml | 2 +- .github/actions/test/windows/action.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/test/nix/action.yml b/.github/actions/test/nix/action.yml index d2a608ee2a4..b8c1778974c 100644 --- a/.github/actions/test/nix/action.yml +++ b/.github/actions/test/nix/action.yml @@ -65,7 +65,7 @@ runs: run: |- Import-Module ./.github/workflows/GHWorkflowHelper/GHWorkflowHelper.psm1 $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" - $latestRelease = $releases | 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 $latestVersion = $latestRelease.tag_name.TrimStart("v") Write-Host "Latest DSC Version: $latestVersion" diff --git a/.github/actions/test/windows/action.yml b/.github/actions/test/windows/action.yml index d2a3bd2f73e..923e9606e59 100644 --- a/.github/actions/test/windows/action.yml +++ b/.github/actions/test/windows/action.yml @@ -48,7 +48,7 @@ runs: run: |- Import-Module .\.github\workflows\GHWorkflowHelper\GHWorkflowHelper.psm1 $releases = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/Dsc/releases" - $latestRelease = $releases | 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 $latestVersion = $latestRelease.tag_name.TrimStart("v") Write-Host "Latest DSC Version: $latestVersion" From 6f55a1a57628895e7db16db04cac34111d5476c5 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Mon, 10 Nov 2025 14:17:50 -0800 Subject: [PATCH 19/20] Add error codes in schema --- dsc/pwsh.profile.dsc.resource.json | 5 +++++ dsc/pwsh.profile.resource.ps1 | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json index 7fa232ae7a4..24ade8bb624 100644 --- a/dsc/pwsh.profile.dsc.resource.json +++ b/dsc/pwsh.profile.dsc.resource.json @@ -48,6 +48,11 @@ ], "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", diff --git a/dsc/pwsh.profile.resource.ps1 b/dsc/pwsh.profile.resource.ps1 index 51e47607a02..d27a79cb544 100644 --- a/dsc/pwsh.profile.resource.ps1 +++ b/dsc/pwsh.profile.resource.ps1 @@ -160,9 +160,12 @@ switch ($Operation) { } 'export' { if ($inputJson) { - throw "Input is not expected for export operation." + Write-Error "Input not supported for export operation" + exit 2 } ExportOperation } } + +exit 0 From 7cec7d020f26c0cea27e87846546bb1af60ff995 Mon Sep 17 00:00:00 2001 From: Aditya Patwardhan Date: Wed, 12 Nov 2025 18:07:04 -0800 Subject: [PATCH 20/20] Address CR feedback from Travis --- dsc/pwsh.profile.dsc.resource.json | 18 +- dsc/pwsh.profile.resource.ps1 | 270 +++++++++++++++-------------- 2 files changed, 151 insertions(+), 137 deletions(-) diff --git a/dsc/pwsh.profile.dsc.resource.json b/dsc/pwsh.profile.dsc.resource.json index 24ade8bb624..cd18e94eec6 100644 --- a/dsc/pwsh.profile.dsc.resource.json +++ b/dsc/pwsh.profile.dsc.resource.json @@ -17,8 +17,10 @@ "-NoProfile", "-ExecutionPolicy", "Bypass", - "-Command", - "$Input | ./pwsh.profile.resource.ps1 -operation 'get'" + "-File", + "./pwsh.profile.resource.ps1", + "-operation", + "get" ], "input": "stdin" }, @@ -30,8 +32,10 @@ "-NoProfile", "-ExecutionPolicy", "Bypass", - "-Command", - "$Input | ./pwsh.profile.resource.ps1 -operation 'set'" + "-File", + "./pwsh.profile.resource.ps1", + "-operation", + "set" ], "input": "stdin" }, @@ -43,8 +47,10 @@ "-NoProfile", "-ExecutionPolicy", "Bypass", - "-Command", - "$Input | ./pwsh.profile.resource.ps1 -operation 'export'" + "-File", + "./pwsh.profile.resource.ps1", + "-operation", + "export" ], "input": "stdin" }, diff --git a/dsc/pwsh.profile.resource.ps1 b/dsc/pwsh.profile.resource.ps1 index d27a79cb544..ad9cfa4a63a 100644 --- a/dsc/pwsh.profile.resource.ps1 +++ b/dsc/pwsh.profile.resource.ps1 @@ -7,165 +7,173 @@ param( [ValidateSet('get', 'set', 'export')] [string]$Operation, [Parameter(ValueFromPipeline)] - $stdinput + [string[]]$UserInput ) -enum ProfileType { - AllUsersCurrentHost - AllUsersAllHosts - CurrentUserAllHosts - CurrentUserCurrentHost -} - -class PwshResource { - [ProfileType] $profileType - [string] $content - [string] $profilePath - [bool] $_exist - - [string] ToJson() { - return ([ordered] @{ - profileType = $this.profileType - content = $this.content - profilePath = $this.profilePath - _exist = $this._exist - }) | ConvertTo-Json -Compress -EnumsAsStrings +Begin { + enum ProfileType { + AllUsersCurrentHost + AllUsersAllHosts + CurrentUserAllHosts + CurrentUserCurrentHost } - PwshResource([ProfileType] $profileType, [string] $content, [bool] $_exist) { - $this.profileType = $profileType - $this.content = $content - $this.profilePath = GetProfilePath -profileType $profileType - $this._exist = $_exist - } + function New-PwshResource { + param( + [Parameter(Mandatory = $true)] + [ProfileType] $ProfileType, - PwshResource([ProfileType] $profileType) { - $this.profileType = $profileType - $this.profilePath = GetProfilePath -profileType $profileType + [Parameter(ParameterSetName = 'WithContent')] + [string] $Content, - $fileExists = Test-Path $this.profilePath - if ($fileExists) { - $this.content = Get-Content -Path $this.profilePath - } - else { - $this.content = $null + [Parameter(ParameterSetName = 'WithContent')] + [bool] $Exist + ) + + # Create the PSCustomObject with properties + $resource = [PSCustomObject]@{ + profileType = $ProfileType + content = $null + profilePath = GetProfilePath -profileType $ProfileType + _exist = $false } - $this._exist = $fileExists - } -} + # 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 + } -function GetProfilePath { - param ( - [ProfileType] $profileType - ) + # 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 + } else { + $resource.content = $null + } + $resource._exist = $fileExists + } - $path = switch ($profileType) { - 'AllUsersCurrentHost' { $PROFILE.AllUsersCurrentHost} - 'AllUsersAllHosts' { $PROFILE.AllUsersAllHosts} - 'CurrentUserAllHosts' { $PROFILE.CurrentUserAllHosts} - 'CurrentUserCurrentHost' { $PROFILE.CurrentUserCurrentHost} + return $resource } - return $path -} - -function ExportOperation { - $allUserCurrentHost = [PwshResource]::new('AllUsersCurrentHost') - $allUsersAllHost = [PwshResource]::new('AllUsersAllHosts') - $currentUserAllHost = [PwshResource]::new('CurrentUserAllHosts') - $currentUserCurrentHost = [PwshResource]::new('CurrentUserCurrentHost') - - # Cannot use the ToJson() method here as we are adding a note property - $allUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings - $allUsersAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings - $currentUserAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings - $currentUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings -} - -function GetOperation { - param ( - [Parameter(Mandatory = $true)] - [PwshResource] $InputResource, - [Parameter()] - [switch] $AsJson - ) - - $profilePath = GetProfilePath -profileType $InputResource.profileType.ToString() + function GetProfilePath { + param ( + [ProfileType] $profileType + ) - $actualState = [PwshResource]::new($InputResource.profileType) - - $actualState.profilePath = $profilePath - - $exists = Test-Path $profilePath + $path = switch ($profileType) { + 'AllUsersCurrentHost' { $PROFILE.AllUsersCurrentHost } + 'AllUsersAllHosts' { $PROFILE.AllUsersAllHosts } + 'CurrentUserAllHosts' { $PROFILE.CurrentUserAllHosts } + 'CurrentUserCurrentHost' { $PROFILE.CurrentUserCurrentHost } + } - if ($InputResource._exist -and $exists) { - $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 - $actualState._exist = $true - } - else { - $actualState.Content = $null - $actualState._exist = $false + return $path } - if ($AsJson) { - return $actualState.ToJson() - } - else { - return $actualState + function ExportOperation { + $allUserCurrentHost = New-PwshResource -ProfileType 'AllUsersCurrentHost' + $allUsersAllHost = New-PwshResource -ProfileType 'AllUsersAllHosts' + $currentUserAllHost = New-PwshResource -ProfileType 'CurrentUserAllHosts' + $currentUserCurrentHost = New-PwshResource -ProfileType 'CurrentUserCurrentHost' + + # Cannot use the ToJson() method here as we are adding a note property + $allUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $allUsersAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'AllUsersAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $currentUserAllHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserAllHosts' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings + $currentUserCurrentHost | Add-Member -NotePropertyName '_name' -NotePropertyValue 'CurrentUserCurrentHost' -PassThru | ConvertTo-Json -Compress -EnumsAsStrings } -} - -function SetOperation { - param ( - [PwshResource] $InputResource - ) - $actualState = GetOperation -InputResource $InputResource - - if ($InputResource._exist) { - if (-not $actualState._exist) { - $null = New-Item -Path $actualState.profilePath -ItemType File -Force + function GetOperation { + param ( + [Parameter(Mandatory = $true)] + $InputResource, + [Parameter()] + [switch] $AsJson + ) + + $profilePath = GetProfilePath -profileType $InputResource.profileType.ToString() + + $actualState = New-PwshResource -ProfileType $InputResource.profileType + + $actualState.profilePath = $profilePath + + $exists = Test-Path $profilePath + + if ($InputResource._exist -and $exists) { + $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 + $actualState._exist = $true + } else { + $actualState.Content = $null + $actualState._exist = $false } - if ($null -ne $InputResource.content) { - Set-Content -Path $actualState.profilePath -Value $InputResource.content + if ($AsJson) { + return $actualState.ToJson() + } else { + return $actualState } } - elseif ($actualState._exist) { - Remove-Item -Path $actualState.profilePath -Force - } -} -$inputJson = $input | ConvertFrom-Json + function SetOperation { + param ( + $InputResource + ) -if ($inputJson) { - $InputResource = [PwshResource]::new( $inputJson.profileType, $inputJson.content, $inputJson._exist ) -} + $actualState = GetOperation -InputResource $InputResource -switch ($Operation) { - 'get' { - GetOperation -InputResource $InputResource -AsJson + if ($InputResource._exist) { + if (-not $actualState._exist) { + $null = New-Item -Path $actualState.profilePath -ItemType File -Force + } + + if ($null -ne $InputResource.content) { + Set-Content -Path $actualState.profilePath -Value $InputResource.content + } + } elseif ($actualState._exist) { + Remove-Item -Path $actualState.profilePath -Force + } } - 'set' { - SetOperation -InputResource $InputResource +} +End { + $inputJson = $input | ConvertFrom-Json + + if ($inputJson) { + $InputResource = New-PwshResource -ProfileType $inputJson.profileType -Content $inputJson.content -Exist $inputJson._exist } - 'export' { - if ($inputJson) { - Write-Error "Input not supported for export operation" - exit 2 + + switch ($Operation) { + 'get' { + GetOperation -InputResource $InputResource -AsJson } + 'set' { + SetOperation -InputResource $InputResource + } + 'export' { + if ($inputJson) { + Write-Error "Input not supported for export operation" + exit 2 + } - ExportOperation + ExportOperation + } } -} -exit 0 + exit 0 +}