Public/InstalledModule/Update-InstalledModule.ps1
# Module: Orbit.Tools # Function: ModuleManagement # Author: Barbara Forbes, David Eberhardt # Updated: 17-SEP 2022 # Status: Live function Update-InstalledModule { <# .SYNOPSIS Updates and reloads modules from a Repository. .DESCRIPTION Updates all local modules that originated from a Repository (PowerShellGallery by default) .PARAMETER Name List of modules to update .PARAMETER Exclude List of modules to exclude from updating. .PARAMETER AllowPrerelease Updates to latest Version including PreReleases (if found) for every Module discovered .PARAMETER SkipMajorVersion Skip major version updates to account for breaking changes. .PARAMETER Scope String. CurrentUser or AllUsers. Default is CurrentUser. .PARAMETER Repository String. If not provided, targets the PowerShell gallery (PSGallery) EXPERIMENTAL. Untested behaviour may occur for custom repositories (Credential Parameter is not parsed, etc.) Please use "Get-PsResource -Name MyModule | Where Repository -eq 'MyRepo' | Update-PsResource" as an alternative .PARAMETER Credential PsCredential. If provided, attaches the Credential on calls to Find-Module and Update-PsResource respectively. EXPERIMENTAL. Functionality is untested (Additional Parameters of *Module-CmdLets are not provided/parsed) .EXAMPLE Update-InstalledModule Updates all Modules to latest version found in Repository PowerShellGallery and installs them in the User Scope .EXAMPLE Update-InstalledModule [-Name] PSReadLine,PowerShellGet -SkipMajorVersion -Scope AllUsers Updates Modules PSReadLine and PowerShellGet to latest version found in Repository PowerShellGallery. Using Switch SkipMajorVersion will only update to the latest minor version currently installed of the module Scope AllUsers requires Administrative Rights. Script will terminate if not run as Administrator .EXAMPLE Update-InstalledModule [-Name] PSReadLine -AllowPrerelease Updates all Modules to latest version found in Repository PowerShellGallery including PreReleases NOTE: If the Name Parameter is not provided this will update ALL Modules to PreReleases found for each! .EXAMPLE Update-InstalledModule -Exclude Az Updates all Modules to latest version found in Repository PowerShellGallery, except Az .EXAMPLE Update-InstalledModule -Repository MyRepo Updates all Modules to latest version found in Repository MyRepo, except Az .EXAMPLE Update-InstalledModule -Repository MyRepo -Credential $MyPsCredential Authenticating against the Repository MyRepo with the provided Credential $MyCredential; Updates all Modules to latest version found in Repository MyRepo, except Az .INPUTS System.String .OUTPUTS System.Void .NOTES Inspired by Barbara Forbes (@ba4bes, https://4bes.nl) 'Update-EveryModule', just separated out into two scripts. This is splitting Update-EveryModule into Update-InstalledModule: Updating modules with options Limit-InstalledModule: Removing old versions except the latest The parameters Repository and Credential are added to allow more flexibility with other repositories. They are currently EXPERIMENTAL and untested. Handle with Care! To avoid having to confirm a Trusted Source, the InstallationPolicy for the Repository can be set to Trusted with: Set-PSRepository -Name "PsGallery" -InstallationPolicy Trusted Set-PSRepository -Name "MyRepo" -InstallationPolicy Trusted .COMPONENT SupportingFunction .FUNCTIONALITY Updates all Modules installed for a given Repository providing some options to control .LINK https://github.com/DEberhardt/Orbit/tree/main/docs/Orbit.Tools/Update-InstalledModule.md .LINK https://github.com/DEberhardt/Orbit/tree/main/docs/Orbit.Tools/Limit-InstalledModule.md .LINK https://github.com/DEberhardt/Orbit/tree/main/docs/about/about_ModuleManagement.md .LINK https://github.com/DEberhardt/Orbit/tree/main/docs/ #> [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'Low')] [Alias('uim')] [OutputType([System.Void])] param ( [Parameter(HelpMessage = 'Modules will be updated')] [System.Collections.Generic.List[object]]$Name = @(), [Parameter(HelpMessage = 'Excluded Modules will not be updated')] [System.Collections.Generic.List[object]]$Exclude = @(), [Parameter()] [Alias('Prerelease')] [switch]$AllowPrerelease, [parameter()] [switch]$SkipMajorVersion, [Parameter()] [ValidateSet('CurrentUser', 'AllUsers')] [String]$Scope = 'CurrentUser', [Parameter()] [String]$Repository = 'PSGallery', [Parameter()] [System.Management.Automation.PSCredential]$Credential = [System.Management.Automation.PSCredential]::Empty ) begin { Show-OrbitFunctionStatus -Level Live Write-Verbose -Message "[BEGIN ] $($MyInvocation.MyCommand)" # Setting Preference Variables according to Upstream settings if (-not $PSBoundParameters['Verbose']) { $VerbosePreference = $PSCmdlet.SessionState.PSVariable.GetValue('VerbosePreference') } if (-not $PSBoundParameters['Debug']) { $DebugPreference = $PSCmdlet.SessionState.PSVariable.GetValue('DebugPreference') } else { $DebugPreference = 'Continue' } if ( $PSBoundParameters['InformationAction']) { $InformationPreference = $PSCmdlet.SessionState.PSVariable.GetValue('InformationAction') } else { $InformationPreference = 'Continue' } #region Helper functions # Testing for Administrator if ( $Scope -eq 'AllUsers' ) { function Test-Administrator { $user = [Security.Principal.WindowsIdentity]::GetCurrent() (New-Object Security.Principal.WindowsPrincipal $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator) } # Check for Admin privileges If (! (Test-Administrator)) { throw "No Administrative privileges. To install in the 'AllUsers' Scope, please run this script as Administrator" } } #endregion #region Validating Requirements - Module and Repository try { $PSResourceGetModuleName = 'Microsoft.PowerShell.PSResourceGet' $PSResourceGetModule = Get-Module -ListAvailable -Name $PSResourceGetModuleName if ( -not $PSResourceGetModule ) { Write-Verbose -Message "This Cmdlet utilises the Module '$PSResourceGetModuleName'. To continue, please install this module" - try { $PSResourceGetModule = (Find-Module $PSResourceGetModuleName -ErrorAction Stop)[0] } catch { $PSResourceGetModule = (Find-Module $PSResourceGetModuleName -AllowPrerelease -ErrorAction Stop)[0] } $PSResourceGetModule | Install-Module -Confirm } $PSResourceGetModule | Import-Module -ErrorAction Stop } catch { if ( -not $_.ErrorRecord -match 'Assembly with same name is already loaded' ) { Write-Error "Module '$PSResourceGetModuleName' could not be loaded properly, please investigate" } } if ( $PSResourceGetModule.PrivateData.PSdata.Prerelease ) { Write-Warning -Message "This Cmdlet utilises the Module '$PSResourceGetModuleName' which currently is installed in a Prerelease version. Please report issues to 'https://github.com/PowerShell/PSResourceGet/issues'" } $PSResourceRepository = Get-PSResourceRepository -Name $Repository if ( -not $PSResourceRepository ) { if ( $Repository -eq 'PSGallery' ) { Register-PSResourceRepository -PSGallery -Trusted Set-PSResourceRepository PSGallery -ApiVersion v2 } else { $URI = Read-Host -Prompt "Repository not registered yet. Enter URI of Repository '$Repository' to register repository" Register-PSResourceRepository -Name $Repository -Trusted -Uri $URI } } elseif ( -not $PSResourceRepository.trusted ) { Write-Warning -Message "Repository $Repository - found but not trusted. To avoid having to confirm installing from this repository every single time, trusting this repository is recommended" Set-PSResourceRepository -Name $Repository -Trusted -Confirm Write-Verbose "PSResourceRepository '$Repository' registered, but not trusted. Trusted flag set - OK" } else { Write-Verbose "PSResourceRepository '$Repository' registered & trusted - OK" } #endregion #region Defining Scope - Query & Filter # Setting Exclusions Write-Verbose "Excluding Modules: $($Exclude -join ', ')" # Preparing Query $GetInstalledPSResource = @{ ErrorAction = 'Stop' } if ( $PSBoundParameters['Name'] ) { Write-Verbose -Message "Querying Modules with Name(s): $($Name -join ', ')" $GetInstalledPSResource.Name = @($Name) } else { Write-Verbose -Message 'Querying all Modules' } # Querying installed Modules $CurrentModules = Get-PSResource @GetInstalledPSResource | Where-Object Repository -EQ $Repository if ( $PSBoundParameters['Exclude'] ) { $CurrentModules = $CurrentModules | Where-Object Name -NotMatch "^$($Exclude -join '|')" } #endregion #region Confirm scope if ( -not $CurrentModules ) { throw 'No Modules have been queried, please investigate with Get-PSResource or Get-Module' } $Count = $(if ($CurrentModules.GetType().BaseType.Name -eq 'Array') { $CurrentModules.Count } else { 1 }) Write-Verbose -Message "Modules found to process: $Count" if ( $Count -gt 10 ) { $go = New-Object System.Management.Automation.Host.ChoiceDescription '&Yes', 'Continue' $goWithConfirm = New-Object System.Management.Automation.Host.ChoiceDescription '&Confirm', 'Continue with Confirm' $abort = New-Object System.Management.Automation.Host.ChoiceDescription '&No', 'Abort' $options = [System.Management.Automation.Host.ChoiceDescription[]]($go, $goWithConfirm, $abort) $title = 'Multiple Modules discovered!' $message = "Detected $Count Modules to process, excluding: $(if ( $Exclude ) { $Exclude -join ', ' } else { 'none' } ) | Proceed?" $result = $host.ui.PromptForChoice($title, $message, $options, 0) switch ($result) { 0 { } 1 { $ConfirmRequired = $true } 2 { break } } } #endregion #region Preparing Splatting parameters $FindModuleParams = @{ ErrorAction = 'Stop' Repository = $Repository } $PSResource = @{ ErrorAction = 'Stop' AcceptLicense = $true Force = $true Scope = $Scope } switch ( $PSBoundParameters ) { 'Credential' { $FindModuleParams.Credential = $PSResource.Credential = $Credential } 'WhatIf' { $PSResource.WhatIf = $true } } if ( $ConfirmRequired -or $PSBoundParameters['Confirm'] ) { $PSResource.Confirm = $True } if ( $PSBoundParameters['AllowPreRelease'] ) { $FindModuleParams.AllowPreRelease = $true $PSResource.Prerelease = $true } #endregion } process { Write-Verbose -Message "[PROCESS] $($MyInvocation.MyCommand)" $CurrentModules | ForEach-Object { $ModuleName = $_.Name [System.Version]$ModuleVersion = ($_.Version.tostring().split('-'))[0] #region Finding latest Module Version try { Write-Verbose "$ModuleName - Checking Repository '$Repository' for latest version" $OnlineModule = Find-Module -Name $ModuleName @FindModuleParams # Finding most recent Minor Version if SkipMajorVersion is used. if ( $SkipMajorVersion.IsPresent -and $OnlineModule.Version.ToString().Split('.')[0] -gt $_.Version.ToString().split('.')[0] ) { Write-Warning "$ModuleName - Found new major version! Online: $($OnlineModule.Version), Local: $($_.Version)" $MaximumVersion = New-Object -TypeName System.Version -ArgumentList ($($_.Version.Split('.')[0]), 999, 999) Write-Verbose "$ModuleName - Checking Repository '$Repository' for latest minor version ($MaximumVersion)" -Verbose $OnlineModule = Find-Module -Name $ModuleName @FindModuleParams -MaximumVersion $MaximumVersion } [System.Version]$OnlineModuleVersion = $OnlineModule.Version.tostring().split('-')[0] } catch { Write-Error "Module $ModuleName not found in Repository: $_" $OnlineModule = $OnlineModuleVersion = $null } #endregion #region Update $LocalModule = Get-Module -Name $ModuleName -ListAvailable -Verbose:$false | Sort-Object Version -Descending | Select-Object -First 1 if ( $LocalModule ) { $LocalModuleVersion = if ( $LocalModule.Count -gt 1 ) { $LocalModule[0].Version } else { $LocalModule.Version } } else { $LocalModuleVersion = $null } Write-Verbose -Message "$ModuleName - Online: $($OnlineModule.Version), Local: $($_.Version) $( if ($LocalModuleVersion) { "| $($LocalModuleVersion)"})" if ( $LocalModuleVersion -eq $OnlineModuleVersion ) { Write-Verbose -Message "$ModuleName - Module is up to date: ($LocalModuleVersion)" } elseif ( $OnlineModuleVersion -gt $ModuleVersion -and $OnlineModuleVersion -gt $LocalModuleVersion ) { # Removing Module if Loaded $Loaded = Get-Module -Name $ModuleName if ( $Loaded ) { Write-Verbose -Message "$ModuleName - Module loaded; Removing Module" Remove-Module -Name $ModuleName -Force -Verbose:$false -ErrorAction SilentlyContinue } #region Updating Module $UpdateInstalled = $false Write-Information "INFO: $ModuleName - Updating Module from $($LocalModuleVersion) to $($OnlineModuleVersion)" try { $PSResource.Name = $ModuleName if ($PSBoundParameters['Debug'] -or $DebugPreference -eq 'Continue') { " Function: $($MyInvocation.MyCommand.Name) - PSResource:", ($PSResource | Format-Table -AutoSize | Out-String).Trim() | Write-Debug " NOTE: Version ($OnlineModuleVersion) is added later as -Version or -RequiredVersion" | Write-Debug } $PSResource.Version = $OnlineModuleVersion try { Update-PSResource @PSResource $UpdateInstalled = $true } catch { # Overcoming halting errors when Module cannot be installed due to a signing issue. #NOTE Parameter Force is not available, or we could add both Skip parameters to the command before execution [string]$ExceptionMessage = $_.Exception.Message if ( $_.Exception.Message -match '-SkipPublisherCheck' ) { Write-Warning -Message "$(($ExceptionMessage -split 'ensure')[0]) - Re-running with -SkipPublisherCheck" try { Update-PSResource @PSResource -SkipPublisherCheck $UpdateInstalled = $true } catch { throw $_ } } } } catch { Write-Error "Update of '$ModuleName' with Update-PSResource failed: $_ " continue } #endregion # Importing module if it was loaded before if ( $Loaded -and $UpdateInstalled ) { Write-Verbose -Message "$ModuleName - Importing Module" try { $null = Import-Module -Name $ModuleName -Verbose:$false -ErrorAction Stop Write-Verbose -Message "$ModuleName - Module re-imported" } catch { try { $null = Import-Module -Name $ModuleName -Verbose:$false -Force -ErrorAction Stop Write-Verbose -Message "$ModuleName - Module re-imported forcefully" } catch { Write-Verbose -Message "$ModuleName - Importing Module failed, please restart session to importing Module" -Verbose } } } #endregion } elseif ($null -ne $OnlineModule) { Write-Verbose -Message "$ModuleName - Module is up to date: $($_.Version)" } #endregion } } #process end { Write-Verbose -Message "[END ] $($MyInvocation.MyCommand)" } #end } # Update-InstalledModule |