DevOpsFeedHelper.psm1
<#
.SYNOPSIS Powershell module to support connecting and maintaining connections to DevOps Artifacts Feeds. .DESCRIPTION This module can be used by to help connecting with or maintaining existing DevOps Artifacts Feeds without needing the knowledge of prerequisites. 2021-06-12 Peter de Wit .NOTES This script was created from the steps explained in: https://docs.microsoft.com/en-us/azure/devops/artifacts/tutorials/private-powershell-library?view=azure-devops Keep in mind that this module is created because the current version of PowerShellGet has some limitations with connecting to DevOps Artifacts Feeds. This should be fixed in a later release of PowerShellGet. See notes within function Register-DevOpsFeed. When using msbuild, NuGet.exe and/or dotnet Azure Artifacts Credential Provider is GA: https://github.com/microsoft/artifacts-credprovider #> #Global variables $Script:SourceUri_v2 = "https://pkgs.dev.azure.com/{0}/{1}/_packaging/{2}/nuget/v2/" $Script:SourceUri_v3 = "https://pkgs.dev.azure.com/{0}/{1}/_packaging/{2}/nuget/v3/index.json" $Script:DefaultFeedEnvironmentVariableNameForUser = "DevOpsSharedFeedUser" $Script:DefaultFeedEnvironmentVariableNameForPat = "DevOpsSharedFeedPAT" $Script:DefaultFeedEncryptionEntropy = "6a9909f8-bbf6-48d0-b6fa-ff3543fa4c75" $Script:FeedEncryptionEntropy = $Script:DefaultFeedEncryptionEntropy $Script:ConfigFolder = Join-Path -Path ([Environment]::GetFolderPath("ApplicationData")) -ChildPath "DevOpsFeedHelper" $Script:ConfigPath = Join-Path -Path $ConfigFolder -ChildPath "config.json" $Script:DefaultFeedName = "" $Script:DevOpsFeedContext = [PsCustomObject] @{ FeedName = $Script:DefaultFeedName } <# Custom attribute to easy pass parameters from wrapper functions #> Class PassParameterAttribute : Attribute { [Bool] $PassAlways = $False [String] $PassAs = "" PassParameterAttribute() {} } Function Test-IsWindowsPlatform { If ($PsVersionTable.PSVersion.Major -eq 5) { Return $True } If ($PsVersionTable.Platform -and $PsVersionTable.Platform.StartsWith("Win")) { Return $True } Return $False } Function Set-DevOpsFeedEncryptionEntropy { [CmdletBinding(SupportsShouldProcess)] Param( [Parameter(Mandatory=$True, ValueFromPipeline)] [String] $EncryptionEntropy ) Begin { "Set-DevOpsFeedEncryptionEntropy - START" | Write-Verbose $Value = "" } Process { # Only select last $Value = $EncryptionEntropy } End { Set-Variable -Name "FeedEncryptionEntropy" -Value $Value -Scope Script "Set-DevOpsFeedEncryptionEntropy - END" | Write-Verbose } <# .SYNOPSIS Powershell function to change the encryption entropy used for encrypting and decrypting DevOps Artifacts Feed credentials to local environment settings. .DESCRIPTION This function can be used to change the encryption entropy so that it will be used for encrypting and decrypting DevOps Artifacts Feed credentials within the current session. .OUTPUTS NONE #> } Function ConvertTo-EncryptedStringWithDpApi { [CmdletBinding()] [OutputType([String])] Param( [Parameter(Mandatory=$True, ValueFromPipeline)] [String] $String, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $EncryptionEntropy = ($Script:FeedEncryptionEntropy) ) Begin { "ConvertTo-EncryptedStringWithDpApi - START" | Write-Verbose If (-not(Test-IsWindowsPlatform)) { "ConvertTo-EncryptedStringWithDpApi - Using cryptography functionality is only supported on Windows. Result is NOT encrypted." | Write-Warning } Else { Add-Type -AssemblyName System.Security | Out-Null } } Process { If (-not(Test-IsWindowsPlatform)) { $String } Else { $Bytes = [System.Text.Encoding]::Unicode.GetBytes($String) If ([String]::IsNullOrEmpty($EncryptionEntropy)) { $EntropyBytes = $Null } Else { $EntropyBytes = [System.Text.Encoding]::Unicode.GetBytes($EncryptionEntropy) } $SecureStr = [Security.Cryptography.ProtectedData]::Protect($bytes, $EntropyBytes, [Security.Cryptography.DataProtectionScope]::CurrentUser) $SecureStrBase64 = [System.Convert]::ToBase64String($SecureStr) $SecureStrBase64 } } End { "ConvertTo-EncryptedStringWithDpApi - END" | Write-Verbose } <# .SYNOPSIS Powershell function to encrypt a string by using the local machine key and an optional entropy. .DESCRIPTION This function can be used to encrypt a given string by using strong encryption with the local machine key as se store DevOps Artifacts Feed credentials to the local environment setting in the user scope. If possible the credentials will be encrypted with a local machine key and entropy only known within this module. .OUTPUTS NONE #> } Function ConvertFrom-EncryptedStringWithDpApi { [CmdletBinding()] [OutputType([String])] Param( [Parameter(Mandatory=$True, ValueFromPipeline)] [String] $String, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $EncryptionEntropy = ($Script:FeedEncryptionEntropy) ) Begin { "ConvertFrom-EncryptedStringWithDpApi - START" | Write-Verbose If (-not(Test-IsWindowsPlatform)) { "ConvertFrom-EncryptedStringWithDpApi - Using cryptography functionality is only supported on Windows. Result is NOT decrypted." | Write-Warning } Else { Add-Type -AssemblyName System.Security | Out-Null } } Process { If (-not(Test-IsWindowsPlatform)) { $String } Else { $SecureStr = [System.Convert]::FromBase64String($String) $EntropyBytes = [System.Text.Encoding]::Unicode.GetBytes($EncryptionEntropy) $Bytes = [Security.Cryptography.ProtectedData]::Unprotect($SecureStr, $EntropyBytes, [Security.Cryptography.DataProtectionScope]::LocalMachine) $UnEncryptedString = [System.Text.Encoding]::Unicode.GetString($Bytes) $UnEncryptedString } } End { "ConvertFrom-EncryptedStringWithDpApi - END" | Write-Verbose } <# .SYNOPSIS Powershell function to write DevOps Artifacts Feed credentials to local environment settings. .DESCRIPTION This function can be used to store DevOps Artifacts Feed credentials to the local environment setting in the user scope. If possible the credentials will be encrypted with a local machine key and entropy only known within this module. .OUTPUTS NONE #> } Function Update-PowerShellGetToLatest { [CmdletBinding(SupportsShouldProcess)] Param() $PackageProviders = Get-PackageProvider $NuGetPackageProvider = $PackageProviders | Where-Object { $_.Name -eq "NuGet" } $PsGetPackageProvider = $PackageProviders | Where-Object { $_.Name -eq "PowerShellGet" } If ($Null -ne $PsGetPackageProvider -and $PsGetPackageProvider.Version.Major -ge 2 -and $PsGetPackageProvider.Version.Minor -ge 2) { "PowerShellGet package provider version {0} was found. No action needed." -f $PsGetPackageProvider.Version.ToString() | Write-Host Return; } [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 # If we have NuGet >= 3 installed we are good to go. If ($Null -eq $NuGetPackageProvider -or $NuGetPackageProvider.Version.Major -lt 3) { If ($Null -eq $NuGetPackageProvider) { "Update-PowerShellGetToLatest - NuGet package provider was not found. Installing..." | Write-Verbose } Else { "Update-PowerShellGetToLatest - NuGet package provider version {0} was found. Version >= 3 required. Installing..." -f $NuGetPackageProvider.Version.ToString() | Write-Verbose } If (Test-IsWindowsPlatform -and -not ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { Throw "To install package providers you need to run this script as administrator. Operation canceled." Return; } Install-PackageProvider -Name "NuGet" -Force | Out-Null } Else { "Found package provider 'NuGet' version '{0}'. No action required." -f $NuGetPackageProvider.Version.ToString() | Write-Verbose } Try { Set-PSRepository -Name "PSGallery" -InstallationPolicy "Trusted" -ErrorAction Stop } Catch { Register-PSRepository -Default } "Update-PowerShellGetToLatest - Installing latest version of PowerShellGet..." | Write-Verbose Try { Install-Module -Name PowerShellGet -MinimumVersion 2.2.5 -Force -ErrorAction Stop | Out-Null $Installed = Get-Module -Name "PowerShellGet" -ListAvailable -Refresh | Sort-Object -Property Version -Descending | Select-Object -First 1 "Update-PowerShellGetToLatest - PowerShellGet version '{0}' is installed." -f $Installed.Version.ToString() | Write-Verbose "PowerShellGet installation is completed." | Write-Host } Catch { "Something went wrong when installing latest version of PowerShellGet. Message: {0}" -f $_.ErrorDetails.Message | Write-Error } <# .SYNOPSIS Powershell function to support with upgrading the current PowerShellGet module towards the latest version. .DESCRIPTION This function can be used to install the latest PowerShellGet module with their depencendies. .OUTPUTS NONE #> } Function Remove-DevOpsFeedCredential { [CmdletBinding()] [Alias("Remove-DevOpsCredential")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope='Function')] Param( [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedEnvironmentVariableNameForUser = $($Script:DefaultFeedEnvironmentVariableNameForUser), [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedEnvironmentVariableNameForPat = $($Script:DefaultFeedEnvironmentVariableNameForPat) ) "Remove-DevOpsFeedCredential - Removing local credentials stored within environment settings..." | Write-Verbose [System.Environment]::SetEnvironmentVariable($FeedEnvironmentVariableNameForUser, $Null) [System.Environment]::SetEnvironmentVariable($FeedEnvironmentVariableNameForPat, $Null) If (Test-Path -Path $Script:ConfigPath -PathType "Leaf") { Remove-Item -Path $Script:ConfigPath -Force | Out-Null } "Credentials removed from environment settings and/or local configuration." | Write-Host "Remove-DevOpsFeedCredential - Done." | Write-Verbose <# .SYNOPSIS Powershell function to remove DevOps Artifacts Feed credentials from local environment settings. .DESCRIPTION This function can be used to remove DevOps Artifacts Feed credentials from the local environment setting in the user scope. .OUTPUTS NONE #> } Function Get-DevOpsFeedCredential { [CmdletBinding()] [Alias("Get-DevOpsCredential")] [OutputType([System.Management.Automation.PSCredential])] Param( [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedEnvironmentVariableNameForUser = $($Script:DefaultFeedEnvironmentVariableNameForUser), [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedEnvironmentVariableNameForPat = $($Script:DefaultFeedEnvironmentVariableNameForPat), [Parameter(Mandatory=$False)] [Switch] $NoEncryption ) $EnvValueUser = [System.Environment]::GetEnvironmentVariable($FeedEnvironmentVariableNameForUser) $EnvValuePat = [System.Environment]::GetEnvironmentVariable($FeedEnvironmentVariableNameForPat) If (-not [String]::IsNullOrEmpty($EnvValueUser) -and -not [String]::IsNullOrEmpty(($EnvValuePat))) { If (($NoEncryption.IsPresent -and $True -eq $NoEncryption) -or -not $EnvValuePat.EndsWith("=")) { # Seems like encryption was not used. "Get-DevOpsFeedCredential - PAT token is unencrypted or NoEncryption switch is set." | Write-Verbose } Else { "Get-DevOpsFeedCredential - Looks like the PAT token is encrypted with a local machine key. Decrypting..." | Write-Verbose $EnvValuePat = $EnvValuePat | ConvertFrom-EncryptedStringWithDpApi } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', 'No other option available')] $PatTokenS = ConvertTo-SecureString -String $EnvValuePat -AsPlainText -Force $Cred = New-Object System.Management.Automation.PSCredential($EnvValueUser, $PatTokenS) $Cred Return } ` Else { "Get-DevOpsFeedCredential - No DevOps credentials found within environment variables." | Write-Verbose } If (-not (Test-Path -Path $Script:ConfigPath -PathType "Leaf")) { "Get-DevOpsFeedCredential - No DevOps credentials found within config file." | Write-Verbose Return } $Config = Get-Content -Path $Script:ConfigPath -Raw | ConvertFrom-Json $PatToken = $Config.PatToken If (($NoEncryption.IsPresent -and $True -eq $NoEncryption) -or -not $Config.PatToken.EndsWith("=")) { # Seems like encryption was not used. "Get-DevOpsFeedCredential - PAT token is unencrypted or NoEncryption switch is set." | Write-Verbose } Else { "Get-DevOpsFeedCredential - Looks like the PAT token is encrypted with a local machine key. Decrypting..." | Write-Verbose $PatToken = $PatToken | ConvertFrom-EncryptedStringWithDpApi } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', 'No other option available')] $PatTokenS = ConvertTo-SecureString -String $PatToken -AsPlainText -Force $Cred = New-Object System.Management.Automation.PSCredential($Config.Username, $PatTokenS) $Cred <# .SYNOPSIS Powershell function to retrieve DevOps Artifacts Feed credentials from local environment settings. .DESCRIPTION This function can be used to retrieve DevOps Artifacts Feed credentials from the local environment setting in the user scope. If possible the credentials will be decrypted with a local machine key and entropy only known within this module. .OUTPUTS The credentials found, otherwise $null. #> } Function Set-DevOpsFeedCredential { [CmdletBinding()] [Alias("Set-DevOpsCredential")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope='Function')] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $Username, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $PatToken, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedEnvironmentVariableNameForUser = $($Script:DefaultFeedEnvironmentVariableNameForUser), [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedEnvironmentVariableNameForPat = $($Script:DefaultFeedEnvironmentVariableNameForPat), [Parameter(Mandatory=$False)] [Switch] $NoEncryption, [Parameter(Mandatory=$False)] [Switch] $StoreInEnv ) If ($NoEncryption.IsPresent -and $True -eq $NoEncryption) { "Set-DevOpsFeedCredential - NoEncryption switch was set so PAT token will be stored within environment variable in PLAIN TEXT." | Write-Warning } Else { "Set-DevOpsFeedCredential - Encrypting PAT token with a local machine key..." | Write-Verbose $PatToken = $PatToken | ConvertTo-EncryptedStringWithDpApi } If ($StoreInEnv.IsPresent -and $True -eq $StoreInEnv) { "Set-DevOpsFeedCredential - Writing DevOps credentials to environment variables..." | Write-Verbose [System.Environment]::SetEnvironmentVariable($FeedEnvironmentVariableNameForUser, $Username) [System.Environment]::SetEnvironmentVariable($FeedEnvironmentVariableNameForPat, $PatToken) "DevOps Artifacts Feed Credentials saved in local environment settings." | Write-Host } ` Else { "Set-DevOpsFeedCredential - Writing DevOps credentials to local config '{0}'..." -f $Script:ConfigPath | Write-Verbose New-Item -Path $Script:ConfigFolder -ItemType "Directory" -Force | Out-Null [PSCustomObject] @{ Username = $Username PatToken = $PatToken } | ConvertTo-Json | Out-File -Path $Script:ConfigPath -Encoding "Utf8" -Force } "Set-DevOpsFeedCredential - Done." | Write-Verbose <# .SYNOPSIS Powershell function to write DevOps Artifacts Feed credentials to local environment settings. .DESCRIPTION This function can be used to store DevOps Artifacts Feed credentials to the local environment setting in the user scope. If possible the credentials will be encrypted with a local machine key and entropy only known within this module. .OUTPUTS NONE #> } Function Set-DevOpsFeedContext { [CmdletBinding(SupportsShouldProcess)] [Alias("Set-DevOpsContext")] Param( [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedName ) "Set-DevOpsFeedContext- Saving DevOps Feed context within current session..." | Write-Verbose $NewContext = [PsCustomObject] @{ FeedName = $FeedName } Set-Variable -Name "DevOpsFeedContext" -Value $NewContext -Scope Script "DevOps Feed context set." | Write-Host "Set-DevOpsFeedContext - Done." | Write-Verbose <# .SYNOPSIS Powershell function to setup DevOps Artifacts Feed connection context into the current user session. .DESCRIPTION This function can be used to setup the default DevOps Artifacts Feed connection context that can be used within other functions within this module. .OUTPUTS NONE #> } Function Get-DevOpsFeedContext { [CmdletBinding()] [OutputType([PsCustomObject])] Param() $Script:DevOpsFeedContext <# .SYNOPSIS Powershell function to retrieve default DevOps Artifacts Feed connection context from the current user session. .DESCRIPTION This function can be used to retrieve the default DevOps Artifacts Feed connection context that will be used in other functions within this module. .OUTPUTS NONE #> } Function Unregister-DevOpsFeed { [CmdletBinding()] Param( [Parameter(Mandatory=$False)] [String] $FeedName = $($Script:DevOpsFeedContext.FeedName) ) # If one of the required connection info properties is not found, we cannot proceed. If ([String]::IsNullOrEmpty($FeedName)) { Throw "Feed cannot be null." Return; } # Maybe look at SourceLocation instead of feedname? $Repo = Get-PsRepository -Name $FeedName -ErrorAction SilentlyContinue If ($Null -ne $Repo) { "Unregister-DevOpsFeed - Found repository '{0}'. Unregistering..." -f $Repo.Name | Write-Verbose $Repo | Unregister-PSRepository -ErrorAction SilentlyContinue | Out-Null } Else { "Unregister-DevOpsFeed - No repository found for feed '{0}'." -f $FeedName | Write-Verbose } $PackageSources = Get-PackageSource -Name $FeedName -ErrorAction SilentlyContinue If ($Null -ne $PackageSources) { "Unregister-DevOpsFeed - Found {0} package sources for feed '{1}'. Unregistering..." -f $PackageSources.Count, $FeedName | Write-Verbose $PackageSources | Unregister-PackageSource -ErrorAction SilentlyContinue | Out-Null } Else { "Unregister-DevOpsFeed - No package source found for feed '{0}'." -f $FeedName | Write-Verbose } If ($Null -ne $Repo -or $Null -ne $PackageSources) { "Disconnected from DevOps feed '{0}'" -f $FeedName | Write-Host } <# .SYNOPSIS Powershell function to unregister a connection to a DevOps artifacts feed. .DESCRIPTION This function can be used to unregister a new connection to a DevOps artifacts feed. .OUTPUTS NONE .EXAMPLE PS> Register-DevOpsFeed -FeedName "Feed1" #> } Function Register-DevOpsFeed { [CmdletBinding()] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $OrganizationName, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $ProjectId, [Parameter(Mandatory=$False)] [String] $FeedName = $($Script:DevOpsFeedContext.FeedName), [Parameter(Mandatory=$False)] [Switch] $NonInteractive, [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForUser = $($Script:DefaultFeedEnvironmentVariableNameForUser), [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForPat = $($Script:DefaultFeedEnvironmentVariableNameForPat), [Parameter(Mandatory=$False)] [Switch] $SkipStoreCredentials ) # If one of the required connection info properties is not found, we cannot proceed. If ([String]::IsNullOrEmpty($FeedName)) { Throw "Feed name cannot be null. Consider using function Set-DevOpsFeedContext first." Return; } $FeedCredentialParams = @{ FeedEnvironmentVariableNameForUser = $FeedEnvironmentVariableNameForUser FeedEnvironmentVariableNameForPat = $FeedEnvironmentVariableNameForPat } # I advise to store the PAT (with only read access to feeds) in an user environment variable or local file. $Creds = Get-DevOpsFeedCredential @FeedCredentialParams If ($Null -eq $Creds) { If ($NonInteractive.IsPresent) { Throw "Cannot request credentials in non-interactive mode. Operation canceled." Return; } $Creds = Get-Credential -Message ("Enter your username and PAT token to connect to '{0}' feed within organization '{1}'." -f $FeedName, $OrganizationName) If (-not($Creds)) { Throw "Cannot proceed without credential info". Return; } $Username = $Creds.GetNetworkCredential().UserName $PatToken = $Creds.GetNetworkCredential().Password If (-not $SkipStoreCredentials.IsPresent -or -not $SkipStoreCredentials) { If (-not([String]::IsNullOrEmpty($Username)) -and -not([String]::IsNullOrEmpty($PatToken))) { "Writing feed credentials to environment settings..." | Write-Host Set-DevOpsCredential -Username $Username -PatToken $PatToken @FeedCredentialParams } Else { "Cannot write empty values in environment settings." | Write-Warning } } } # Unregister possible feed info Unregister-DevOpsFeed -FeedName $Feedname -ErrorAction SilentlyContinue $Uri_v2 = $Script:SourceUri_v2 -f $OrganizationName, $ProjectId, $FeedName $Uri_v3 = $Script:SourceUri_v3 -f $OrganizationName, $ProjectId, $FeedName # Below code is commented out because in specific situations NuGet keeps using the device flow. <# "Register-DevOpsFeed - Registering PS repository for feed '{0}' and location '{1}'..." -f $FeedName, $Uri_v2 | Write-Verbose Register-PSRepository -Name $FeedName -InstallationPolicy "Trusted" -SourceLocation $Uri_v2 -PublishLocation $Uri_v2 -Credential $Creds | Out-Null "Register-DevOpsFeed - PS repository for feed '{0}' registered." -f $FeedName | Write-Verbose #> # Register a source for NuGet and for PowershellGet "Register-DevOpsFeed - Registering package source for feed '{0}', provider '{1}', location '{2}'..." -f $FeedName, "NuGet", $Uri_v3 | Write-Verbose Register-PackageSource -Name $FeedName -Location $Uri_v3 -ProviderName "NuGet" -Trusted -SkipValidate -Credential $Creds | Out-Null "Register-DevOpsFeed - Package source for feed '{0}' and provider '{1}' registered." -f $FeedName, "NuGet" | Write-Verbose "Register-DevOpsFeed - Registering package source for feed '{0}', provider '{1}', location '{2}'..." -f $FeedName, "PowershellGet", $Uri_v2 | Write-Verbose Register-PackageSource -Name $FeedName -Location $Uri_v2 -ProviderName "PowerShellGet" -PackageManagementProvider "NuGet" -Trusted -Credential $Creds | Out-Null "Register-DevOpsFeed - Package source for feed '{0}' and provider '{1}' registered." -f $FeedName, "PowershellGet" | Write-Verbose "Connected with DevOps feed '{0}'" -f $FeedName | Write-Host <# .SYNOPSIS Powershell function to register a connection to a DevOps artifacts feed. .DESCRIPTION This function can be used to register a new connection to a DevOps artifacts feed. If particular components are not present they will be installed. To make sure the settings can be configured (and in particular credentials) this function uses environment settings. See NOTES for more information. .OUTPUTS NONE .NOTES As with PowerShellGet version 3 the problems with authentication on the *-Module Cmdlets is solved. At this moment (11-8-2022) this version is still in preview and all cmdlets are rewritten. https://github.com/PowerShell/PowerShellGet/issues/64#issuecomment-1204498111 Another way to keep credentials persistent is by using the Azure Artifacts Credential Provider and env variable VSS_NUGET_EXTERNAL_FEED_ENDPOINTS more info: https://github.com/Microsoft/artifacts-credprovider. I could not get this to work, so I created this module. .EXAMPLE PS> Register-DevOpsFeed -OrganizationName "Contoso" -ProjectId "f3d20478-9f40-4c29-8657-c397d98eea42" -FeedName "Feed1" -NonInteractive #> } Function Find-DevOpsFeedModule { [CmdletBinding()] Param( [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $Name, [Parameter(Mandatory=$False)] [String] $FeedName = $($Script:DevOpsFeedContext.FeedName), [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForUser = $($Script:DefaultFeedEnvironmentVariableNameForUser), [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForPat = $($Script:DefaultFeedEnvironmentVariableNameForPat) ) If ([String]::IsNullOrEmpty($FeedName)) { Throw "FeedName cannot be null. Please provide a FeedName or set DevOps context first through function Set-DevOpsFeedContext." Return; } If ($Null -eq (Get-PSRepository -name $FeedName -ErrorAction SilentlyContinue)) { Throw ("Repository for feed '{0}' is not registered. Please connect with feed first through function Register-DevOpsFeed. Operation canceled." -f $FeedName) Return; } $FeedCredentialParams = @{ FeedEnvironmentVariableNameForUser = $FeedEnvironmentVariableNameForUser FeedEnvironmentVariableNameForPat = $FeedEnvironmentVariableNameForPat } $Creds = Get-DevOpsFeedCredential @FeedCredentialParams If ($Null -eq $Creds) { Throw "No credentials found. Please save credentials first through function Set-DevOpsFeedCredential. Operation canceled." Return; } $FindParameters = @{ Repository = $FeedName Credential = $Creds } If (-not([String]::IsNullOrEmpty($Name))) { $FindParameters.Add("Name", $Name) } Find-Module @FindParameters <# .SYNOPSIS Powershell function that works as a wrapper around Find-Module to find modules from within a DevOps Artifacts feed. .DESCRIPTION This function can be used to find modules within a previous registered DevOps artifacts feed. .OUTPUTS If found, a list of modules. .EXAMPLE PS> Find-DevOpsFeedModule -Name "My.ModuleName" #> } Function Install-DevOpsFeedModule { [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'MinimumVersion', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'MaximumVersion', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'RequiredVersion', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Scope', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Force', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AllowPreRelease', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AcceptLicense', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'SkipPublisherCheck', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AllowClobber', Justification = 'Parameters are read dynamically')] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [PassParameterAttribute()] [String] $Name, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [PassParameterAttribute()] [String] $MinimumVersion, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [PassParameterAttribute()] [String] $MaximumVersion, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [PassParameterAttribute()] [String] $RequiredVersion, [Parameter(Mandatory=$False)] [ValidateSet("AllUsers", "CurrentUser")] [PassParameterAttribute(PassAlways=$True)] [String] $Scope = "CurrentUser", [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $AllowClobber, [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $SkipPublisherCheck, [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $Force, [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $AllowPreRelease, [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $AcceptLicense, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [PassParameterAttribute(PassAlways=$True, PassAs="Repository")] [String] $FeedName = $($Script:DevOpsFeedContext.FeedName), [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForUser = $($Script:DefaultFeedEnvironmentVariableNameForUser), [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForPat = $($Script:DefaultFeedEnvironmentVariableNameForPat) ) If ([String]::IsNullOrEmpty($FeedName)) { Throw "FeedName cannot be null. Please provide a FeedName or set DevOps context first through function Set-DevOpsFeedContext." Return; } If ($Null -eq (Get-PSRepository -name $FeedName -ErrorAction SilentlyContinue)) { Throw ("Repository for feed '{0}' is not registered. Please connect with feed first through function Register-DevOpsFeed. Operation canceled." -f $FeedName) Return; } $FeedCredentialParams = @{ FeedEnvironmentVariableNameForUser = $FeedEnvironmentVariableNameForUser FeedEnvironmentVariableNameForPat = $FeedEnvironmentVariableNameForPat } $Creds = Get-DevOpsFeedCredential @FeedCredentialParams If ($Null -eq $Creds) { Throw "No credentials found. Please save credentials first through function Set-DevOpsFeedCredential. Operation canceled." Return; } $Module = Find-DevOpsFeedModule -Name $Name -FeedName $FeedName If ($Null -eq $Module) { Throw ("Module '{0}' was not found within feed '{1}'. Operation canceled." -f $Name, $FeedName) Return; } $InstallParameters = @{ Credential = $Creds } # We use our PassParameterAttribute to define which parameters we should pass along $LocalVariables = Get-Variable -Scope "Local" $ParameterList = (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters.Values ` | Where-Object { $_.Attributes.TypeId.Name -eq [PassParameterAttribute].Name } ` | Select-Object -Property *, @{ Name="AttributeValue"; Expression = { $_.Attributes | Where-Object { $_.TypeId.Name -eq [PassParameterAttribute].Name} } } -ExcludeProperty "Attributes" ` | Select-Object -Property "Name", "SwitchParameter", ` @{ Name = "PassAlways"; Expression = {$_.AttributeValue.PassAlways} }, ` @{ Name = "PassAs"; Expression = { If ([String]::IsNullOrEmpty($_.AttributeValue.PassAs)) { $_.Name } Else { $_.AttributeValue.PassAs }} }, ` @{ Name = "Value"; Expression = { $ParamName = $_.Name; $LocalVariables | Where-Object { $_.Name -eq $ParamName } | Select-Object -ExpandProperty Value } }, ` @{ Name = "Bound"; Expression = { $PSBoundParameters.ContainsKey($_.Name) } } $ParameterList | Where-Object { $True -eq $_.Bound -or $True -eq $_.PassAlways } | ForEach-Object { $InstallParameters.Add($_.PassAs, $_.Value) } Install-Module @InstallParameters <# .SYNOPSIS Powershell function that works as a wrapper around Install-Module to install modules from a DevOps Artifacts feed. .DESCRIPTION This function can be used to install modules within a previous registered DevOps artifacts feed. .OUTPUTS NONE .EXAMPLE PS> Install-DevOpsFeedModule -Name "My.PrivateModule" -MinimumVersion "1.2.2" #> } Function Update-DevOpsFeedModule { [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'MaximumVersion', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'RequiredVersion', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Scope', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'Force', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AllowPreRelease', Justification = 'Parameters are read dynamically')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'AcceptLicense', Justification = 'Parameters are read dynamically')] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [PassParameterAttribute()] [String] $Name, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [PassParameterAttribute()] [String] $MaximumVersion, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [PassParameterAttribute()] [String] $RequiredVersion, [Parameter(Mandatory=$False)] [ValidateSet("AllUsers", "CurrentUser")] [PassParameterAttribute(PassAlways=$True)] [String] $Scope = "CurrentUser", [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $Force, [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $AllowPreRelease, [Parameter(Mandatory=$False)] [PassParameterAttribute()] [Switch] $AcceptLicense, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $FeedName = $($Script:DevOpsFeedContext.FeedName), [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForUser = $($Script:DefaultFeedEnvironmentVariableNameForUser), [Parameter(Mandatory=$False)] [String] $FeedEnvironmentVariableNameForPat = $($Script:DefaultFeedEnvironmentVariableNameForPat) ) If ([String]::IsNullOrEmpty($FeedName)) { Throw "FeedName cannot be null. Please provide a FeedName or set DevOps context first through function Set-DevOpsFeedContext." Return; } If ($Null -eq (Get-PSRepository -name $FeedName -ErrorAction SilentlyContinue)) { Throw ("Repository for feed '{0}' is not registered. Please connect with feed first through function Register-DevOpsFeed. Operation canceled." -f $FeedName) Return; } $FeedCredentialParams = @{ FeedEnvironmentVariableNameForUser = $FeedEnvironmentVariableNameForUser FeedEnvironmentVariableNameForPat = $FeedEnvironmentVariableNameForPat } $Creds = Get-DevOpsFeedCredential @FeedCredentialParams If ($Null -eq $Creds) { Throw "No credentials found. Please save credentials first through function Set-DevOpsFeedCredential. Operation canceled." Return; } $Module = Find-DevOpsFeedModule -Name $Name -FeedName $FeedName If ($Null -eq $Module) { Throw ("Module '{0}' was not found within feed '{1}'. Operation canceled." -f $Name, $FeedName) Return; } $UpdateParameters = @{ Credential = $Creds } # We use our PassParameterAttribute to define which parameters we should pass along $LocalVariables = Get-Variable -Scope "Local" $ParameterList = (Get-Command -Name $PSCmdlet.MyInvocation.InvocationName).Parameters.Values ` | Where-Object { $_.Attributes.TypeId.Name -eq [PassParameterAttribute].Name } ` | Select-Object -Property *, @{ Name="AttributeValue"; Expression = { $_.Attributes | Where-Object { $_.TypeId.Name -eq [PassParameterAttribute].Name} } } -ExcludeProperty "Attributes" ` | Select-Object -Property "Name", "SwitchParameter", ` @{ Name = "PassAlways"; Expression = {$_.AttributeValue.PassAlways} }, ` @{ Name = "PassAs"; Expression = { If ([String]::IsNullOrEmpty($_.AttributeValue.PassAs)) { $_.Name } Else { $_.AttributeValue.PassAs }} }, ` @{ Name = "Value"; Expression = { $ParamName = $_.Name; $LocalVariables | Where-Object { $_.Name -eq $ParamName } | Select-Object -ExpandProperty Value } }, ` @{ Name = "Bound"; Expression = { $PSBoundParameters.ContainsKey($_.Name) } } $ParameterList | Where-Object { $True -eq $_.Bound -or $True -eq $_.PassAlways } | ForEach-Object { $UpdateParameters.Add($_.PassAs, $_.Value) } Update-Module @UpdateParameters <# .SYNOPSIS Powershell function that works as a wrapper around Install-Module to install modules from a DevOps Artifacts feed. .DESCRIPTION This function can be used to install modules within a previous registered DevOps artifacts feed. .OUTPUTS NONE .EXAMPLE PS> Install-DevOpsFeedModule -Name "My.PrivateModule" -MinimumVersion "1.2.2" #> } Function Find-NuGetExecutable { [Cmdletbinding()] [OutputType([String])] Param() # Try to find NuGet executable from the where statement. $NuGetLocation = (Get-Command -Syntax "nuget.exe" -ErrorAction SilentlyContinue) If (-not [String]::IsNullOrEmpty($NuGetLocation) -and (Test-Path -Path $NuGetLocation -PathType "Leaf" -ErrorAction SilentlyContinue)) { $NuGetLocation = (Resolve-Path -Path $NuGetLocation).Path } # If NuGet wasn't found, we can try to get it from a pipeline environment variable ElseIf (-not [String]::IsNullOrEmpty($env:NUGETEXETOOLPATH) -and (Test-Path -Path $env:NUGETEXETOOLPATH -PathType "Leaf" -ErrorAction SilentlyContinue)) { $NuGetLocation = $env:NUGETEXETOOLPATH } $NuGetLocation <# .SYNOPSIS Finds the nuget.exe executable by searching PATHS. .DESCRIPTION This function can be used to locate the nuget.exe executable. It not found within the PATHS as an alternative it will look at the DevOps pipeline environment variable NUGETEXETOOLPATH. #> } Function New-NuGetPackage { [CmdletBinding(SupportsShouldProcess, ConfirmImpact="Low" )] Param( [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $NuGetSpecFile, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $RootFolder, [Parameter(Mandatory=$True)] [ValidateNotNullOrEmpty()] [String] $OutputFolder, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $Version, [Parameter(Mandatory=$False)] [String[]] $Excludes = @(), [Parameter(Mandatory=$False)] [HashTable] $Properties = @{}, [Parameter(Mandatory=$False)] [ValidateNotNullOrEmpty()] [String] $NuGetExecToolPath = $(Find-NuGetExecutable) ) If ([String]::IsNullOrEmpty($NuGetExecToolPath)) { Throw "Environment variable 'NUGETEXETOOLPATH' is not found and could not locate nuget.exe within PATH. Operation canceled." Return; } $NuGetExecToolPath = Resolve-Path -Path $NuGetExecToolPath If (-not(Test-Path -Path $NuGetExecToolPath -PathType "Leaf")) { Throw "nuget tool was not found at '$($NuGetExecToolPath)'. Operation canceled." Return; } $NuGetSpecFile = Resolve-Path -Path $NuGetSpecFile If (-Not(Test-Path -Path $NuGetSpecFile -PathType "Leaf")) { Throw "Could not find nuspec file '$($NuGetSpecFile)'. Operation canceled." Return; } $RootFolder = Resolve-Path -Path $RootFolder If (-Not(Test-Path -Path $RootFolder -PathType "Container")) { Throw "Root folder '$($RootFolder)' could not be found. Operation canceled." Return; } $OutputFolder = Resolve-Path -Path $OutputFolder If (-Not(Test-Path -Path $OutputFolder -PathType "Container")) { Throw "Output folder '$($OutputFolder)' could not be found. Operation canceled." Return; } $PropertiesText = "" If ($Properties -and $Properties.Count -gt 0) { $PropertiesArray = @() ForEach($Key in $Properties.Keys) { $Value = $Properties[$Key] If ($Null -ne $Value) { $PropertiesArray += "{0}={1}" -f $Key, $Value.ToString() # We only accept strings } } If ($PropertiesArray.Length -gt 0) { $PropertiesText = $PropertiesArray -join ";" } } $CmdParameters = @($NuGetSpecFile, "-OutputDirectory", $OutputFolder, "-BasePath", $RootFolder) If (-not [String]::IsNullOrEmpty($Version)) { $CmdParameters += "-Version", $Version } If (-not [String]::IsNullOrEmpty($PropertiesText)) { $CmdParameters += "-Properties", $PropertiesText } If ($Excludes -and $Excludes.Count -gt 0) { $Excludes | ForEach-Object { $CmdParameters += "-Exclude", $_ } } If ($PSCmdlet.ShouldProcess("nuget.exe pack", ("Creating new nuget package in folder '{0}' from nuspec '{1}'." -f $OutputFolder, $NuGetSpecFile))) { "Executing the following command: `n {0} pack {1}" -f $NuGetExecToolPath, ($CmdParameters -join " ") | Write-Verbose & $NuGetExecToolPath pack $CmdParameters } <# .SYNOPSIS Creates a new NuGet package file from the given nuspec file and by using the nuget executable. .DESCRIPTION This function can be used as an alternative to pipeline tasks or other commands (dotnet pack i.e.) to pack nuget packages. By using this function the consumer has more flexibility of the package before publishing it to private feeds. .PARAMETER NuGetSpecFile The location the to nuget spec file. .PARAMETER RootFolder The root folder of the project. All files within this root will be packed. .PARAMETER OutputFolder The generated package will be saved to this location. .PARAMETER Version OPTIONAL: Use this parameter to overrule the version within the nuspec file (if specified). .PARAMETER Properties OPTIONAL: Use this hashtable to provide additional properties to the nuget executable to fill placeholders within the nuspec file. .PARAMETER NuGetExecToolPath OPTIONAL: Use this parameter if not running from a Azure DevOps pipeline. It needs the exact location of the nuget.exe executable. default: $env:NUGETEXETOOLPATH #> } # Exports Export-ModuleMember -Function "Update-PowerShellGetToLatest" Export-ModuleMember -Function "Set-DevOpsFeedEncryptionEntropy" Export-ModuleMember -Function "Set-DevOpsFeedContext" -Alias "Set-DevOpsContext" Export-ModuleMember -Function "Get-DevOpsFeedContext" -Alias "Get-DevOpsContext" Export-ModuleMember -Function "Remove-DevOpsFeedCredential" -Alias "Remove-DevOpsCredential" Export-ModuleMember -Function "Get-DevOpsFeedCredential" -Alias "Get-DevOpsCredential" Export-ModuleMember -Function "Set-DevOpsFeedCredential" -Alias "Set-DevOpsCredential" Export-ModuleMember -Function "Unregister-DevOpsFeed" Export-ModuleMember -Function "Register-DevOpsFeed" Export-ModuleMember -Function "Find-DevOpsFeedModule" Export-ModuleMember -Function "Install-DevOpsFeedModule" Export-ModuleMember -Function "Update-DevOpsFeedModule" Export-ModuleMember -Function "Find-NuGetExecutable" Export-ModuleMember -Function "New-NuGetPackage" |