Skip to content

Instantly share code, notes, and snippets.

@JPRuskin
Created April 30, 2024 09:44
Show Gist options
  • Save JPRuskin/90e9c086b673e743b2a33df760af7571 to your computer and use it in GitHub Desktop.
Save JPRuskin/90e9c086b673e743b2a33df760af7571 to your computer and use it in GitHub Desktop.
PowerShell Universal App to display the last X Chocolatey Community Package build results, highlighting failures and showing failures that have an existing PR in place.
#requires -Version 6.1
[CmdletBinding()]
param(
[uint16]$Last = 20
)
if (-not $cache:CachedCalls) {
$cache:CachedCalls = @{}
}
function Get-GistFileRawUrl {
[OutputType([string[]])]
[CmdletBinding()]
param(
# Full url to the gist
[Parameter(Mandatory, ParameterSetName="Url")]
[string]$GistUrl = "https://gist.github.com/$($Owner)/$($GistID)",
# The owner account of the gist
[Parameter(Mandatory, ParameterSetName="Split")]
[string]$Owner,
# The ID of the gist
[Parameter(Mandatory, ParameterSetName="Split")]
[string]$GistID,
# Filename within the gist
[Parameter(Mandatory)]
[string]$Name,
# The number of revisions to return
[uint]$Revisions = 1
)
end {
if ($PSCmdlet.ParameterSetName -eq 'Url' -and $GistUrl -match "^https://(?:gist.githubusercontent.com|gist.github.com)/(?<Owner>.+)/(?<GistId>.+)/?") {
$Owner, $GistID = $Matches.Owner, $Matches.GistID
}
if ($Revisions -gt 1) {
$GistRequest = @{
Uri = "https://api.github.com/gists/$($GistID)/commits"
Headers = @{
Accept = "application/vnd.github+json"
}
}
if ($env:github_api_token) {
$GistRequest.Headers += @{Authorization = "Bearer $($env:github_api_token)"}
} elseif ($secret:GitHubPAT) {
$GistRequest.Headers += @{Authorization = "Bearer $($secret:GitHubPAT)"}
}
$Requested = 0
$PerPage = [math]::Min($Revisions, 100)
$Page = 1
$(while ($Requested -lt $Revisions) {
Invoke-RestMethod @GistRequest -Body @{
per_page = $PerPage
page = $Page++
} -Headers @{
}
$Requested += $PerPage
}).ForEach{$_}.ForEach{
"https://gist.githubusercontent.com/$($Owner)/$($GistId)/raw/$($_.version)/$($Name)"
} | Select-Object -First $Revisions
} else {
"https://gist.githubusercontent.com/$($Owner)/$($GistId)/raw/$($Name)"
}
}
}
function Get-GitHubRepositoryPullRequestNumber {
[CmdletBinding()]
[OutputType([uint16[]])]
param(
[Parameter()]
[string]$Repository = "chocolatey-community/chocolatey-packages",
[hashtable]$Filter = @{
state = 'open'
base = 'master'
sort = 'updated'
direction = 'desc'
}
)
end {
$GitHubRequest = @{
Uri = "https://api.github.com/repos/$($Repository)/pulls"
Headers = @{
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}
}
if ($env:github_api_token) {
$GitHubRequest.Headers += @{Authorization = "Bearer $($env:github_api_token)"}
} elseif ($secret:GitHubPAT) {
$GistRequest.Headers += @{Authorization = "Bearer $($secret:GitHubPAT)"}
}
if (-not $cache:CachedCalls) {$cache:CachedCalls = @{}}
if (-not $cache:CachedCalls[$GitHubRequest.Uri]) {
$cache:CachedCalls[$GitHubRequest.Uri] = Invoke-RestMethod @GitHubRequest -Body $Filter
}
$cache:CachedCalls[$GitHubRequest.Uri].Number
}
}
function Get-ChocolateyPackageUpdatesInGitHubPR {
<#
.Synopsis
#>
[CmdletBinding()]
param(
[Parameter()]
[string]$Repository = "chocolatey-community/chocolatey-packages",
[Parameter(Mandatory, ValueFromPipeline)]
[uint16[]]$PR = $(Get-GitHubRepositoryPullRequestNumber)
)
begin {
$GitHubRequest = @{
Headers = @{
Accept = "application/vnd.github+json"
"X-GitHub-Api-Version" = "2022-11-28"
}
}
if ($env:github_api_token) {
$GitHubRequest.Headers += @{Authorization = "Bearer $($env:github_api_token)"}
} elseif ($secret:GitHubPAT) {
$GistRequest.Headers += @{Authorization = "Bearer $($secret:GitHubPAT)"}
}
if (-not $cache:CachedCalls) {$cache:CachedCalls = @{}}
}
process {
foreach ($PR in $PR) {
$GitHubRequest.Uri = "https://api.github.com/repos/$($Repository)/pulls/$($PR)/files"
if (-not $cache:CachedCalls[$GitHubRequest.Uri]) {
$cache:CachedCalls[$GitHubRequest.Uri] = Invoke-RestMethod @GitHubRequest -UseBasicParsing
}
$PackageFolderRegex = "^(?<PackageType>automatic|deprecated|extensions|manual)\/(?<PackageId>.+?)\/.+" # Arguably we could just match on automatic for AU
[PSCustomObject]@{
PR = $PR[0]
PackageId = @($cache:CachedCalls[$GitHubRequest.Uri].FileName) -match $PackageFolderRegex -replace $PackageFolderRegex,'${PackageId}' | Select-Object -Unique
}
}
}
}
function ParseMarkdownTable {
param(
[Parameter(Mandatory, ParameterSetName = "Markdig")]
$Table,
[Parameter(Mandatory, ParameterSetName = "String")]
[string[]]$TableString = $Table.Inline.Content[0].Text.Split("`n"),
[Parameter(Mandatory)]
[ValidateSet("Pushed","Error","Ignored","OK")]
[string]$Type
)
begin {
$script:Regex = @{
"Pushed" = @{
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>'
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)'
Updated = '\[(?<Updated>True|False)\].+'
Pushed = '(?<Pushed>True|False)'
RemoteVersion = '\[(?<RemoteVersion>.*)?\]\((?<SoftwareUrl>.+)?\)'
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)'
}
"Error" = @{
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>'
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)'
RemoteVersion = '\[(?<RemoteVersion>.*)?\]\((?<SoftwareUrl>.+)?\)'
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)'
Error = '\[(?<ErrorMessage>.*)\]\((?:.+)\)'
}
"Ignored" = @{
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>'
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)'
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)'
IgnoreMessage = '(?<IgnoreMessage>.*)'
}
"OK" = @{
Icon = '<img src="(?<IconUri>.+)?" width="32" height="32"\/>'
Name = '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)'
Updated = '\[?(?<Updated>True|False).+'
Pushed = '(?<Pushed>True|False)'
RemoteVersion = '\[(?<RemoteVersion>.*)?\]\((?<SoftwareUrl>.+)?\)'
NuspecVersion = '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)?\)'
}
}
}
end {
$Headers = $TableString[0].Split('|').Where{$_}
$TableString[2..$TableString.Count].ForEach{
$TableRegex = @(
'^' # Start of line
$Headers.ForEach{
$script:Regex.$Type.$_
}
) -join "\|"
if ($_ -match $TableRegex) {
$ht = $Matches
$ht.Remove("0")
} else {
Write-Warning "$($Type): '$($_)' did not match '$($TableRegex)'"
}
# $Line = $_.Split('|')
# $ht = [ordered]@{
# Icon = $Line[$($ErrorHeaders.IndexOf('Icon'))] -replace '<img src="(?<Uri>.+)" width="32" height="32"/>','${Uri}'
# Name = $Line[$($ErrorHeaders.IndexOf('Name'))] -replace '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)','${Name}'
# RemoteVersion = if ($Line[$($ErrorHeaders.IndexOf('RemoteVersion'))] -match '\[(?<RemoteVersion>.+)\]\((?<SoftwareUrl>.+)\)') {$Matches.RemoteVersion}
# NuspecVersion = $Line[$($ErrorHeaders.IndexOf('NuspecVersion'))] -replace '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)\)', '${NuspecVersion}'
# PackageUrl = $Line[$($ErrorHeaders.IndexOf('Name'))] -replace '\[(?<Name>.+)\]\((?<PackageUrl>.+)\)', '${PackageUrl}'
# SourceUrl = $Line[$($ErrorHeaders.IndexOf('NuspecVersion'))] -replace '\[(?<NuspecVersion>.+)\]\((?<SourceUrl>.+)\)','${SourceUrl}'
# }
# foreach ($Key in @("Icon","Name","RemoteVersion","NuspecVersion")) {
# $ht.$Key = $Line[$($ErrorHeaders.IndexOf($Key))]
# }
[PSCustomObject]$ht
}
}
}
function Get-AUOutputFromMarkdown {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('Uri')]
[string]$Path
)
process {
if ($cache:CachedCalls[$Path]) {
return $cache:CachedCalls[$Path]
}
$Raw = if (Test-Path $Path) {
Get-Content $Path
} else {
(Invoke-WebRequest $Path -UseBasicParsing).content
}
$Markdown = $Raw | ConvertFrom-Markdown
$Output = [ordered]@{}
# $Stats = $Markdown.Tokens.Where{$_.Level -eq 1}[0] # Never used
$Pushed = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'Pushed'}[0]
$Errors = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'Errors'}[0]
$Ignored = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'Ignored'}[0]
$Ok = $Markdown.Tokens.Where{$_.Level -eq 2 -and $_.Inline.Content.ToString() -eq 'OK'}[0]
if ($Pushed) {
$Output.Pushed = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($Pushed) + 1].Inline.Content[0].Text.Split("`n")) -Type Pushed
}
if ($Errors) {
$Output.Errors = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($Errors) + 1].Inline.Content[0].Text.Split("`n")) -Type Error
# The full message from the error is not contained in the table, so we look it up separately and overwrite the partial
foreach ($Package in $Output.Errors) {
$Package.ErrorMessage = $Markdown.Tokens[$Markdown.Tokens.IndexOf(
$Markdown.Tokens.Where{$_.Level -eq 3 -and $_.Inline.Content.ToString() -eq $Package.Name}[0]
) + 1].Lines -join "`n"
# We also add any open PRs targeting that package
Add-Member -InputObject $Package -Name ExistingPR -MemberType NoteProperty -Value (Get-GitHubRepositoryPullRequestNumber | Get-ChocolateyPackageUpdatesInGitHubPR).Where{$_.PackageId -eq $Package.Name}.PR
}
}
if ($Ignored) {
$Output.Ignored = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($Ignored) + 1].Inline.Content[0].Text.Split("`n")) -Type Ignored
}
if ($OK) {
$Output.OK = ParseMarkdownTable -TableString ($Markdown.Tokens[$Markdown.Tokens.IndexOf($OK) + 1].Inline.Content[0].Text.Split("`n")) -Type OK
}
# Sort Build Stats
$Output.Build = [PSCustomObject]@{
Time = Get-Date (Select-String -InputObject $Raw -Pattern "(?<=\*\*UTC\*\*: )\d{4}-\d{2}-\d{2} \d{2}:\d{2}").Matches.Value
AUVersion = "1.0.0" # There's actually no source for this data, turns out, despite the badge.
Pushed = $Output.Pushed.Count
Errors = $Output.Errors.Count
Ignored = $Output.Ignored.Count
Report = $Path
}
$cache:CachedCalls[$Path] = [PSCustomObject]$Output
$cache:CachedCalls[$Path]
}
}
function Get-UpdateFlakiness {
[CmdletBinding()]
[OutputType([HashTable])]
param(
[Parameter(Mandatory)]
[string]$Id,
$DataSet = $R
)
@{
Failed = $DataSet.Where{$_.Errors.Name -eq $Id}.Count
Total = $DataSet.Count
Series = $DataSet.Where({$_.Errors.Name -notcontains $Id}, "Until").Count
LastNonFailure = $DataSet.Where({$_.Errors.Name -notcontains $Id}, "First", 1).Build.Time
}
}
function Get-PackageModerationStatus {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Id,
[Parameter()]
[string]$Version
)
$Result = if ($Version) {
Invoke-RestMethod "https://community.chocolatey.org/api/v2/Packages()?`$filter=(Id eq '$Id' and Version eq '$Version')&includePrerelease=true"
} else {
Invoke-RestMethod "https://community.chocolatey.org/api/v2/Packages()?`$filter=(Id eq '$Id')&includePrerelease=true"
}
$Result[-1].properties.PackageStatus
}
# if (-not $MyInvocation.InvocationName -eq '.') {
# $R = Get-GistFileRawUrl -GistUrl "https://gist.github.com/choco-bot/a14b1e5bfaf70839b338eb1ab7f8226f" -Name "Update-AUPackages.md" -Revisions $Last | Get-AUOutputFromMarkdown
# $R.Errors.Where{-not $_.ExistingPR}.Name # List to work on
# }
New-UDDashboard -Title 'Community Packages Dashboard' -Pages @(
New-UDPage -Name 'Home' -Title "Chocolatey Community Package Build History" -Content {
$R = Get-GistFileRawUrl -GistUrl "https://gist.github.com/choco-bot/a14b1e5bfaf70839b338eb1ab7f8226f" -Name "Update-AUPackages.md" -Revisions $Last | Get-AUOutputFromMarkdown
New-UDGridLayout -Content {
New-UDCard -Id "LastRun" -Title "Last Run" -Content {
New-UDHtml "`n`n`n"
New-UDList -Content {
New-UDListItem -Icon (New-UDIcon -Icon Clock -Size 2x) -Label "$($R[0].Build.Time)"
New-UDListItem -Icon (New-UDIcon -Icon ThumbsUp -Style @{color='green'} -Size 2x) -Label "$($R[0].Build.Pushed)" -SubTitle Pushed
New-UDListItem -Icon (New-UDIcon -Icon Check -Size 2x) -Label "$($R[0].OK.Count)" -SubTitle OK
New-UDListItem -Icon (New-UDIcon -Icon Meh -Size 2x) -Label "$($R[0].Build.Ignored)" -SubTitle Ignored
New-UDListItem -Icon (New-UDIcon -Icon ThumbsDown -Style @{color='red'} -Size 2x) -Label "$($R[0].Build.Errors)" -SubTitle Failed
New-UDListItem -Icon (New-UDIcon -Icon Archive -Size 2x) -Label "$($R[0].Build.Pushed + $R[0].OK.Count + $R[0].Build.Ignored + $R[0].Build.Errors)" -SubTitle Total
}
} -TitleAlignment center -Raised -Sx @{
"background-color" = "background.default" # to troubleshoot
}
$Chart = @{
Type = "bar"
LabelProperty = "Date"
Data = @(
$R[-1..-$R.Length].ForEach{
@{
Date = "$(Get-Date $_.Build.Time -Format 'MM/dd')"
Pushed = $_.Build.Pushed
#OK = $_.OK.Count
Ignored = $_.Build.Ignored
Errors = $_.Build.Errors
}
}
)
Dataset = @(
#New-UDChartJSDataset -DataProperty OK -Label OK -BackgroundColor lightgrey
New-UDChartJSDataset -DataProperty Errors -Label Errors -BackgroundColor red -BorderWidth 2 -AdditionalOptions @{borderRadius=5}
New-UDChartJSDataset -DataProperty Ignored -Label Ignored -BackgroundColor lightgray -BorderWidth 2 -AdditionalOptions @{borderRadius=5}
New-UDChartJSDataset -DataProperty Pushed -Label Pushed -BackgroundColor green -BorderWidth 2 -AdditionalOptions @{borderRadius=5}
)
Options = @{
responsive = $true
scales = @{
xAxes = @{
stacked = $true
}
yAxes = @{
stacked = $true
}
}
}
}
New-UDChartJS @Chart -Id "BuildChart"
New-UDTabs -Id "Tables" -Tabs {
New-UDTab -Text "Failed" -Content {
New-UDTable -Id "ErrorTable" -Data $R[0].Errors -Columns @(
New-UDTableColumn -Property Name -Title "Package" -Render {
New-UDImage -Url $EventData.IconUri -Width 25
New-UDTypography -Text $EventData.Name -Style @{
"font-size" = "1.2rem"
"margin-left" = "20px"
} -Sx @{
color = "text.secondary"
}
} -Truncate
New-UDTableColumn -Property Flakiness -Title "Flakiness" -Render {
$Flakiness = Get-UpdateFlakiness -Id $EventData.Name
New-UDTooltip -Content {
"$($Flakiness.Failed / $Flakiness.Total * 100)%"
} -TooltipContent {
"$($Flakiness.Failed) / $($Flakiness.Total) Failed"
New-UDHtml "`n"
"$($Flakiness.Series) in a row."
if ($Flakiness.LastNonFailure) {
New-UDHtml "`n"
" Last non-failure: $($Flakiness.LastNonFailure)"
}
} -Type info
}
New-UDTableColumn -Property NuspecVersion -Title "Current Version" -Render {
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow
}
New-UDTableColumn -Property ExistingPR -Title "PR" -Render {
if ($EventData.ExistingPR) {
New-UDLink -Url "https://github.com/chocolatey-community/chocolatey-packages/pull/$($EventData.ExistingPR)" -Text "#$($EventData.ExistingPR)"
} else {
New-UDTypography -Text "None" -Sx @{color="text.secondary"}
}
}
New-UDTableColumn -Property ErrorMessage -Title "Error"
) -OnRowExpand {} # Consider OnRowExpand New-UDNivoChart -Calendar
} -Dynamic
New-UDTab -Text "Ignored" -Content {
New-UDTable -Id "IgnoredTable" -Data $R[0].Ignored -Columns @(
New-UDTableColumn -Property Name -Title "Package" -Render {
New-UDImage -Url $EventData.IconUri -Width 25
New-UDTypography -Text $EventData.Name -Style @{
"font-size" = "1.2rem"
"margin-left" = "20px"
} -Sx @{
color = "text.secondary"
}
} -Truncate
New-UDTableColumn -Property NuspecVersion -Title "Current Version" -Render {
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow
}
New-UDTableColumn -Property IgnoreMessage -Title "Reason"
)
} -Dynamic
New-UDTab -Text "OK" -Disabled -Content {
New-UDTable -Id "OKTable" -Data $R[0].OK -Columns @(
New-UDTableColumn -Property Name -Title "Package" -Render {
New-UDImage -Url $EventData.IconUri -Width 25
New-UDTypography -Text $EventData.Name -Style @{
"font-size" = "1.2rem"
"margin-left" = "20px"
} -Sx @{
color = "text.secondary"
}
} -Truncate
New-UDTableColumn -Property NuspecVersion -Title "Current Version" -Render {
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow
}
)
} -Dynamic
New-UDTab -Text "Pushed" -Content {
New-UDTable -Id "PushedTable" -Data $R[0].Pushed -Columns @(
New-UDTableColumn -Property Name -Title "Package" -Render {
New-UDImage -Url $EventData.IconUri -Width 25
New-UDTypography -Text $EventData.Name -Style @{
"font-size" = "1.2rem"
"margin-left" = "20px"
} -Sx @{
color = "text.secondary"
}
} -Truncate
New-UDTableColumn -Property NuspecVersion -Title "Existing Version" -Render {
New-UDLink -Text $EventData.NuspecVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.NuspecVersion)" -OpenInNewWindow
}
New-UDTableColumn -Property RemoteVersion -Title "New Version" -Render {
New-UDLink -Text $EventData.RemoteVersion -Url "https://community.chocolatey.org/packages/$($EventData.Name)/$($EventData.RemoteVersion)" -OpenInNewWindow
}
# New-UDTableColumn -Property Pushed -Title "Status" -Render {
# Get-PackageModerationStatus -Id $EventData.Name -Version $EventData.RemoteVersion
# }
)
} -Dynamic
} -RenderOnActive
} -Layout '{"lg":[{"w":2,"h":14,"x":10,"y":0,"i":"grid-element-42050adf-edd1-42ab-9f52-ead3d1727f5a","moved":false,"static":false},{"w":10,"h":14,"x":0,"y":0,"i":"grid-element-6c6dead5-ad51-41fd-903c-745a1eb01285","moved":false,"static":false},{"w":12,"h":3,"x":0,"y":14,"i":"grid-element-082bf0c9-98b1-4e04-85c8-3ec35b77b6d4","moved":false,"static":false}],"md":[{"w":2,"h":13,"x":8,"y":0,"i":"grid-element-LastRun","moved":false,"static":false},{"w":8,"h":13,"x":0,"y":0,"i":"grid-element-BuildChart","moved":false,"static":false},{"w":10,"h":4,"x":0,"y":13,"i":"grid-element-Tables","moved":false,"static":false}]}' #-Design
}
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment