Skip to content

Commit

Permalink
Add automatic daily update checking (#185)
Browse files Browse the repository at this point in the history
The module will now check daily with PowerShellGallery to see if there
is a newer version of the module available.  This check can be disabled
with the new `DisableUpdateCheck` config property.

The check happens asynchronously.  The web request is kicked off when
the module is loaded, but the result is only checked when the user
runs a command.

The result is logged at the Verbose level in any failure case as well
as when the module is up-to-date.  When there is a newer version
available, it will write the message out as a Warning.  The web request
will only run once per day, and the user message will only be written
once per day as well.
  • Loading branch information
HowardWolosky authored May 30, 2020
1 parent a1ba15e commit a9f48a8
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 2 deletions.
8 changes: 8 additions & 0 deletions GitHubConfiguration.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ function Set-GitHubConfiguration
Specify this switch to stop the module from reporting any of its usage (which would be used
for diagnostics purposes).
.PARAMETER DisableUpdateCheck
Specify this switch to stop the daily update check with PowerShellGallery which can
inform you when there is a newer version of this module available.
.PARAMETER LogPath
The location of the log file that all activity will be written to if DisableLogging remains
$false.
Expand Down Expand Up @@ -194,6 +198,8 @@ function Set-GitHubConfiguration

[switch] $DisableTelemetry,

[switch] $DisableUpdateCheck,

[string] $LogPath,

[switch] $LogProcessId,
Expand Down Expand Up @@ -281,6 +287,7 @@ function Get-GitHubConfiguration
'DisablePiiProtection',
'DisableSmarterObjects',
'DisableTelemetry',
'DisableUpdateCheck',
'LogPath',
'LogProcessId',
'LogRequestBody',
Expand Down Expand Up @@ -615,6 +622,7 @@ function Import-GitHubConfiguration
'disablePiiProtection' = $false
'disableSmarterObjects' = $false
'disableTelemetry' = $false
'disableUpdateCheck' = $false
'defaultNoStatus' = $false
'defaultOwnerName' = [String]::Empty
'defaultRepositoryName' = [String]::Empty
Expand Down
2 changes: 2 additions & 0 deletions GitHubCore.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ function Invoke-GHRestMethod
[switch] $NoStatus
)

Invoke-UpdateCheck

# Normalize our Uri fragment. It might be coming from a method implemented here, or it might
# be coming from the Location header in a previous response. Either way, we don't want there
# to be a leading "/" or trailing '/'
Expand Down
3 changes: 2 additions & 1 deletion PowerShellForGitHub.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
'GitHubTeams.ps1',
'GitHubUsers.ps1',
'NugetTools.ps1',
'Telemetry.ps1')
'Telemetry.ps1',
'UpdateCheck.ps1')

# Minimum version of the Windows PowerShell engine required by this module
PowerShellVersion = '4.0'
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ access token.
If you ever wish to clear it in the future, just call `Clear-GitHubAuthentication`).

> For automated scenarios (like GithHub Actions) where you are dynamically getting the access token
> needed for authentication, refer to `Example 2` in `Get-Help Set-StoreBrokerAuthentication -Examples`
> needed for authentication, refer to `Example 2` in `Get-Help Set-GitHubAuthentication -Examples`
> for how to configure in a promptless fashion.
>
> Alternatively, you _could_ configure PowerShell itself to always pass in a plain-text access token
Expand Down
145 changes: 145 additions & 0 deletions UpdateCheck.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

# We'll cache our job name and result so that we don't check more than once per day.
$script:UpdateCheckJobName = $null
$script:HasLatestVersion = $null

