Earlier this year I appeared on the PowerShell Podcast. I ended the interview with a scripting challenge.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The Core Challenge
Using whatever tools and techniques you want, write a PowerShell function that
will query the Issues section of a GitHub repository and create output showing
the number of open issues by label and the percentage of all open issues.
Remember that multiple labels may be used with an issue. For example, if there are 54 open issues and the bug label is used 23 times, your output would show a count of 23 and a total percentage of 42.59 for the bug label. The function should work for any GitHub repository, but test it with the PowerShell repository. Naturally, the function should follow community accepted best practices, have parameter validation, and proper error handling.
Bonus Challenge
I also included extra-credit for more experienced scripters.
Once you have the function, add custom formatting to display the results in a table,including the repository name or path. Create an alternative view that will also display the repository and the label URI that GitHub uses to create a filtered page view. Finally, create a control script using the function to create a markdown report for the PowerShell repository showing the top 25 labels. The markdown report should have clickable links.
My Solution
I posted the challenge as a gist and asked people to submit their solutions by posting a comment with a link to their work. I encourage you to see how people approached the challenge. Of course, you are more than welcome to develop your own solution and share a link to your work.
But here's how I approached the challenge. My solution is not necessarily the best way or only way to meet the requirements. But it is always educational to compare how people solve the same problem. In this case, many people might think the best approach is to use the GitHub API and Invoke-RestMethod
. I chose a different approach.
I am a big user of the GitHub command-line client, gh.exe. You can easily install it with winget.
winget install --id github.cli --source winget
You will need to restart your PowerShell session before you can use the command. You'll also need to authenticate.
gh auth login
Follow the prompts. Once you are authenticated, you can use the gh
command to query GitHub without having to deal with API tokens, headers, and so on. What is especially compelling is that you can have gh.exe output JSON. This means I can use it in PowerShell.
gh.exe issue list --repo jdhitsolutions/psscripttools --limit 50 --json 'id,title,labels' | ConvertFrom-Json
I wrote a PowerShell 7 function wrapped around this command.
Function Get-ghIssueLabelCount {
[cmdletbinding()]
[OutputType('ghLabelStatus')]
[alias('ghlc')]
Param(
[Parameter(
Position = 0,
Mandatory,
ValueFromPipeline,
HelpMessage = 'Specify the Github owner and repo in the format: owner/repo. You might need to match casing with GitHub.'
)]
[ValidateNotNullOrEmpty()]
[ValidatePattern('\w+/\w+$', ErrorMessage = 'Please use the format OWNER/Repository. e.g. jdhitsolutions/psreleasetools')]
[string]$Repository,
[Parameter(HelpMessage = 'Specify the first X number of issue labels sorted by count in descending order.')]
[ValidateScript({ $_ -gt 0 }, ErrorMessage = 'Enter a value greater than 0.')]
[int]$First = 25,
[Parameter(HelpMessage = 'Specify the number of issues to analyze')]
[ValidateScript({ $_ -gt 0 }, ErrorMessage = 'Enter a value greater than 0.')]
[int]$Limit = 1000
)
Begin {
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Starting $($MyInvocation.MyCommand)"
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Running under PowerShell version $($PSVersionTable.PSVersion)"
Write-Verbose "[$((Get-Date).TimeOfDay) BEGIN ] Using host: $($Host.Name)"
$ReportDate = Get-Date
} #begin
Process {
Try {
$gh = Get-Command -Name gh.exe -ErrorAction Stop
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Using $( gh.exe --version | Select-Object -First 1)"
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Processing $Limit issues from $Repository"
$ghData = gh.exe issue list --repo $Repository --limit $Limit --json 'id,title,labels' | ConvertFrom-Json
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Found $($ghData.count) items"
} #Try
Catch {
Write-Warning 'This command requires the gh.exe command-line utility.'
} #Catch
If ($ghData.count -gt 0) {
Write-Verbose "[$((Get-Date).TimeOfDay) PROCESS] Getting top $First issue labels"
$data = $ghData.labels |
Group-Object -Property Name -NoElement |
Sort-Object Count, Name -Descending |
Select-Object -First $First
foreach ($Item in $data) {
#create a custom object
if ($item.Name -match '\s') {
$escName = '%22{0}%22' -f ($item.Name -replace '\s', '+')
$uri = "https://github.com/$Repository/issues?q=is%3Aissue+is%3Aopen+label%3A$escName"
}
else {
$uri = "https://github.com/$Repository/issues?q=is%3Aissue+is%3Aopen+label%3A$($Item.Name)"
}
[PSCustomObject]@{
PStypeName = 'ghLabelStatus'
Count = $Item.Count
PctTotal = ($item.Count / $ghData.Count) * 100
Label = $Item.Name
LabelUri = $uri
Repository = $Repository
IssueCount = $ghData.Count
ReportDate = $ReportDate
}
}
} #if data found
else {
Write-Warning "No open issues found in $Repository"
}
} #process
End {
Write-Verbose "[$((Get-Date).TimeOfDay) END ] Ending $($MyInvocation.MyCommand)"
} #end
}
The function groups the issues by label and writes a custom object to the pipeline.
PS C:\> Get-ghIssueLabelCount -Repository jdhitsolutions/psscripttools
Count : 4
PctTotal : 80
Label : bug
LabelUri : https://github.com/jdhitsolutions/psscripttools/issues?q=is%3Aissue+is%3Aopen+label%3Abug
Repository : jdhitsolutions/psscripttools
IssueCount : 5
ReportDate : 3/7/2024 11:29:38 AM
...
This should meet the basic requirements.
Formatting
When I created my custom object, I also defined a unique type name.
[PSCustomObject]@{
PStypeName = 'ghLabelStatus'
Count = $Item.Count
PctTotal = ($item.Count / $ghData.Count) * 100
Label = $Item.Name
LabelUri = $uri
Repository = $Repository
IssueCount = $ghData.Count
ReportDate = $ReportDate
}
This allows me to create a custom format file using New-PSFormatXML
<!--
Format type data generated 12/04/2023 12:58:47 by PROSPERO\Jeff
This file was created using the New-PSFormatXML command that is part
of the PSScriptTools module.
https://github.com/jdhitsolutions/PSScriptTools
-->
<Configuration>
<ViewDefinitions>
<View>
<!--Created 12/04/2023 12:58:47 by PROSPERO\Jeff-->
<Name>default</Name>
<ViewSelectedBy>
<TypeName>ghLabelStatus</TypeName>
</ViewSelectedBy>
<GroupBy>
<ScriptBlock>"{0} [{1}]" -f $_.Repository,$_.ReportDate</ScriptBlock>
<Label>Repository</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>Count</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PctTotal</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Label</Label>
<Width>15</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Count</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>"{0:p2}" -f ($_.Count/$_.IssueCount)</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Label</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<!--Created 12/04/2023 15:18:46 by PROSPERO\Jeff-->
<Name>uri</Name>
<ViewSelectedBy>
<TypeName>ghLabelStatus</TypeName>
</ViewSelectedBy>
<GroupBy>
<ScriptBlock>
$link = $PSStyle.FormatHyperlink($_.Repository,"https://github.com/$($_.Repository)/issues")
"$($PSStyle.Foreground.Yellow +$PSStyle.Italic)$link$($PSStyle.Reset) [$($_.ReportDate)]"
</ScriptBlock>
<Label>Repository</Label>
</GroupBy>
<TableControl>
<!--Delete the AutoSize node if you want to use the defined widths.-->
<AutoSize />
<TableHeaders>
<TableColumnHeader>
<Label>Count</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>PctTotal</Label>
<Width>8</Width>
<Alignment>right</Alignment>
</TableColumnHeader>
<TableColumnHeader>
<Label>Label</Label>
<Width>14</Width>
<Alignment>left</Alignment>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>Count</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>"{0:p2}" -f ($_.Count/$_.IssueCount)</ScriptBlock>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
$link = $PSStyle.FormatHyperlink($_.Label,$_.LabelUri)
"$($PSStyle.Foreground.Yellow +$PSStyle.Italic)$link$($PSStyle.Reset)"
</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
The default view displays the issues in a table with the repository name and date.
PS C:\> Get-ghIssueLabelCount -Repository jdhitsolutions/psscripttools
Repository: jdhitsolutions/psscripttools [3/7/2024 11:36:35 AM]
Count PctTotal Label
----- -------- -----
4 80.00% bug
2 40.00% triage
2 40.00% enhancement
1 20.00% pending feedback
The alternate view uses PSStyle to insert hyperlinks.
<Name>uri</Name>
<ViewSelectedBy>
<TypeName>ghLabelStatus</TypeName>
</ViewSelectedBy>
<GroupBy>
<ScriptBlock>
$link = $PSStyle.FormatHyperlink($_.Repository,"https://github.com/$($_.Repository)/issues")
"$($PSStyle.Foreground.Yellow +$PSStyle.Italic)$link$($PSStyle.Reset) [$($_.ReportDate)]"
</ScriptBlock>
<Label>Repository</Label>
</GroupBy>
Using Windows Terminal, I now have clickable links.
Control Script
The last part of the challenge was to write a control script to create a markdown report.
#requires -version 7.4
#ghLabelReport.ps1
Param(
[Parameter(
Position = 0,
Mandatory,
ValueFromPipeline,
HelpMessage = 'Specify the Github owner and repo in the format: owner/repo. You might need to match casing with GitHub.'
)]
[ValidateNotNullOrEmpty()]
[ValidatePattern('\w+/\w+$', ErrorMessage = 'Please use the format OWNER/Repository. e.g. jdhitsolutions/psreleasetools')]
[string]$Repository,
[Parameter(HelpMessage = 'Specify the first X number of issue labels sorted by count in descending order.')]
[ValidateScript({ $_ -gt 0 }, ErrorMessage = 'Enter a value greater than 0.')]
[int]$First = 25,
[Parameter(HelpMessage = 'Specify the number of issues to analyze')]
[ValidateScript({ $_ -gt 0 }, ErrorMessage = 'Enter a value greater than 0.')]
[int]$Limit = 1000
)
#dot source the required function
. $PSScriptRoot\Get-ghIssueStats.ps1
$data = Get-ghIssueLabelCount @PSBoundParameters
if ($data) {
$repoIssues = "https://github.com/$($data[0].Repository)/issues"
$md = [System.Collections.Generic.List[string]]::New()
$md.Add("# Label Report for [$($data[0].Repository)]($repoIssues)`n")
$md.Add("This is the breakdown of labels based on __$($data[0].IssueCount)__ open issues. Total percentages might be more than 100% as issues can have multiple labels.`n")
$md.Add("Count| Percent of Total | Label|")
$md.Add("|-----|-----|-----|")
foreach ($item in $data) {
$md.Add("| $($item.count) | $([math]::Round($item.PctTotal,2)) | [$($item.Label)]($($item.LabelUri))|")
}
$md.Add("`n_$($data[0].ReportDate)_")
}
#write the markdown to the pipeline
$md
The script writes markdown to the pipeline. If I want a file, I can use Out-File
C:\scripts\ghLabelReport.ps1 -Repository jdhitsolutions/psscripttools | Out-File c:\temp\psscripttools-issues.md
Here's the report for the PowerShell repository.
C:\scripts\ghLabelReport.ps1 -Repository powershell/powershell | Out-File c:\temp\powershell-issues.md
Summary
Sometimes it helps to think outside the box or even backwards. I knew what data I needed to construct the objects I wanted. Then it was a matter of finding the best technique to get me the data. In my case, I opted for a command-line tool. This adds a dependency for anyone else who wants to try my code. But it made it very easy for me to write the code.
I will post my files as a GitHub gist so you don't have to try and copy from here. If you have questions or comments on the code, please post them as comments on the gist.
2 thoughts on “GitHub Scripting Challenge Solution”
Comments are closed.