Behind the PowerShell Pipeline logo

Behind the PowerShell Pipeline

Subscribe
Archives
December 20, 2024

Creating GitHub Extension PowerShell Tools

In the last newsletter, I introduced you to extensions in the gh.exe command-line tool for managing GitHub. I showed you some PowerShell code I had written to wrap around the native command-line tool and left other projects to you. But, as is typical, I couldn't leave well enough alone. Plus, I was bothered with a limitation in my previous code. Today, I want to show you how I wrapped the native gh.exe commands for working with extensions in PowerShell.

A Better Search

One thing that bothered me about my previous code was the way emojis were handled. Some descriptions of extensions contain emojis,

gh extension search gh-f --sort updated
Search results with emojis
figure 1

This also works with native JSON output.

gh extension search gh-f --sort updated --json fullName,url,updatedAt,description
JSON output with rendered emojis
figure 2

But as soon as I convert the JSON to a PowerShell object, the emojis are lost.

gh extension search gh-f --sort updated --json fullName,url,updatedAt,description | ConvertFrom-Json | select Fullname,description
Malformed emojis
figure 3

There is something in the gh.exe output that prevents the emoji from being properly rendered.

The gh.exe tool is using the native GitHub APIs, so started reverse-engineering the gh.exe expression to return the same results using the GitHub API.

$search = "fzf"
$tr = Invoke-RestMethod -Uri "https://api.github.com/search/repositories?q=topic:gh-extension+$search+in:description" -Headers @{ "Accept" = "application/vnd.github.v3+json" }
$tr.items | ForEach-Object {
    [PSCustomObject]@{
        name        = $_.name
        description = $_.description
        url         = $_.html_url
    }
}

The search is a compound query looking for the search term in the repository description and the topic gh-extension. The results are JSON by default, so I can convert them to PowerShell objects

API search results
figure 4

This is better in my opinion. With this proof-of-concept, I wrote this function.

Function Find-GHExtension {
    [CmdletBinding()]
    [OutputType('ghExtension')]
    Param(
        [Parameter(Position = 0, Mandatory, HelpMessage = 'What topic are you searching for?')]
        [ValidateNotNullOrEmpty()]
        [string]$SearchTopic
    )

    $splat = @{
        headers = @{ 'Accept' = 'application/vnd.github.v3+json' }
        uri     = "https://api.github.com/search/repositories?q=topic:gh-extension+$SearchTopic+in:description"
    }
    $response = Invoke-RestMethod @splat

    If ($response.total_count -gt 0) {
        $response.items | ForEach-Object {
            [PSCustomObject]@{
                PSTypeName  = 'ghExtension'
                FullName    = $_.full_name
                Name        = $_.name
                Created     = $_.created_at
                Updated     = $_.updated_at
                Description = $_.description.trim()
                Url         = $_.html_url.trim()
                Stars       = $_.stargazers_count
                Owner       = $_.owner.login
            }
        }
    }
    else {
        Write-Warning "No gh extensions found for $SearchTopic"
    }
}

> ⚠ Warning > The GitHub API is subject to rate limits which you might hit if you run this repeatedly in a short time frame.

I included additional properties in the output object to make it more useful. I also added a type name to the object so I can create a custom format file to display the results in a more readable format. I will skip that for now.

Now, I get much better results.

Find-GHExtension
figure 5

Installing Extensions

Because I had a search function, it only made sense to have an install function. This function requires PowerShell 7,

Function Install-GHExtension {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('none')]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipelineByPropertyName,
            ValueFromPipeline,
            HelpMessage = 'What extension are you installing?'
            )]
        [ValidateNotNullOrEmpty()]
        [ValidatePattern(('^\w+/\S+$'),ErrorMessage='The name should be in the "[HOST/]OWNER/REPO" format, got {0}')]
        [string]$FullName
    )
    Process {
        #Add support for WhatIf
        If ($PSCmdlet.ShouldProcess($FullName)) {
            $response = gh extension install $FullName
        } #whatIf
    } #process
}
Want to read the full issue?
GitHub Bluesky LinkedIn About Jeff
Powered by Buttondown, the easiest way to start and grow your newsletter.