9

New GitHub CLI extension tools

 1 year ago
source link: https://github.blog/2023-01-13-new-github-cli-extension-tools/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

New GitHub CLI extension tools

Support for GitHub CLI extensions has been expanded with new authorship tools and more ways to discover and install custom commands. Learn how to write powerful extensions in Go and find new commands to install.

New GitHub CLI extension tools
Author
January 13, 2023

Since the GitHub CLI 2.0 release, developers and organizations have customized their CLI experience by developing and installing extensions. Since then, the CLI team has been busy shipping several new features to further enhance the experience for both extension consumers and authors. Additionally, we’ve shipped go-gh 1.0 release, a Go library giving extension authors access to the same code that powers the GitHub CLI itself. Finally, the CLI team released the gh/pre-extension-precompile action, which automates the compilation and release of Go, Rust, or C++ extensions.

This blog post provides a tour of what’s new, including an in-depth look at writing a CLI extension with Go.

Introducing extension discovery

In the 2.20.0 release of the GitHub CLI, we shipped two new commands, including gh extension browse and gh extension search, to make discovery of extensions easier (all extension commands are aliased under gh ext, so the rest of this post will use that shortened version).

gh ext browse

gh ext browse is a new kind of command for the GitHub CLI: a fully interactive Terminal User Interface (TUI). It allows users to explore published extensions interactively right in the terminal.

A screenshot of a terminal showing the user interface of

Once gh ext browse has launched and loads extension data, you can browse through all of the GitHub CLI extensions available for installation sorted by star count by pressing the up and down arrows (or k and j).

Pressing / focuses the filter box, allowing you to trim the list down to a search term.

A screenshot of

You can select any extension by highlighting it. The selected extension can be installed by pressing i or uninstalled by pressing r. Pressing w will open the currently highlighted extension’s repository page on GitHub in your web browser.

Our hope is that this is a more enjoyable and easy way to discover new extensions and we’d love to hear feedback on the approach we took with this command.

gh ext search

In tandem with gh ext browse we’ve shipped another new command intended for scripting and automation: gh ext search. This is a classic CLI command which, with no arguments, prints out the first 30 extensions available to install sorted by star count.

A screenshot of the

A green check mark on the left indicates that an extension is installed locally.

Any arguments provided narrow the search results:

A screenshot of the

Results can be further refined and processed with flags, like:

  • --limit, for fetching more results
  • --owner, for only returning extensions by a certain author
  • --sort, for example for sorting by updated
  • --license, to filter extensions by software license
  • --web, for opening search results in your web browser
  • --json, for returning results as JSON

This command is intended to be scripted and will produce composable output if piped. For example, you could install all of the extensions I have written with:

gh ext search --owner vilmibm | cut -f2 | while read -r extension; do gh ext install $extension; done
A screenshot of a terminal after running

For more information about gh ext search and example usage, see gh help ext search.

Writing an extension with go-gh

The CLI team wanted to accelerate extension development by putting some of the GitHub CLI’s own code into an external library called go-gh for use by extension authors. The GitHub CLI itself is powered by go-gh, ensuring that it is held to a high standard of quality. This library is written in Go just like the CLI itself.

To demonstrate how to make use of this library, I’m going to walk through building an extension from the ground up. I’ll be developing a command called askfor quickly searching the threads in GitHub Discussions. The end result of this exercise lives on GitHub if you want to see the full example.

Getting startedThe initial code

package main

import (
        "fmt"

        "github.com/cli/go-gh"
)

func main() {
        fmt.Println("hi world, this is the gh-ask extension!")
        client, err := gh.RESTClient(nil)
        if err != nil {
                fmt.Println(err)
                return
        }
        response := struct {Login string}{}
        err = client.Get("user", &response)
        if err != nil {
                fmt.Println(err)
                return
        }
        fmt.Printf("running as %s\n", response.Login)
}


Selecting a repository


…a list of relevant threads from whatever repository you're in locally…

