Public/Get-ModuleStatus.ps1
|
<# .SYNOPSIS Checks whether a newer version of a module is available in the PowerShell Gallery. .DESCRIPTION The Get-ModuleStatus function compares the installed version of a module with the latest version in the PowerShell Gallery and returns a status object. The gallery is queried at most once per CacheHours (default 24). The result is cached in <ApplicationData>/PowerShell/Config/<ModuleName>/UpdateCheck.json, so importing a module does not make a network call every time. Failed lookups (for example when offline) are cached too, so they are not retried on every import. The function never throws: if the check fails it writes a verbose message and returns a status object with LatestVersion set to $null. Set the TCS_SKIP_UPDATE_CHECK environment variable to 1 to turn the check off (for example in CI). -Force overrides this and the cache. .PARAMETER ShowMessage Writes a warning when an update is available. .PARAMETER ModuleName The name of the module as published in the PowerShell Gallery. .PARAMETER ModulePath The folder that contains the module manifest (<ModuleName>.psd1). .PARAMETER CacheHours How long a gallery result is reused, in hours. 0 always queries the gallery. .PARAMETER Force Queries the gallery now, ignoring the cache and TCS_SKIP_UPDATE_CHECK. .INPUTS None This function does not accept pipeline input. .OUTPUTS PSCustomObject ModuleName, CurrentVersion, LatestVersion, UpdateAvailable, CheckedAt (UTC) and Source ('Gallery', 'Cache' or 'Skipped'). .EXAMPLE Get-ModuleStatus -ModuleName 'tcs.core' -ModulePath (Get-Module tcs.core).ModuleBase -ShowMessage Warns if a newer tcs.core is available, using the cached result if it is less than a day old. .EXAMPLE (Get-ModuleStatus -ModuleName 'tcs.jira' -ModulePath $path -Force).UpdateAvailable Queries the gallery now and returns whether an update is available. .NOTES Author: Nigel Tatschner Company: TheCodeSaiyan Uses Find-PSResource (Microsoft.PowerShell.PSResourceGet) when available, otherwise Find-Module (PowerShellGet). #> function Get-ModuleStatus { [CmdletBinding()] [OutputType([PSCustomObject])] param ( [switch]$ShowMessage, [ValidateNotNullOrEmpty()] [Parameter(Mandatory)] [string]$ModuleName, [ValidateNotNullOrEmpty()] [Parameter(Mandatory)] [string]$ModulePath, [ValidateRange(0, 8760)] [int]$CacheHours = 24, [switch]$Force ) $status = [PSCustomObject]@{ ModuleName = $ModuleName CurrentVersion = $null LatestVersion = $null UpdateAvailable = $false CheckedAt = $null Source = 'Skipped' } try { $manifestPath = Join-Path -Path $ModulePath -ChildPath "$ModuleName.psd1" $status.CurrentVersion = [version](Import-PowerShellDataFile -LiteralPath $manifestPath).ModuleVersion if (-not $Force -and $env:TCS_SKIP_UPDATE_CHECK -in @('1', 'true', 'yes')) { return $status } $cachePath = Join-Path -Path (Join-Path -Path (Get-ModuleConfigRoot) -ChildPath $ModuleName) -ChildPath 'UpdateCheck.json' $cache = $null if (-not $Force -and $CacheHours -gt 0 -and (Test-Path -LiteralPath $cachePath)) { try { $cache = Read-JsonFileAsHashtable -Path $cachePath } catch { Write-Verbose "Ignoring unreadable update cache '$cachePath': $($_.Exception.Message)" } } $lastChecked = $null if ($cache -and $cache['LastChecked']) { # PowerShell 7 turns ISO dates into DateTime; Windows PowerShell leaves them as strings $lastChecked = if ($cache['LastChecked'] -is [datetime]) { $cache['LastChecked'].ToUniversalTime() } else { [datetime]::Parse($cache['LastChecked'], [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal) } } if ($lastChecked -and ([datetime]::UtcNow - $lastChecked).TotalHours -lt $CacheHours) { $status.Source = 'Cache' $status.CheckedAt = $lastChecked if ($cache['LatestVersion']) { $status.LatestVersion = [version]$cache['LatestVersion'] } } else { $status.Source = 'Gallery' $status.CheckedAt = [datetime]::UtcNow try { # Lookup errors are collected, never shown: a missing or offline gallery must not print errors on import $lookupErrors = $null if (Get-Command -Name Find-PSResource -ErrorAction SilentlyContinue) { $found = Find-PSResource -Name $ModuleName -ErrorAction SilentlyContinue -ErrorVariable lookupErrors -WarningAction SilentlyContinue -Verbose:$false 2>$null } else { $found = Find-Module -Name $ModuleName -ErrorAction SilentlyContinue -ErrorVariable lookupErrors -WarningAction SilentlyContinue -Verbose:$false 2>$null } $status.LatestVersion = $found | ForEach-Object { [version]([string]$_.Version -replace '-.*$', '') } | Sort-Object -Descending | Select-Object -First 1 if (-not $status.LatestVersion -and $lookupErrors) { Write-Verbose "Update check for '$ModuleName' failed: $($lookupErrors[0])" } } catch { Write-Verbose "Update check for '$ModuleName' failed: $($_.Exception.Message)" } try { $versionText = if ($status.LatestVersion) { $status.LatestVersion.ToString() } else { $null } Write-JsonFile -Path $cachePath -Data @{ LastChecked = $status.CheckedAt.ToString('o') LatestVersion = $versionText } } catch { Write-Verbose "Could not write update cache '$cachePath': $($_.Exception.Message)" } } $status.UpdateAvailable = [bool]($status.LatestVersion -and $status.CurrentVersion -lt $status.LatestVersion) if ($ShowMessage -and $status.UpdateAvailable) { Write-Warning "An update is available for '$ModuleName'. Installed: $($status.CurrentVersion), latest: $($status.LatestVersion). Run 'Update-Module $ModuleName' (or 'Update-PSResource $ModuleName') to update." } } catch { Write-Verbose "Get-ModuleStatus failed for '$ModuleName': $($_.Exception.Message)" } return $status } |