PackageUpdateInfo.psm1
$script:ModuleRoot = $PSScriptRoot $script:ModuleVersion = (Import-PowerShellDataFile -Path "$($script:ModuleRoot)\PackageUpdateInfo.psd1").ModuleVersion # Detect whether at some level dotsourcing was enforced if ($PackageUpdateInfo_dotsourcemodule) { $script:doDotSource = $true } else { $script:doDotSource = $false } <# Note on Resolve-Path: All paths are sent through Resolve-Path/Resolve-PSFPath in order to convert them to the correct path separator. This allows ignoring path separators throughout the import sequence, which could otherwise cause trouble depending on OS. Resolve-Path can only be used for paths that already exist, Resolve-PSFPath can accept that the last leaf my not exist. This is important when testing for paths. #> # Detect whether at some level loading individual module files, rather than the compiled module was enforced if ($PackageUpdateInfo_importIndividualFiles) { $importIndividualFiles = $true } else { $importIndividualFiles = $false} if (Test-Path (Resolve-PSFPath -Path "$($script:ModuleRoot)\..\.git" -SingleItem -NewChild)) { $importIndividualFiles = $true } if ("<was compiled>" -eq '<was not compiled>') { $importIndividualFiles = $true } function Import-ModuleFile { <# .SYNOPSIS Loads files into the module on module import. .DESCRIPTION This helper function is used during module initialization. It should always be dotsourced itself, in order to proper function. This provides a central location to react to files being imported, if later desired .PARAMETER Path The path to the file to load .EXAMPLE PS C:\> . Import-ModuleFile -File $function.FullName Imports the file stored in $function according to import policy #> [CmdletBinding()] Param ( [string] $Path ) $resolvedPath = $ExecutionContext.SessionState.Path.GetResolvedPSPathFromPSPath($Path).ProviderPath if ($doDotSource) { . $resolvedPath } else { $ExecutionContext.InvokeCommand.InvokeScript($false, ([scriptblock]::Create([io.file]::ReadAllText($resolvedPath))), $null, $null) } } #region Load individual files if ($importIndividualFiles) { # Execute Preimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\preimport.ps1")) { . Import-ModuleFile -Path $path } # Import all internal functions foreach ($function in (Get-ChildItem "$ModuleRoot\internal\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore)) { . Import-ModuleFile -Path $function.FullName } # Import all public functions $functions = (Get-ChildItem "$ModuleRoot\functions" -Filter "*.ps1" -Recurse -ErrorAction Ignore) foreach ($function in $functions) { . Import-ModuleFile -Path $function.FullName } # Execute Postimport actions foreach ($path in (& "$ModuleRoot\internal\scripts\postimport.ps1")) { . Import-ModuleFile -Path $path } # End it here, do not load compiled code below return } #endregion Load individual files #region Load compiled code # Place all code that should be run before functions are imported here #region try to import module BurntToast, to enable Windows 10 Toast Notifications $optionalModule = @{ ModuleName = 'BurntToast' ModuleVersion = '0.6.3' } $script:EnableToastNotification = $true $moduleToLoad = Get-Module -Name $optionalModule.ModuleName -ListAvailable -Verbose:$false | Where-Object { $_.Version -ge [version]$optionalModule.ModuleVersion } | Sort-Object -Property Version | Select-Object -Last 1 if ($moduleToLoad) { try { $moduleToLoad | Import-Module -ErrorAction Stop } catch { Write-Warning "Fail to import optional module $($moduleToLoad.Name) ($($moduleToLoad.Version)). Unable to show Toast Notifications even when the module is present." $script:EnableToastNotification = $false } } else { if (($PSVersionTable.PSEdition -like "Desktop") -or ($PSVersionTable.PSEdition -like "Core" -and $isWindows)) { if ((Get-CimInstance -ClassName Win32_OperatingSystem -ErrorAction SilentlyContinue).Version -ge [version]'10.0') { Write-Warning "Missing the optional module '$($optionalModule.ModuleName)'. Unable to show Toast Notifications." Write-Warning "It is recommended to install module $($optionalModule.ModuleName) (min. v$($optionalModule.ModuleVersion)) to enable the possibility to enable Windows 10 toast notication for PackageUpdateInfo." Write-Warning "( Install-Module -Name $($optionalModule.ModuleName) -MinimumVersion $($optionalModule.ModuleVersion) )" } } $script:EnableToastNotification = $false } #endregion try to import module BurntToast, to enable Windows 10 Toast Notifications function Get-VersionDifference { <# .SYNOPSIS Get-VersionDifference .DESCRIPTION Subtracts two version objects from each other to get the difference .PARAMETER LowerVersion The module info from the local existing version .PARAMETER HigherVersion The module info from the online existing version .EXAMPLE PS C:\> Get-VersionDifference -LowerVersion "1.0.0.0" -HigherVersion "1.1.2.3" Return 0.1.2.3 as difference between the both versions #> [CmdletBinding()] [OutputType([version])] param ( [version] $LowerVersion, [version] $HigherVersion ) $versionDiffMajor = $HigherVersion.Major - $LowerVersion.Major $versionDiffMinor = $HigherVersion.Minor - $LowerVersion.Minor $versionDiffBuild = $HigherVersion.Build - $LowerVersion.Build $versionDiffRevision = $HigherVersion.Revision - $LowerVersion.Revision if ($versionDiffMajor -lt 0) { $versionDiffMajor = 0 } if ($versionDiffMinor -lt 0) { $versionDiffMinor = 0 } if ($versionDiffBuild -lt 0) { $versionDiffBuild = 0 } if ($versionDiffRevision -lt 0) { $versionDiffRevision = 0 } $versionDiff = [version]::new($versionDiffMajor, $versionDiffMinor, $versionDiffBuild, $versionDiffRevision) $versionDiff } function Show-ToastNotification { <# .SYNOPSIS Create a new Toast Notification from a PackUpdateInfo object .DESCRIPTION Create a new Toast Notification from a PackUpdateInfo object Helper function used for internal commands. .PARAMETER PackageUpdateInfo The PackageUpdate.Info object to show in the toast notification .EXAMPLE PS C:\> Show-ToastNotification -PackageUpdateInfo $PackageUpdateInfo Show Toast Notification on modules with outstanding updates #> [CmdletBinding( DefaultParameterSetName = 'Default', SupportsShouldProcess = $false, ConfirmImpact = 'Low')] [Alias()] [OutputType([PackageUpdate.Info])] param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PackageUpdate.Info[]] $PackageUpdateInfo ) begin { } process { # general toast notification header $toastHeader = New-BTHeader -Id '001' -Title 'PackageUpdateInfo - Update available' -ActivationType Foreground # the text $toastText = @() $toastText += $PackageUpdateInfo.Repository + "\" + $PackageUpdateInfo.Name + " v" + $PackageUpdateInfo.VersionOnline $toastText += "Published: " + $PackageUpdateInfo.PublishedDate.ToString() + "`n" + "Installed version: v" + $PackageUpdateInfo.VersionInstalled + "$(if (-not $PackageUpdateInfo.HasReleaseNotes) { "`n(No release notes available)" })" if ($PackageUpdateInfo.IsCurrentUserPath) { $toastText += "This is a user specific module." } else { $toastText += "This is machine wide module." } # the logo if ($PackageUpdateInfo.IconUri) { $iconPath = Join-Path -Path $script:ModuleTempPath -ChildPath $PackageUpdateInfo.IconUri.Segments[-1] if (Test-Path -Path $iconPath) { Remove-Item -Path $iconPath -Force -ErrorAction:SilentlyContinue } try { Invoke-WebRequest -Uri $PackageUpdateInfo.IconUri -OutFile $iconPath -SkipCertificateCheck -SkipHeaderValidation -ErrorAction Stop $toastLogo = $iconPath } catch { Write-Verbose -Message "Warning! Unable to get icon from '$($PackageUpdateInfo.IconUri)' for module '$($PackageUpdateInfo.Name)'" $toastLogo = $script:ModuleIconPath } } else { $toastLogo = $script:ModuleIconPath } # the buttons $toastButton = @() #$toastButton += New-BTButton -Content 'Install' -Arguments "C:\Windows\notepad.exe" if ($PackageUpdateInfo.HasReleaseNotes) { if ($PackageUpdateInfo.ReleaseNotesIsUri) { $toastButtonArgument = $PackageUpdateInfo.ReleaseNotes } else { $toastButtonArgument = "$($script:ModuleTempPath)\$($PackageUpdateInfo.Name)_v$($PackageUpdateInfo.VersionOnline)_$(Get-Date -Format 'yyyyMMddHHmmssfff').txt" Set-Content -Path $toastButtonArgument -Value $PackageUpdateInfo.ReleaseNotes -Force -Encoding Default } $toastButton += New-BTButton -Content 'Release notes' -Arguments $toastButtonArgument } $toastButton += New-BTButton -Dismiss # create the toast notification $notificationParams = @{ Header = $toastHeader Text = $toastText AppLogo = $toastLogo Button = $toastButton } New-BurntToastNotification @notificationParams } end { } } function Test-UpdateIsNeeded { <# .SYNOPSIS Test-UpdateIsNeeded .DESCRIPTION Command to test if a module version update appears as "update needed". If "needed" depends on the set rules and report-on-different-version parts. .PARAMETER ModuleLocal The module info from the local existing version .PARAMETER ModuleOnline The module info from the online existing version .EXAMPLE PS C:\> Test-UpdateIsNeeded -ModuleLocal $moduleLocal -ModuleOnline $moduleOnline Check if URI is a URI that can be covered for plain text release notes #> [CmdletBinding()] [OutputType([bool])] param ( $ModuleLocal, $moduleOnline ) $versionDiff = Get-VersionDifference -LowerVersion $ModuleLocal.Version -HigherVersion $moduleOnline.Version $name = $ModuleLocal.Name $rules = Get-PackageUpdateRule -IncludeDefaultRule | Sort-Object -Property Id -Descending foreach ($rule in $rules) { Write-Verbose -Message "Working on rule $($rule.Id)" $stop = $false # check for exclude and abort further processing for rule foreach ($exclude in $rule.ExcludeModuleFromChecking) { if ($name -like $exclude) { $stop = $true Write-Verbose -Message "Rule $($rule.Id) does match exclude pattern ($([string]::Join(", ", $rule.ExcludeModuleFromChecking))) for module $($name)" } } if ($stop) { continue } $stop = $true # check for inclue to declare, the rule fits for processing foreach ($include in $rule.IncludeModuleForChecking) { if ($name -like $include) { $stop = $false Write-Verbose -Message "Rule $($rule.Id) does match include pattern ($([string]::Join(", ", $rule.IncludeModuleForChecking))) for module $($name)" } } if ($stop) { continue } # Version checking and return boolean $higherChange = $false if ([bool]$versionDiff.Major) { if ($rule.ReportChangeOnMajor) { Write-Verbose -Message "Major version change in module $($name) found. (Diff: $($versionDiff))" return $true } else { $higherChange = $true } } if ([bool]$versionDiff.Minor) { if ($rule.ReportChangeOnMinor -and (-not $higherChange)) { Write-Verbose -Message "Minor version change in module $($name) found. (Diff: $($versionDiff))" return $true } else { $higherChange = $true } } if ([bool]$versionDiff.Build) { if ($rule.ReportChangeOnBuild -and (-not $higherChange)) { Write-Verbose -Message "Build version change in module $($name) found. (Diff: $($versionDiff))" return $true } else { $higherChange = $true } } if ([bool]$versionDiff.Revision) { if ($rule.ReportChangeOnRevision -and (-not $higherChange)) { Write-Verbose -Message "Revision version change in module $($name) found. (Diff: $($versionDiff))" return $true } else { $higherChange = $true } } # Arriving here means, the name filter apply and version change did not result in status for "report-update-needed" # Function return $false, due to there is an update present, but compairing to the defined rule(s), reporting on the version change is suppressed return $false } } function ConvertFrom-PackageUpdateSetting { <# .SYNOPSIS ConvertFrom-PackageUpdateSetting .DESCRIPTION Convert from a PackageUpdateSetting object to a PSCustomObject .PARAMETER InputObject The PackageUpdateSetting object to convert .PARAMETER AsHashTable Output is done as hashtable, not as PSObject .EXAMPLE PS C:\> ConvertFrom-PackageUpdateSetting -InputObject (Get-PackageUpdateSetting) Check if URI is a URI that can be covered for plain text release notes #> [CmdletBinding()] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "SetBehaviour")] [PackageUpdate.Configuration] $InputObject, [switch] $AsHashTable ) begin { } process { $hash = [ordered]@{ } $notToString = @("System.Boolean", "System.String[]", "System.Int", "PackageUpdate.ModuleRule", "PackageUpdate.ModuleRule[]") foreach ($property in $InputObject.psobject.Properties) { if ($property.TypeNameOfValue -in $notToString) { $hash[$property.Name] = $property.Value } else { $hash[$property.Name] = [String]($property.Value) } } if ($AsHashTable) { $hash } else { [PSCustomObject]$hash } } end { } } function ConvertTo-PackageUpdateSetting { <# .SYNOPSIS ConvertFrom-PackageUpdateSetting .DESCRIPTION Convert from a PackageUpdateSetting object to a PSCustomObject .PARAMETER InputObject PSCustomObject object to convert .EXAMPLE PS C:\> Get-PackageUpdateSetting | ConvertFrom-PackageUpdateSetting | ConvertTo-PackageUpdateSetting Check if URI is a URI that can be covered for plain text release notes #> [CmdletBinding()] [OutputType([PackageUpdate.Configuration])] param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "SetBehaviour")] [PSCustomObject] $InputObject ) begin { } process { $hash = [ordered]@{ } foreach ($property in $InputObject.psobject.Properties) { $hash[$property.Name] = $property.Value } [PackageUpdate.Configuration]$hash } end { } } function Assert-PossibleReleaseNotesURI { <# .SYNOPSIS Assert-PossibleReleaseNotesURI .DESCRIPTION Check if URI is a URI that can be covered for plain text release notes .PARAMETER URI The URI to check .EXAMPLE PS C:\> Assert-PossibleReleaseNotesURI -URI 'https://github.com/AndiBellstedt/PackageUpdateInfo/blob/master/PackageUpdateInfo/changelog.md' Check if URI is a URI that can be covered for plain text release notes #> [CmdletBinding()] [OutputType([bool])] param ( [string] $URI ) if($URI -like "https://github.com*" -or $URI -like "https://raw.githubusercontent.com*" -or $URI -like "https://gitlab.com*") { $true } else { $false } } function Add-PackageUpdateRule { <# .SYNOPSIS Add rule for checking and reporting on installed modules .DESCRIPTION This command allows to declare how a modules is handled in reporting for special. For example, you can configure PackageUpdateINfo to suppress revision updates on a frequent updated module, so that only build, minor or major updates are reportet as "update needed". .PARAMETER Id The Id as an identifier for the rule .PARAMETER ExcludeModuleFromChecking ModuleNames to exclude from update checking .PARAMETER IncludeModuleForChecking ModuleNames to include from update checking By default all modules are included. Default value is: "*" .PARAMETER ReportChangeOnMajor Report when major version changed for a module This means 'Get-PackageUpdateSetting' report update need, only when the major version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 1 0 0 0 .PARAMETER ReportChangeOnMinor Report when minor version changed for a module This means 'Get-PackageUpdateSetting' report update need, only when the minor version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 0 1 0 0 .PARAMETER ReportChangeOnBuild Report when build version changed for a module This means 'Get-PackageUpdateSetting' report update need, when the build version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 0 0 1 0 .PARAMETER ReportChangeOnRevision Report when revision part changed for a module This means 'Get-PackageUpdateSetting' report update need, when the revision version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 1 0 0 0 .PARAMETER SettingObject Settings object parsed in from command Get-PackageUpdateSetting This is an optional parameter. By default it will use the default settings object from the module. .PARAMETER PassThru The rule object will be parsed to the pipeline for further processing .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Add-PackageUpdateRule -IncludeModuleForChecking "MyModule" -ReportChangeOnMajor $true -ReportChangeOnMinor $true -ReportChangeOnBuild $true -ReportChangeOnRevision $false Add a new custom rule for "MyModule" to supress notifications on revision updates of the module #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [Alias('apur')] [OutputType([PackageUpdate.ModuleRule])] Param ( [ValidateRange(1, [int]::MaxValue)] [int] $Id, [Alias("Include", "IncludeModule")] [String[]] $IncludeModuleForChecking, [Alias("Exclude", "ExcludeModule")] [AllowEmptyString()] [String[]] $ExcludeModuleFromChecking, [bool] $ReportChangeOnMajor = $true, [bool] $ReportChangeOnMinor = $true, [bool] $ReportChangeOnBuild = $true, [bool] $ReportChangeOnRevision = $true, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PackageUpdate.Configuration] $SettingObject, [switch] $PassThru ) begin { } process { # If no setting object is piped in, get the current settings if (-not $SettingObject) { $SettingObject = Get-PackageUpdateSetting } if ($Id) { if ($Id -in $SettingObject.CustomRule.Id) { Write-Error -Message "Unable to add rule with Id $($Id), because a rule with this Id already exist." -ErrorAction Stop } } if ($ExcludeModuleFromChecking) { foreach ($item in $SettingObject.CustomRule) { foreach ($toExclude in $ExcludeModuleFromChecking) { if ($toExclude -in $item.ExcludeModuleFromChecking) { Write-Error -Message "Unable to add rule with exclude module '$($toExclude)', because a rule with this module as excluded module already exist." -ErrorAction Stop } } } } if ($IncludeModuleForChecking) { foreach ($item in $SettingObject.CustomRule) { foreach ($toInclude in $IncludeModuleForChecking) { if ($toInclude -in $item.IncludeModuleForChecking) { Write-Error -Message "Unable to add rule with include module '$($toInclude)', because a rule with this module to include already exist." -ErrorAction Stop } } } } $rule = [PackageUpdate.ModuleRule]::new() # Set the rule properties values if ("Id" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting Id: '$($Id)'" $rule.Id = $Id } else { $Id = ($SettingObject.CustomRule.Id | Sort-Object | Select-Object -Last 1) + 1 Write-Verbose "Setting Id: '$($Id)'" $rule.Id = $Id } if ("ExcludeModuleFromChecking" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ExcludeModuleFromChecking: '$([string]::Join(", ", $ExcludeModuleFromChecking))'" $rule.ExcludeModuleFromChecking = $ExcludeModuleFromChecking } if ("IncludeModuleForChecking" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting IncludeModuleForChecking: '$([string]::Join(", ", $IncludeModuleForChecking))'" $rule.IncludeModuleForChecking = $IncludeModuleForChecking } if ("ReportChangeOnMajor" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnMajor: $($ReportChangeOnMajor)" } $rule.ReportChangeOnMajor = $ReportChangeOnMajor if ("ReportChangeOnMinor" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnMinor: $($ReportChangeOnMinor)" } $rule.ReportChangeOnMinor = $ReportChangeOnMinor if ("ReportChangeOnBuild" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnBuild: $($ReportChangeOnBuild)" } $rule.ReportChangeOnBuild = $ReportChangeOnBuild if ("ReportChangeOnRevision" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnRevision: $($ReportChangeOnRevision)" } $rule.ReportChangeOnRevision = $ReportChangeOnRevision $SettingObject.CustomRule += $rule if ($pscmdlet.ShouldProcess($SettingObject.Path, "Add custom ModuleRule")) { $SettingObject | ConvertFrom-PackageUpdateSetting | ConvertTo-Json | Out-File -FilePath $SettingObject.Path -Encoding default -Force } if($PassThru) { [PackageUpdate.ModuleRule]$rule } } end { } } function Export-PackageUpdateInfo { <# .SYNOPSIS Export PackageUpdateInfo to a data file .DESCRIPTION Export PackageUpdateInfo to a data file .PARAMETER InputObject The PackageUpdateInfo from Get-PackageUpdateInfo function. .PARAMETER Path The filepath where to export the infos. Please specify a file as path. Default path value is: Linux: "$HOME/.local/share/powershell/PackageUpdateInfo/PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") Windows: "$HOME\AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") .PARAMETER OutputFormat The output format for the data Available formats are "XML","JSON","CSV" .PARAMETER Encoding File Encoding for the file .PARAMETER Force If the directory for the file is not present, but a directory other then the default is specified, the function will try to create the diretory. .PARAMETER Append The output file will not be replaced. All information will be appended. .PARAMETER IncludeTimeStamp A timestamp will be added to the information records. .PARAMETER PassThru The exported objects will be parsed to the pipeline for further processing. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Get-PackageUpdateInfo | Export-PackageUpdateInfo Example for usage of Export-PackageUpdateInfo #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [Alias('epui')] [OutputType([PackageUpdate.Info])] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PackageUpdate.Info[]] $InputObject, [Parameter(Position = 0)] [Alias("FullName", "FilePath")] [String] $Path, [ValidateSet("XML", "JSON", "CSV")] [Alias("Format")] [String] $OutputFormat = "XML", [ValidateSet("default", "utf7", "utf8", "utf32", "unicode", "ascii", "string", "oem", "bigendianunicode")] [String] $Encoding = "default", [switch] $Force, [switch] $Append, [switch] $IncludeTimeStamp, [switch] $PassThru ) begin { # Set path variable to default value, when not specified if (-not $path) { if ($IsLinux) { $path = (Join-Path $HOME ".local/share/powershell/PackageUpdateInfo/PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") } else { $path = (Join-Path $HOME "AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") } } # If file is specified as path if (Test-Path -Path $Path -PathType Leaf -IsValid) { # If file is present, resolve the path if (Test-Path -Path $Path -PathType Leaf) { $outputPath = Resolve-Path -Path $Path } else { # If Force switch is specified and the path does not exists if ($Force -and (-not (Resolve-Path -Path (Split-Path $Path -ErrorAction SilentlyContinue))) ) { $null = New-Item -ItemType Directory -Path (Split-Path $Path) -ErrorAction Stop } # Try to create the file and resolve the path $outputPath = New-Item -ItemType File -Path $Path -ErrorAction Stop $outputPath = Resolve-Path -Path $outputPath } } elseif (Test-Path -Path $Path -PathType Container) { # If directory is specified as path Write-Error -Message "Specified Path is a directory. Please specify an file." -ErrorAction Stop } else { Write-Error -Message "Specified Path is an invalid directory. Please specify an valid file as output path." -ErrorAction Stop } $output = @() } process { if ($IncludeTimeStamp) { $InputObject | Add-Member -MemberType NoteProperty -Name TimeStamp -Value (Get-Date -Format s) -Force } if ($OutputFormat -in "JSON", "CSV") { $output += foreach ($object in $InputObject) { $hash = [ordered]@{ Name = $object.Name Repository = $object.Repository VersionInstalled = $object.VersionInstalled.ToString() VersionOnline = $object.VersionOnline.ToString() NeedUpdate = $object.NeedUpdate Path = $object.Path ProjectUri = $object.ProjectUri IconUri = $object.IconUri ReleaseNotes = $object.ReleaseNotes Author = $object.Author PublishedDate = ($object.PublishedDate | Get-Date -Format "yyyy-MM-dd HH:mm:ss") Description = $object.Description } if ($IncludeTimeStamp) { $hash.add("TimeStamp", $object.TimeStamp) } New-Object -TypeName psobject -Property $hash } } else { $output += $InputObject } } end { if ($output) { if ($pscmdlet.ShouldProcess($outputPath, "Export PackageUpdateInfo")) { $outFileParams = @{ Encoding = $Encoding } if ($Append -and $OutputFormat -notlike "XML") { $outFileParams.Add("Append", $true) } if ($OutputFormat -in "JSON") { $outFileParams.Add("FilePath", $outputPath.Path) $output | ConvertTo-Json | Out-File @outFileParams } elseif ($OutputFormat -in "CSV") { $outFileParams.Add("Path", $outputPath.Path) $outFileParams.Add("Delimiter", ';') $outFileParams.Add("NoTypeInformation", $true) $output | Export-Csv @outFileParams } else { $Exportdata = if ($Append -and ((Get-ChildItem -Path $outputPath.Path).Length -gt 0) ) { Import-Clixml -Path $outputPath.Path -ErrorAction SilentlyContinue } else { @() } $Exportdata += $output $Exportdata | Export-Clixml -Path $outputPath.Path -Encoding $Encoding } } if ($PassThru) { $output | ForEach-Object { [PackageUpdate.Info]$_ } } } else { Write-Verbose -Message "No data were processed, nothing to output." "" | Out-File $outputPath.Path -Encoding $Encoding } } } function Get-PackageUpdateInfo { <# .SYNOPSIS Get info about up-to-dateness for installed modules .DESCRIPTION Get-PackageUpdateInfo query locally installed modules and compare them against the online versions for up-to-dateness .PARAMETER Name The name of the module to check .PARAMETER Repository The repository to check .PARAMETER ShowOnlyNeededUpdate This switch suppresses up-to-date modules from the output. .PARAMETER ShowToastNotification This switch invokes nice Windows-Toast-Notifications with release note information on modules with update needed. .PARAMETER CurrentUser Only look for modules in the current user profile. This is helpfully if you're running without admin right, which you should always do as your default work preference. .PARAMETER AllUsers Only look for modules in the AllUsers/system directories. Keep in mind, that admin rights are required to update those modules. .PARAMETER Force Force to query info about up-to-dateness for installed modules, even if the UpdateCheckInterval from last check is not expired. .EXAMPLE PS C:\> Get-PackageUpdateInfo Outputs update information for all modules (currentUser and AllUsers). Output can look like: Name Repository VersionInstalled VersionOnline NeedUpdate Path ---- ---------- ---------------- ------------- ---------- ---- PSReadline PSGallery 1.2 1.2 False C:\Program Files\WindowsPowerShell\Modules\PSReadline Pester PSGallery 4.4.0 4.4.2 True C:\Program Files\WindowsPowerShell\Modules\Pester .EXAMPLE PS C:\> Get-PackageUpdateInfo -ShowOnlyNeededUpdate This will filter output to show only modules where NeedUpdate is True Output can look like: Name Repository VersionInstalled VersionOnline NeedUpdate Path ---- ---------- ---------------- ------------- ---------- ---- Pester PSGallery 4.4.0 4.4.2 True C:\Program Files\WindowsPowerShell\Modules\Pester .EXAMPLE PS C:\> "Pester", "PSReadline" | Get-PackageUpdateInfo Pipeline is supported. This returns the infos only for the two modules "Pester", "PSReadline" This also can be done with Get-Module cmdlet: Get-Module "Pester", "PSReadline" | Get-PackageUpdateInfo #> [CmdletBinding( DefaultParameterSetName = 'DefaultSet1', SupportsShouldProcess = $false, ConfirmImpact = 'Low')] [Alias('gpui')] [OutputType([PackageUpdate.Info])] Param ( [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Name, [string[]] $Repository, [switch] $ShowOnlyNeededUpdate, [switch] [Alias('ToastNotification', 'Notify')] $ShowToastNotification, [Parameter(ParameterSetName = 'CurrentUser')] [switch] $CurrentUser, [Parameter(ParameterSetName = 'AllUsers')] [switch] $AllUsers, [switch] $Force ) begin { if ($ShowToastNotification -and (-not $script:EnableToastNotification)) { Write-Verbose -Message "System is not able to do Toast Notifications" -Verbose } # Doing checks if the update check against the modules is done $moduleSetting = Get-PackageUpdateSetting if (-not $Force) { if ($moduleSetting.LastCheck -gt $moduleSetting.LastSuccessfulCheck) { $effectiveCheckDate = $moduleSetting.LastCheck } else { $effectiveCheckDate = $moduleSetting.LastSuccessfulCheck } if ( ($effectiveCheckDate + $moduleSetting.UpdateCheckInterval) -ge (Get-Date) ) { Write-Warning -Message "Skip checking for updates on modules, due to last check happens at $($effectiveCheckDate.ToShortTimeString()) and minimum UpdateCheckInterval is set to $($moduleSetting.UpdateCheckInterval)" break } } else { Write-Verbose -Message "Force parameter specified. Bypassing checking on UpdateCheckInterval and enforcing up-to-dateness check on modules" } Set-PackageUpdateSetting -LastCheck (Get-Date) # Get the necessary repositories $getPSRepositoryParams = @{ } if ($Repository) { $getPSRepositoryParams.Add("Name", $Repository) } $psRepositories = Get-PSRepository @getPSRepositoryParams -ErrorAction Stop } process { if (-not $Name) { $Name = "*" } foreach ($nameItem in $Name) { # Get local module(s) Write-Verbose "Get local module(s): $($nameItem)" $getModuleParams = @{ ListAvailable = $true Name = $nameItem Verbose = $false } $modulesLocal = Get-Module @getModuleParams | Where-Object RepositorySourceLocation | Sort-Object Name, Version -Descending | Group-Object Name | ForEach-Object { $_.group[0] } # Filtering out if switches are specified Write-Verbose "Do the filtering..." if ($CurrentUser) { $modulesLocal = foreach ($path in $CurrentUserModulePath) { $modulesLocal | Where-Object path -Like "$($path)*" } } if ($AllUsers) { $modulesLocal = foreach ($path in $AllUsersModulePath) { $modulesLocal | Where-Object path -Like "$($path)*" } } if ($Repository) { $modulesLocal = foreach ($psRepository in $psRepositories) { $modulesLocal | Where-Object RepositorySourceLocation -like "$($psRepository.SourceLocation)*" } } # Get available modules from online repositories Write-Verbose "Get available modules from online repositories" $modulesOnline = foreach ($moduleLocalName in $modulesLocal.Name) { $findModuleParams = @{ "Name" = $moduleLocalName "Verbose" = $false } if ($Repository) { $findModuleParams["Repository"] = $Repository } Find-Module @findModuleParams } # Compare the version and create output Write-Verbose "Compare the version and create output" foreach ($moduleOnline in $modulesOnline) { $moduleLocal = $modulesLocal | Where-Object Name -like $moduleOnline.Name if ([version]($moduleOnline.version) -gt [version]($moduleLocal.version)) { Write-Verbose "Update available for module '$($moduleOnline.Name)': local v$($moduleLocal.version) --> v$($moduleOnline.version) online" $UpdateAvailable = Test-UpdateIsNeeded -ModuleLocal $moduleLocal -ModuleOnline $moduleOnline #$UpdateAvailable = $true } elseif ([version]($moduleOnline.version) -lt [version]($moduleLocal.version)) { Write-Warning "Local version for module '$($moduleOnline.Name)' is higher than online version: local v$($moduleLocal.version) <-- v$($moduleOnline.version) online" $UpdateAvailable = $false } else { Write-Verbose "The module '$($moduleOnline.Name)' is up to date (Version $($moduleLocal.version))" $UpdateAvailable = $false } if ($ShowOnlyNeededUpdate -and (-not $UpdateAvailable)) { continue } $outputHash = [ordered]@{ Name = $moduleLocal.Name Repository = ($psRepositories | Where-Object SourceLocation -like "$($moduleLocal.RepositorySourceLocation.ToString().Trim('/'))*").Name VersionInstalled = $moduleLocal.version VersionOnline = $moduleOnline.version NeedUpdate = $UpdateAvailable Path = $moduleLocal.ModuleBase.Replace($moduleLocal.Version, '').trim('\') ProjectUri = $moduleOnline.ProjectUri IconUri = $moduleOnline.IconUri ReleaseNotes = $moduleOnline.ReleaseNotes Author = $moduleOnline.Author PublishedDate = $moduleOnline.PublishedDate Description = $moduleOnline.Description } $PackageUpdateInfo = New-Object -TypeName PackageUpdate.Info -Property $outputHash if ($script:EnableToastNotification -and $ShowToastNotification -and $PackageUpdateInfo.NeedUpdate) { Show-ToastNotification -PackageUpdateInfo $PackageUpdateInfo } $PackageUpdateInfo } } } end { Set-PackageUpdateSetting -LastSuccessfulCheck (Get-Date) } } function Get-PackageUpdateRule { <# .SYNOPSIS Get rule(s) for checking and reporting on installed modules .DESCRIPTION This command get the existing custom rule(s) how modules are handled in reporting. .PARAMETER Id The Id as an identifier for the rule .PARAMETER ExcludeModuleFromChecking ModuleNames to exclude from update checking .PARAMETER IncludeModuleForChecking ModuleNames to include from update checking By default all modules are included. Default value is: "*" .PARAMETER IncludeDefaultRule Outputs the DefautRule from the setting object, in addition to the customrules .PARAMETER SettingObject Settings object parsed in from command Get-PackageUpdateSetting This is an optional parameter. By default it will use the default settings object from the module. .EXAMPLE PS C:\> Get-PackageUpdateRule Get all the existing custom rules .EXAMPLE PS C:\> Get-PackageUpdateRule -Id 1 Get all the custom rule with Id 1 #> [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'Low', DefaultParameterSetName = "ShowAll")] [Alias('gpur')] [OutputType([PackageUpdate.ModuleRule])] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ById")] [ValidateRange(1, [int]::MaxValue)] [int[]] $Id, [Parameter(ParameterSetName = "ShowAll")] [Alias("Include", "IncludeModule")] [String] $IncludeModuleForChecking, [Parameter(ParameterSetName = "ShowAll")] [Alias("Exclude", "ExcludeModule")] [String] $ExcludeModuleFromChecking, [Parameter(ParameterSetName = "ShowAll")] [switch] $IncludeDefaultRule, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PackageUpdate.Configuration] $SettingObject ) begin { } process { # If no setting object is piped in, get the current settings if (-not $SettingObject) { $SettingObject = Get-PackageUpdateSetting } $output = @() if ($Id) { $output += $SettingObject.CustomRule | Where-Object Id -in $Id | Sort-Object -Property Id } else { if ("ExcludeModuleFromChecking" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { $output += $SettingObject.CustomRule | Where-Object ExcludeModuleFromChecking -like $ExcludeModuleFromChecking | Sort-Object -Property Id } elseif ("IncludeModuleForChecking" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { $output += $SettingObject.CustomRule | Where-Object IncludeModuleForChecking -like $IncludeModuleForChecking | Sort-Object -Property Id } else { $output += $SettingObject.CustomRule | Sort-Object -Property Id } } if ($IncludeDefaultRule) { $output += $SettingObject.DefaultRule } $output } end { } } function Get-PackageUpdateSetting { <# .SYNOPSIS Set behaviour settings for PackageUpdateInfo module .DESCRIPTION Query the basic settings for check and report on up-to-dateness information on installed modules .PARAMETER Path The filepath where to setting file This is optional, default path value is: Linux: "$HOME/.local/share/powershell/PackageUpdateInfo/PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).json") Windows: "$HOME\AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).json") .EXAMPLE PS C:\> Get-PackageUpdateSetting Get the current settings on PackageUpdateInfo behaviour. #> [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'Low')] [Alias('gpus')] [OutputType([PackageUpdate.Configuration])] Param ( [Parameter(Position = 0)] [Alias("FullName", "FilePath")] [String] $Path = $script:ModuleSettingPath ) begin { } process { # read settings file try { $configuration = Get-Content -Path $Path -ErrorAction Stop | ConvertFrom-Json -ErrorAction Stop } catch { Write-Warning -Message "Module configuration file not found! ($($Path))" Write-Warning -Message "Please check the path or initialize configuration by using 'Set-PackageUpdateSetting -Reset'" throw } # Initialize setting object and fill in values $output = New-Object -TypeName PackageUpdate.Configuration $output.CustomRule = foreach ($rule in $configuration.CustomRule) { [PackageUpdate.ModuleRule]@{ Id = $rule.Id ExcludeModuleFromChecking = $rule.ExcludeModuleFromChecking IncludeModuleForChecking = $rule.IncludeModuleForChecking ReportChangeOnBuild = $rule.ReportChangeOnBuild ReportChangeOnMajor = $rule.ReportChangeOnMajor ReportChangeOnMinor = $rule.ReportChangeOnMinor ReportChangeOnRevision = $rule.ReportChangeOnRevision } } $output.DefaultRule = [PackageUpdate.ModuleRule]@{ ExcludeModuleFromChecking = $configuration.DefaultRule.ExcludeModuleFromChecking IncludeModuleForChecking = $configuration.DefaultRule.IncludeModuleForChecking ReportChangeOnBuild = $configuration.DefaultRule.ReportChangeOnBuild ReportChangeOnMajor = $configuration.DefaultRule.ReportChangeOnMajor ReportChangeOnMinor = $configuration.DefaultRule.ReportChangeOnMinor ReportChangeOnRevision = $configuration.DefaultRule.ReportChangeOnRevision } if ("System.TimeSpan" -in $configuration.UpdateCheckInterval.psobject.TypeNames) { $output.UpdateCheckInterval = [timespan]::new($configuration.UpdateCheckInterval.Days, $configuration.UpdateCheckInterval.Hours, $configuration.UpdateCheckInterval.Minutes, $configuration.UpdateCheckInterval.Seconds, $configuration.UpdateCheckInterval.Milliseconds) } else { $output.UpdateCheckInterval = [timespan]$configuration.UpdateCheckInterval } if ($configuration.LastCheck) { $output.LastCheck = [datetime]$configuration.LastCheck } else { $output.LastCheck = [datetime]::MinValue } if ($configuration.LastSuccessfulCheck) { $output.LastSuccessfulCheck = [datetime]$configuration.LastSuccessfulCheck } else { $output.LastSuccessfulCheck = [datetime]::MinValue } $output.Path = $configuration.Path $output } end { } } function Import-PackageUpdateInfo { <# .SYNOPSIS Import PackageUpdateInfo from a data file .DESCRIPTION Import PackageUpdateInfo from a data file previously exported with funtion Export-PackageUpdateInfo. .PARAMETER Path The filepath where to import the informations. Please specify a file as path. Default path value is: Linux: "$HOME/.local/share/powershell/PackageUpdateInfo/PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") Windows: "$HOME\AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") .PARAMETER ShowToastNotification This switch invokes nice Windows-Toast-Notifications with release note information on modules with update needed. .PARAMETER InputFormat The output format for the data Available formats are "XML","JSON","CSV" .PARAMETER Encoding File Encoding for the file .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Import-PackageUpdateInfo Try to import the default file "$HOME\AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml" #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [Alias('ipui')] [OutputType([PackageUpdate.Info])] Param ( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [Alias("FullName", "FilePath")] [String] $Path, [switch] [Alias('ToastNotification', 'Notify')] $ShowToastNotification, [ValidateSet("XML", "JSON", "CSV")] [Alias("Format")] [String] $InputFormat = "XML", [ValidateSet("default", "utf7", "utf8", "utf32", "unicode", "ascii", "string", "oem", "bigendianunicode")] [String] $Encoding = "default" ) begin { if($ShowToastNotification -and (-not $script:EnableToastNotification)) { Write-Verbose -Message "System is not able to do Toast Notifications" -Verbose } # Set path variable to default value, when not specified if(-not $path) { if($IsLinux) { $path = (Join-Path $HOME ".local/share/powershell/PackageUpdateInfo/PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") } else { $path = (Join-Path $HOME "AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateInfo_$($PSEdition)_$($PSVersionTable.PSVersion.Major).xml") } } } process { $file = Get-ChildItem -Path $Path -ErrorAction Stop if ($pscmdlet.ShouldProcess($Path, "Import PackageUpdateInfo")) { if ($file.Length -gt 2) { Write-Verbose "Importing package update information from $($file.FullName)" if ($InputFormat -like "JSON") { $records = Get-Content -Path $file -Encoding $Encoding | ConvertFrom-Json -ErrorAction SilentlyContinue } elseif ($InputFormat -like "CSV") { $records = Import-Csv -Path $file -Delimiter ";" -Encoding $Encoding } else { $records = Import-Clixml -Path $file } foreach ($record in $records) { $_date = $record.PublishedDate | Get-Date $hash = [ordered]@{ Name = $record.Name Repository = $record.Repository VersionInstalled = [version]$record.VersionInstalled VersionOnline = [version]$record.VersionOnline NeedUpdate = [bool]::Parse($record.NeedUpdate) Path = $record.Path ProjectUri = $record.ProjectUri IconUri = $record.IconUri ReleaseNotes = $record.ReleaseNotes Author = $record.Author PublishedDate = $_date Description = $record.Description } $PackageUpdateInfo = [PackageUpdate.Info]$hash if ($script:EnableToastNotification -and $ShowToastNotification -and $PackageUpdateInfo.NeedUpdate) { Show-ToastNotification -PackageUpdateInfo $PackageUpdateInfo } $PackageUpdateInfo } } else { Write-Verbose "Nothing to import. Seems like, all is up to date." } } } end { } } function Remove-PackageUpdateRule { <# .SYNOPSIS remove rule(s) for checking and reporting on installed modules .DESCRIPTION This command remove existing custom rule(s) on how a modules is handled in reporting. .PARAMETER Id The Id of the rule to be removed .PARAMETER InputObject Settings object parsed in from command Get-PackageUpdateSetting This is an optional parameter .PARAMETER Force If specified the user will not prompted on confirmation. .PARAMETER PassThru The rule object will be parsed to the pipeline for further processing .PARAMETER SettingObject Settings object from command Get-PackageUpdateSetting This is an optional parameter. By default it will use the default settings object from the module. .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Get-PackageUpdateRule | Remove-PackageUpdateRule Remove all custom rules for module update handling. #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High', DefaultParameterSetName = "ById")] [Alias('rpur')] [OutputType([PackageUpdate.ModuleRule])] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ById")] [ValidateRange(1, [int]::MaxValue)] [int[]] $Id, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ByInputObject")] [PackageUpdate.ModuleRule[]] $InputObject, [switch] $Force, [switch] $PassThru, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PackageUpdate.Configuration] $SettingObject ) begin { } process { # If no setting object is piped in, get the current settings if (-not $SettingObject) { $SettingObject = Get-PackageUpdateSetting } # Find the rule by Id if ($id) { $InputObject = Get-PackageUpdateRule -Id $id } # Remove the rule(s) from the setting object foreach ($rule in $InputObject) { Write-Verbose -Message "Remove rule Id $($rule.Id)" $SettingObject.CustomRule = $SettingObject.CustomRule | Where-Object Id -ne $rule.id } # Write back the configuration object if ($Force) { $doAction = $true } else { $doAction = $pscmdlet.ShouldProcess("Rule Id $([string]::Join(", ", $rule.id))", "Remove from PackUpdateSetting object ($($SettingObject.Path))") } if ($doAction) { $SettingObject | ConvertFrom-PackageUpdateSetting | ConvertTo-Json | Out-File -FilePath $SettingObject.Path -Encoding default -Force } # Output the object if PassThru is specified if ($PassThru) { $InputObject } } end { } } function Set-PackageUpdateRule { <# .SYNOPSIS Set a rule for checking and reporting on installed modules .DESCRIPTION This command allows to edit existing rules on how a modules is handled in reporting. For example, you can configure PackageUpdateINfo to suppress revision updates on a frequent updated module, so that only build, minor or major updates are reportet as "update needed". .PARAMETER Id The Id as an identifier for the rule .PARAMETER InputObject The rule object to modify .PARAMETER ExcludeModuleFromChecking ModuleNames to exclude from update checking .PARAMETER IncludeModuleForChecking ModuleNames to include from update checking By default all modules are included. Default value is: "*" .PARAMETER ReportChangeOnMajor Report when major version changed for a module This means 'Get-PackageUpdateSetting' report update need, only when the major version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 1 0 0 0 .PARAMETER ReportChangeOnMinor Report when minor version changed for a module This means 'Get-PackageUpdateSetting' report update need, only when the minor version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 0 1 0 0 .PARAMETER ReportChangeOnBuild Report when build version changed for a module This means 'Get-PackageUpdateSetting' report update need, when the build version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 0 0 1 0 .PARAMETER ReportChangeOnRevision Report when revision part changed for a module This means 'Get-PackageUpdateSetting' report update need, when the revision version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 1 0 0 0 .PARAMETER SettingObject Settings object parsed in from command Get-PackageUpdateSetting This is an optional parameter. By default it will use the default settings object from the module. .PARAMETER PassThru The rule object will be parsed to the pipeline for further processing .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Add-PackageUpdateRule -IncludeModuleForChecking "MyModule" -ReportChangeOnMajor $true -ReportChangeOnMinor $true -ReportChangeOnBuild $true -ReportChangeOnRevision $false Add a new custom rule for "MyModule" to supress notifications on revision updates of the module #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium', DefaultParameterSetName = "ById")] [Alias('spur')] [OutputType([PackageUpdate.ModuleRule])] Param ( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ById")] [ValidateRange(1, [int]::MaxValue)] [int] $Id, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "ByInputObject")] [PackageUpdate.ModuleRule[]] $InputObject, [Alias("Include", "IncludeModule")] [String[]] $IncludeModuleForChecking, [Alias("Exclude", "ExcludeModule")] [AllowEmptyString()] [String[]] $ExcludeModuleFromChecking, [bool] $ReportChangeOnMajor, [bool] $ReportChangeOnMinor, [bool] $ReportChangeOnBuild, [bool] $ReportChangeOnRevision, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [PackageUpdate.Configuration] $SettingObject, [switch] $PassThru ) begin { } process { # If no setting object is piped in, get the current settings if (-not $SettingObject) { $SettingObject = Get-PackageUpdateSetting } # Find the rule by Id if ($id) { $InputObject = Get-PackageUpdateRule -Id $id } # Work through all objects foreach ($rule in $InputObject) { # Set the new preference values if ("ExcludeModuleFromChecking" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ExcludeModuleFromChecking: '$([string]::Join(", ", $ExcludeModuleFromChecking))'" $rule.ExcludeModuleFromChecking = $ExcludeModuleFromChecking } if ("IncludeModuleForChecking" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting IncludeModuleForChecking: '$([string]::Join(", ", $IncludeModuleForChecking))'" $rule.IncludeModuleForChecking = $IncludeModuleForChecking } if ("ReportChangeOnMajor" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnMajor: $($ReportChangeOnMajor)" $rule.ReportChangeOnMajor = $ReportChangeOnMajor } if ("ReportChangeOnMinor" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnMinor: $($ReportChangeOnMinor)" $rule.ReportChangeOnMinor = $ReportChangeOnMinor } if ("ReportChangeOnBuild" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnBuild: $($ReportChangeOnBuild)" $rule.ReportChangeOnBuild = $ReportChangeOnBuild } if ("ReportChangeOnRevision" -in $PSCmdlet.MyInvocation.BoundParameters.Keys) { Write-Verbose "Setting ReportChangeOnRevision: $($ReportChangeOnRevision)" $rule.ReportChangeOnRevision = $ReportChangeOnRevision } # change rule and write back PackageUpdateSetting object if ($pscmdlet.ShouldProcess("Rule Id $($rule.id)", "Set properties from PackUpdateSetting object ($($SettingObject.Path))")) { $SettingObject.CustomRule = $SettingObject.CustomRule | Where-Object Id -ne $rule.Id | Sort-Object -Property Id $SettingObject.CustomRule += $rule $SettingObject.CustomRule = $SettingObject.CustomRule | Sort-Object -Property Id $SettingObject | ConvertFrom-PackageUpdateSetting | ConvertTo-Json | Out-File -FilePath $SettingObject.Path -Encoding default -Force } if ($PassThru) { $rule } } } end { } } function Set-PackageUpdateSetting { <# .SYNOPSIS Set behaviour settings for PackageUpdateInfo module .DESCRIPTION Set-PackageUpdateInfo configure basic settings for check and report on up-to-dateness information on installed modules .PARAMETER Path The filepath where to setting file is stored This is optional, default path value is: Linux: "$HOME/.local/share/powershell/PackageUpdateInfo/PackageUpdateSetting_$($PSEdition)_$($PSVersionTable.PSVersion.Major).json") Windows: "$HOME\AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateSetting_$($PSEdition)_$($PSVersionTable.PSVersion.Major).json") .PARAMETER InputObject Settings object parsed in from command Get-PackageUpdateSetting .PARAMETER ExcludeModuleFromChecking ModuleNames to exclude from update checking in the default rule .PARAMETER IncludeModuleForChecking ModuleNames to include from update checking in the default rule By default all modules are included. Default value is: "*" .PARAMETER ReportChangeOnMajor Report when major version changed for a module in the default rule This means 'Get-PackageUpdateSetting' report update need, only when the major version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 1 0 0 0 .PARAMETER ReportChangeOnMinor Report when minor version changed for a module in the default rule This means 'Get-PackageUpdateSetting' report update need, only when the minor version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 0 1 0 0 .PARAMETER ReportChangeOnBuild Report when build version changed for a module in the default rule This means 'Get-PackageUpdateSetting' report update need, when the build version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 0 0 1 0 .PARAMETER ReportChangeOnRevision Report when revision part changed for a module in the default rule This means 'Get-PackageUpdateSetting' report update need, when the revision version version of a module change. Major Minor Build Revision ----- ----- ----- -------- 1 0 0 0 .PARAMETER UpdateCheckInterval The minimum interval/timespan has to gone by,for doing a new module update check Default value is: "01:00:00" .PARAMETER LastCheck Timestamp when last check for update need on modules started .PARAMETER LastSuccessfulCheck Timestamp when last check for update need finished .PARAMETER Reset Reset module to it'S default behaviour .PARAMETER PassThru The setting object will be parsed to the pipeline for further processing .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Set-PackageUpdateSetting -ExcludeModuleFromChecking @("") -IncludeModuleForChecking "*" -ReportChangeOnMajor $true -ReportChangeOnMinor $true -ReportChangeOnBuild $true -ReportChangeOnRevision $true -UpdateCheckInterval "01:00:00" Reset module to it'S default behaviour .EXAMPLE PS C:\> Set-PackageUpdateSetting -Reset Reset module to it'S default behaviour #> [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] [Alias('spus')] [OutputType([PackageUpdate.Configuration])] Param ( [Parameter(ParameterSetName = "SetBehaviour")] [Alias("Exclude", "ExcludeModule")] [AllowEmptyString()] [String[]] $ExcludeModuleFromChecking, [Parameter(ParameterSetName = "SetBehaviour")] [Alias("Include", "IncludeModule")] [String[]] $IncludeModuleForChecking, [Parameter(ParameterSetName = "SetBehaviour")] [bool] $ReportChangeOnMajor, [Parameter(ParameterSetName = "SetBehaviour")] [bool] $ReportChangeOnMinor, [Parameter(ParameterSetName = "SetBehaviour")] [bool] $ReportChangeOnBuild, [Parameter(ParameterSetName = "SetBehaviour")] [bool] $ReportChangeOnRevision, [Parameter(ParameterSetName = "SetBehaviour")] [timespan] $UpdateCheckInterval, [Parameter(ParameterSetName = "SetBehaviour")] [datetime] $LastCheck, [Parameter(ParameterSetName = "SetBehaviour")] [datetime] $LastSuccessfulCheck, [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = "SetBehaviour")] [PackageUpdate.Configuration] $InputObject, [Parameter(ParameterSetName = "ResetBehaviour")] [switch] $Reset, [Alias("FullName", "FilePath")] [String] $Path, [switch] $PassThru ) begin { } process { if ($PSCmdlet.ParameterSetName -like "ResetBehaviour") { if (-not $Path) { $Path = $script:ModuleSettingPath } if ($pscmdlet.ShouldProcess($path, "Reset PackageUpdateInfo behaviour")) { # Initialize default preferences $defaultSetting = [PackageUpdate.Configuration]@{ CustomRule = @() DefaultRule = [PackageUpdate.ModuleRule]@{ ExcludeModuleFromChecking = @("") IncludeModuleForChecking = @("*") ReportChangeOnMajor = $true ReportChangeOnMinor = $true ReportChangeOnBuild = $true ReportChangeOnRevision = $true } UpdateCheckInterval = "01:00:00" LastCheck = [string][datetime]::MinValue LastSuccessfulCheck = [string][datetime]::MinValue Path = $Path } # Write setting to file $defaultSetting | ConvertFrom-PackageUpdateSetting | ConvertTo-Json | Out-File -FilePath $Path -Encoding default -Force if ($PassThru) { $defaultSetting } } } if ($PSCmdlet.ParameterSetName -like "SetBehaviour") { # If no setting object is piped in, get the current settings if (-not $InputObject) { $paramPackageUpdateSetting = @{ } if ($Path) { $paramPackageUpdateSetting["Path"] = $Path } $InputObject = Get-PackageUpdateSetting @paramPackageUpdateSetting } # check if path is specified if ($Path) { if (-not ($Path -like $InputObject.Path)) { Write-Verbose -Message "Setting object is piped in but a different path for PackageUpdate setting is specified. Piped in object will override path setting. (Effective path: $($InputObject.Path))" -Verbose $Path = $InputObject.Path } } else { $Path = $InputObject.Path } # Set the new preference values if ("ExcludeModuleFromChecking" -in $PSBoundParameters.Keys) { Write-Verbose "Setting ExcludeModuleFromChecking: '$([string]::Join(", ", $ExcludeModuleFromChecking))'" $InputObject.DefaultRule.ExcludeModuleFromChecking = $ExcludeModuleFromChecking } if ("IncludeModuleForChecking" -in $PSBoundParameters.Keys) { Write-Verbose "Setting IncludeModuleForChecking: '$([string]::Join(", ", $IncludeModuleForChecking))'" $InputObject.DefaultRule.IncludeModuleForChecking = $IncludeModuleForChecking } if ($PSBoundParameters["ReportChangeOnMajor"]) { Write-Verbose "Setting ReportChangeOnMajor: $($ReportChangeOnMajor)" $InputObject.DefaultRule.ReportChangeOnMajor = $ReportChangeOnMajor } if ($PSBoundParameters["ReportChangeOnMinor"]) { Write-Verbose "Setting ReportChangeOnMinor: $($ReportChangeOnMinor)" $InputObject.DefaultRule.ReportChangeOnMinor = $ReportChangeOnMinor } if ($PSBoundParameters["ReportChangeOnBuild"]) { Write-Verbose "Setting ReportChangeOnBuild: $($ReportChangeOnBuild)" $InputObject.DefaultRule.ReportChangeOnBuild = $ReportChangeOnBuild } if ($PSBoundParameters["ReportChangeOnRevision"]) { Write-Verbose "Setting ReportChangeOnRevision: $($ReportChangeOnRevision)" $InputObject.DefaultRule.ReportChangeOnRevision = $ReportChangeOnRevision } if ($PSBoundParameters["UpdateCheckInterval"]) { Write-Verbose "Setting UpdateCheckInterval: $($UpdateCheckInterval)" $InputObject.UpdateCheckInterval = $UpdateCheckInterval } if ($PSBoundParameters["LastCheck"]) { Write-Verbose "Setting LastCheck: $($LastCheck)" $InputObject.LastCheck = $LastCheck } if ($PSBoundParameters["LastSuccessfulCheck"]) { Write-Verbose "Setting LastSuccessfulCheck: '$($LastSuccessfulCheck)'" $InputObject.LastSuccessfulCheck = $LastSuccessfulCheck } Write-Verbose "Setting 'Path': $($Path)" $InputObject.Path = $Path if ($pscmdlet.ShouldProcess($path, "Export PackageUpdateInfo")) { $InputObject | ConvertFrom-PackageUpdateSetting | ConvertTo-Json | Out-File -FilePath $Path -Encoding default -Force } } } end { } } function Show-PackageUpdateReleaseNote { <# .SYNOPSIS Show release notes from a module .DESCRIPTION Show release notes from a module. .PARAMETER InputObject Input object(s) from Get-PackageUpdateInfo or Import-PackageUpdateInfo to show release notes .PARAMETER Module Input object(s) from Get-Module to show release notes .PARAMETER WhatIf If this switch is enabled, no actions are performed but informational messages will be displayed that explain what would happen if the command were to run. .PARAMETER Confirm If this switch is enabled, you will be prompted for confirmation before executing any operations that change state. .EXAMPLE PS C:\> Get-PackageUpdateInfo | Show-PackageUpdateReleaseNote Get release notes out of PackageUpdateInfo objects .EXAMPLE PS C:\> Get-Module PackageUpdateInfo | Show-PackageUpdateReleaseNote Get relase notes from a module #> [CmdletBinding( SupportsShouldProcess = $true, ConfirmImpact = 'Low')] [Alias('spurn')] [OutputType([PackageUpdate.ReleaseNote])] Param ( [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByPackageUpdeInfoObject')] [Alias("Input")] [PackageUpdate.Info[]] $InputObject, [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'ByModuleObject')] [System.Management.Automation.PSModuleInfo[]] [Alias('ModuleName')] $Module ) begin { } process { $modulesToProcess = @() # Process input pipeline for PackageUpdateInfo objects $modulesToProcess += foreach ($inputObjectItem in $InputObject) { if ($inputObjectItem.HasReleaseNotes) { [PackageUpdate.ReleaseNote]@{ Name = $inputObjectItem.Name Version = $inputObjectItem.VersionOnline ReleaseNotes = $inputObjectItem.ReleaseNotes } } else { Write-Verbose "No release notes found for module $($inputObjectItem.Name) ($($inputObjectItem.Version))" -Verbose } } # Process input pipeline for modules $modulesToProcess += foreach ($moduleItem in $Module) { if ($moduleItem.ReleaseNotes) { [PackageUpdate.ReleaseNote]@{ Name = $moduleItem.Name Version = $moduleItem.Version ReleaseNotes = $moduleItem.ReleaseNotes } } else { Write-Verbose "No release notes found for module $($moduleItem.Name) ($($moduleItem.Version))" -Verbose } } # Working through the module objects foreach ($item in $modulesToProcess) { if ($pscmdlet.ShouldProcess($item.Name, "Show release notes")) { # If release notes are an URL, try to gather release notes from web site if ($item.ReleaseNotesIsURI) { # Set basic variables $msgUnableToGather = "Unable to gather release notes from website. Please use a browser to visit: $($item.ReleaseNotesURI)" $paramInvokeWebRequest = @{ TimeoutSec = 2 ErrorAction = "SilentlyContinue" } # Gathering release notes from web modules is only possible in some certain cases # if url points to a file on a github repo, it is possible to get plain text information # in other cases, the url will be tried to resolve and double check if it is a github repo (quicklinks/ forwardings) switch ($item.ReleaseNotesURI) { { Assert-PossibleReleaseNotesURI -URI $_ } { if ($item.ReleaseNotesURI -like "*/releases/*") { # Github release pages can't be covered. Set unable-message $item.Notes = $msgUnableToGather } else { # Set variables for Invoke-WebRequest $paramInvokeWebRequest["Uri"] = $item.ReleaseNotesURI.ToString() -replace "/blob/", "/raw/" if ($PSVersionTable.PSEdition -like "Desktop") { $paramInvokeWebRequest["UseBasicParsing"] = $true } # Gather release notes from web $webData = Invoke-WebRequest @paramInvokeWebRequest # Set web content as notes $item.Notes = $webData.Content } } default { # Default case. URL seams not to be a known/parseable URL for plain text release notes. # gather the URL anyway to check if it is a forwarding/shortlink URL to github # Set variables for Invoke-WebRequest to resolve the specified URI $paramInvokeWebRequest["Uri"] = $item.ReleaseNotesURI.ToString() if ($PSVersionTable.PSEdition -like "Desktop") { $paramInvokeWebRequest["UseBasicParsing"] = $true } # Gather URI from web $webData = Invoke-WebRequest @paramInvokeWebRequest # Extract URI from WebRequest response if ($PSVersionTable.PSEdition -like "Desktop") { $_uri = $webData.BaseResponse.ResponseUri } else { $_uri = $webData.BaseResponse.RequestMessage.RequestUri } # Is gathered WebRequest a "valid" URI, now? if (Assert-PossibleReleaseNotesURI -URI $_uri) { # Set variables for final Invoke-WebRequest to resolve the plain text release notes $paramInvokeWebRequest.uri = $_uri -replace "/blob/", "/raw/" # Gather release notes from web $webData = Invoke-WebRequest @paramInvokeWebRequest # set web content as notes $item.Notes = $webData.Content } else { # Set unable-message $item.Notes = $msgUnableToGather } } } } # Output result item $item } } } end { } } # Place all code that should be run after functions are imported here $script:ModuleIconPath = Join-Path -Path $script:ModuleRoot -ChildPath "\bin\PackageUpdateInfo.png" if ($isLinux) { $script:ModuleTempPath = Join-Path -Path "/tmp" -ChildPath "PackageUpdateInfo" $script:ModuleSettingPath = Join-Path -Path $HOME -ChildPath ".local/share/powershell/PackageUpdateInfo/PackageUpdateSetting_$($PSEdition)_$($PSVersionTable.PSVersion.Major).json" } else { $script:ModuleTempPath = Join-Path -Path $env:TEMP -ChildPath "PackageUpdateInfo" $script:ModuleSettingPath = Join-Path -Path $HOME -ChildPath "AppData\Local\Microsoft\Windows\PowerShell\PackageUpdateSetting_$($PSEdition)_$($PSVersionTable.PSVersion.Major).json" } $script:CurrentUserModulePath = $env:PSModulePath.split(';') | Where-Object { $_ -like "$(Split-Path $PROFILE -Parent)*" -or $_ -like "$($HOME)*" } $script:AllUsersModulePath = $env:PSModulePath.split(';') | Where-Object { $_ -notlike "$(Split-Path $PROFILE -Parent)*" -and $_ -notlike "$($HOME)*" } if (Test-Path -Path $script:ModuleTempPath) { Get-ChildItem -Path $script:ModuleTempPath | Where-Object LastWriteTime -lt (Get-Date).AddHours(-2) | Remove-Item -Force -Confirm:$false } else { New-Item -Path $script:ModuleTempPath -ItemType Directory -Force | Out-Null } if (-not (Test-Path -Path $script:ModuleSettingPath)) { Write-Verbose -Message "Going to initialize default settings for module PackageUpdateInfo" Set-PackageUpdateSetting -Reset -Path $script:ModuleSettingPath } #endregion Load compiled code |