…a list of relevant threads from the cli/cli repository…
 main

 (
        
        
        

        
        
)

 {
     err := cli(); err !=  {
            fmt.Fprintf(os.Stderr, , err.Error())
            os.Exit()
    }
}

 {
        repoOverride := flag.String(
        , , )
        flag.Parse()

         repo repository.Repository
         err 

         *repoOverride ==  {
                repo, err = gh.CurrentRepository()
        }  {
                repo, err = repository.Parse(*repoOverride)
        }
         err !=  {
                 fmt.Errorf(, err.Error())

        }

        fmt.Printf(
       , repo.Owner(), repo.Name())

}
$ go run .
Going to search discussions  vilmibm/gh-ask
$ go run . --repo cli/cli
Going to search discussions  cli/cli

Accepting an argument

    

         (flag.Args()) <  {
                 errors.New()
        }
        search := strings.Join(flag.Args(), )

        fmt.Printf(
                ,
            repo.Owner(), repo.Name(), search)

        }
$  run . 
Please specify a search term
exit status 

$  run . cats
Going to search discussions in   

$  run . fluffy cats
Going to search discussions in   

Talking to the API

        

client, err := gh.GQLClient()
         err !=  {
                 fmt.Errorf(, err)
        }

        query := fmt.Sprintf(, repo.Owner(), repo.Name())

         Discussion  {
                Title 
                URL    
                Body  
        }

        response :=  {
                Repository  {
                        Discussions  {
                                Edges [] {
                                        Node Discussion
                                }
                        }
                        HasDiscussionsEnabled 
                }
        }{}

        err = client.Do(query, , &response)
         err !=  {
                 fmt.Errorf(, err)
        }

         !response.Repository.HasDiscussionsEnabled {
                 fmt.Errorf(, repo.Owner(), repo.Name())
        }

        matches := []Discussion{}

         _, edge :=  response.Repository.Discussions.Edges {
                 strings.Contains(edge.Node.Body+edge.Node.Title, search) {
                        matches = (matches, edge.Node)
                }
        }

         (matches) ==  {
                fmt.Fprintln(os.Stderr, )
             
        }

         _, d :=  matches {
                fmt.Printf(, d.Title, d.URL)
        }
$ go run . --repo cli/cli actions
gh pr create don trigger `pullrequest:` actions https:
GitHub CLI . https:
What permissions are needed to  OOTB GITHUB_TOKEN with gh pr merge --squash --auto https:
gh actions feedback https:
Pushing changes to an inbound pull request https:
getting workflow id and artifact id to reuse  github actions https:

Formatting output

         (matches) ==  {
                fmt.Println()
        }

    

        isTerminal := term.IsTerminal(os.Stdout)
        tp := tableprinter.New(os.Stdout, isTerminal, )


 isTerminal {
                fmt.Printf(
                        ,
                        repo.Owner(), repo.Name(), search)
        }

        fmt.Println()
         _, d :=  matches {
                tp.AddField(d.Title)
                tp.AddField(d.URL)
                tp.EndRow()
        }

        err = tp.Render()
         err !=  {
                 fmt.Errorf(, err)
        }