function Invoke-UpdateCheck
{
<#
.SYNOPSIS
Checks PowerShellGallery to see if a newer version of this module has been published.
.DESCRIPTION
Checks PowerShellGallery to see if a newer version of this module has been published.
The check will only run once per day.
Runs asynchronously, so the user won't see any message until after they run their first
API request after the module has been imported.
Will always assume true in the event of an incomplete or failed check.
Reports the result to the user via a Warning message (if a newer version is available)
or a Verbose message (if they're running the latest version or the version check failed).
The Git repo for this module can be found here: http://aka.ms/PowerShellForGitHub
.EXAMPLE
Invoke-UpdateCheck
.NOTES
Internal-only helper method.
#>
param()

if (Get-GitHubConfiguration -Name DisableUpdateCheck)
{
return
}

$moduleName = $MyInvocation.MyCommand.Module.Name
$moduleVersion = $MyInvocation.MyCommand.Module.Version

$jobNameToday = "Invoke-UpdateCheck-" + (Get-Date -format 'yyyyMMdd')

# We only check once per day
if ($jobNameToday -eq $script:UpdateCheckJobName)
{
# Have we retrieved the status yet? $null means we haven't.
if ($null -ne $script:HasLatestVersion)
{
# We've already completed the check for today. No further action required.
return
}

$state = (Get-Job -Name $script:UpdateCheckJobName).state
if ($state -eq 'Failed')
{
# We'll just assume we're up-to-date if we failed to check today.
Write-Log -Message '[$moduleName] update check failed for today (web request failed). Assuming up-to-date.' -Level Verbose
$script:HasLatestVersion = $true

# Clear out the job info (even though we don't need the result)
$null = Receive-Job -Name $script:UpdateCheckJobName -AutoRemoveJob -Wait -ErrorAction SilentlyContinue -ErrorVariable errorInfo
return
}
elseif ($state -eq 'Completed')
{
$result = Receive-Job -Name $script:UpdateCheckJobName -AutoRemoveJob -Wait
try
{
# Occasionally the API puts two nearly identical XML responses in the body (each on a separate line).
# We'll try to avoid an unnecessary failure by always using the first line of the response.
$xml = [xml]($result.Content.Split([Environment]::NewLine) | Select-Object -First 1)
$latestVersion = $xml.feed.entry.properties.version |
ForEach-Object {[System.Version]$_} |
Sort-Object -Descending |
Select-Object -First 1

$script:HasLatestVersion = $latestVersion -eq $moduleVersion
if ($script:HasLatestVersion)
{
Write-Log "[$moduleName] update check complete. Running latest version: $($latestVersion.ToString())" -Level Verbose
}
else
{
Write-Log "A newer version of $moduleName is available ($latestVersion). Your current version is $moduleVersion. Run 'Update-Module $moduleName' to get up-to-date." -Level Warning
}
}
catch
{
# This could happen if the server returned back an invalid (non-XML) response for the request
# for some reason.
Write-Log -Message "[$moduleName] update check failed for today (invalid XML response). Assuming up-to-date." -Level Verbose
$script:HasLatestVersion = $true
}

return
}
else
{
# It's still running. Nothing further for us to do right now. We'll check back
# again next time.
return
}
}
else
{
# We either haven't checked yet, or it's a new day so we should check again.
$script:UpdateCheckJobName = $jobNameToday
$script:HasLatestVersion = $null
}

[scriptblock]$scriptBlock = {
param($ModuleName)

$params = @{}
$params.Add('Uri', "https://www.powershellgallery.com/api/v2/FindPackagesById()?id='$ModuleName'")
$params.Add('Method', 'Get')
$params.Add('UseDefaultCredentials', $true)
$params.Add('UseBasicParsing', $true)

try
{
Invoke-WebRequest @params
}
catch
{
# We will silently ignore any errors that occurred, but we need to make sure that
# we are explicitly catching and throwing them so that our reported state is Failed.
throw
}
}

$null = Start-Job -Name $script:UpdateCheckJobName -ScriptBlock $scriptBlock -Arg @($moduleName)

# We're letting this run asynchronously so that users can immediately start using the module.
# We'll check back in on the result of this the next time they run any command.
}

# We will explicitly run this as soon as the module has been loaded,
# and then it will be called again during every major function call in the module
# so that the result can be reported.
Invoke-UpdateCheck

0 comments on commit a9f48a8

Please sign in to comment.