Opening browsers

 {
        lucky := flag.Bool(, , )
    
         (matches) ==  {
                fmt.Println()
        }

         *lucky {
                b := browser.New(, os.Stdout, os.Stderr)
                b.Browse(matches[].URL)
                
        }

    

JSON output

 {
    jsonFlag := flag.Bool(, , )
    jqFlag := flag.String(, , )
 
isTerminal := term.IsTerminal(os.Stdout)
         *jsonFlag {
                output, err := json.Marshal(matches)
                 err !=  {
                         fmt.Errorf(, err)
                }

                 *jqFlag !=  {
                         jq.Evaluate(bytes.NewBuffer(output), os.Stdout, *jqFlag)
                }

                 jsonpretty.Format(os.Stdout, bytes.NewBuffer(output), , isTerminal)
        }

Keep going

I’ll stop here with gh ask but this isn’t all go-gh can do. To browse its other features, check out this list of packages in the reference documentation. You can see my full code on GitHub at vilmibm/gh-ask.

Releasing your extension with cli/gh-extension-precompile

Now that I have a feature-filled extension, I’d like to make sure it’s easy to create releases for it so others can install it. At this point it’s uninstallable since I have not precompiled any of the Go code.

Before I worry about making a release, I have to make sure that my extension repository has the gh-extension tag. I can add that by running gh repo edit --add-tag gh-extension. Without this topic added to the repository, it won’t show up in commands like gh ext browse or gh ext search.

Since I started this extension by running gh ext create, I already have a GitHub Actions workflow defined for releasing. All that’s left before others can use my extension is pushing a tag to trigger a release. The workflow file contains:

name: release
on:
  push:
    tags:
      - "v*"
permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: cli/gh-extension-precompile@v1

Before tagging a release, make sure:

  • The repository is public (assuming you want people to install this for themselves! You can keep it private and make releases just for yourself, if you prefer.).
  • You’ve pushed all the local work you want to see in the release.

To release:

A screenshot of running

Note that the workflow ran automatically. It looks for tags of the form vX.Y.Z and kicks off a build of your Go code. Once the release is done, I can check it to see that all my code compiled as expected:

A screenshot of running

Now, anyone can run gh ext install vilmibm/gh-ask and try out my extension! This is all the work of the gh-extension-precompile action. This action can be used to compile any language, but by default it only knows how to handle Go code.

By default, the action will compile executables for:

  • Linux (amd64, 386, arm, arm64)
  • Windows (amd64, 386, arm64)
  • MacOS (amd64, arm64)
  • FreeBSD (amd64, 386, arm64)
  • Android (amd64, arm64)

To build for a language other than Go, edit .github/workflows/release.yml to add a build_script_override configuration. For example, if my repository had a script at scripts/build.sh, my release.yml would look like:

name: release
on:
  push:
    tags:
      - "v*"

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: cli/gh-extension-precompile@v1
        with:
          build_script_override: "script/build.sh"

The script specified as build_script_override must produce executables in a dist directory at the root of the extension repository with file names ending with: {os}-{arch}{ext}, where the extension is .exe on Windows and blank on other platforms. For example:

  • dist/gh-my-ext_v1.0.0_darwin-amd64
  • dist/gh-my-ext_v1.0.0_windows-386.exe

Executables in this directory will be uploaded as release assets on GitHub. For OS and architecture nomenclature, please refer to this list. We use this nomenclature when looking for executables from the GitHub CLI, so it needs to be respected even for non-Go extensions.

Future directions

The CLI team has some improvements on the horizon for the extensions system in the GitHub CLI. We’re planning a more accessible version of the extension browse command that renders a single column style interface suitable for screen readers. We intend to add support for nested extensions–in other words, an extension called as a subcommand of an existing gh command like gh pr my-extension–making third-party extensions fit more naturally into our command hierarchy. Finally, we’d like to improve the documentation and flexibility of the gh-extension-precompile action.

Are there features you’d like to see? We’d love to hear about it in a discussion or an issue in the cli/cli repository.

Wrap-up

It is our hope that the extensions system in the GitHub CLI inspire you to create features beyond our wildest imagination. Please go forth and make something you’re excited about, even if it’s just to make gh do fun things like run screensavers.

Tags:

The GitHub Insider Newsletter

Get the best of GitHub. Once a month. Directly to your inbox.

Subscribe

More on GitHub CLI

Contributing to open source at GitHub

Contributing to open source at GitHub

A software engineer’s personal journey to becoming an open source contributor.

Release Radar · July 2022 Edition

While some of us have been wrapping up the financial year, and enjoying vacation time, others have been hard at work shipping open source projects and releases. These projects include…

GitHub Availability Report: March 2022

In March, we experienced several incidents resulting in significant impact to multiple GitHub services.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK