Extending the gh CLI with Go

GitHub’s gh CLI can be enhanced via custom extensions. The following offers an introduction, as well as some notes and tips for doing so in Go.

  1. What?
  2. Extensions
  3. How do gh extensions work?
  4. Implementation tips, suggestions, etc.
  5. Bonus experimental idea: bootstrapping developer experience and platform engineering
  6. Further reading

What?

Out of the box, the gh CLI supports a collection of commands for interacting with GitHub features like repositories, releases, pull requests, and more. For example, to view a repository’s open pull requests, use gh pr ls:

demo

gh can be installed via common package managers (brew install gh on Mac OS). It’s also preinstalled on all GitHub-hosted Actions runners and available for use in GitHub Actions CI/CD pipelines, as demonstrated by mdb/ensure-unpublished-release-action’s release pipeline, which invokes gh release create to publish GitHub releases.

Aliases

Beyond its built-in commands, gh supports the creation of custom alias commands.

Extensions

Beyond its built-in features, gh can be extended to support custom commands. I recently created the gh-dispatch extension for triggering repository_dispatch and/or workflow_dispatch events and watching the resulting GitHub Actions workflow run. Once installed, the extension provides a gh dispatch command:

demo

gh extension install installs extensions from their GitHub repositories. To install gh-dispatch:

gh extension install mdb/gh-dispatch

…and to view all installed extensions, use gh extension list:

gh actions-importer  github/gh-actions-importer    v1.0.1
gh actions-status    rsese/gh-actions-status       v1.0.0
gh dash              dlvhdr/gh-dash                v3.4.1
gh dispatch          mdb/gh-dispatch               0.0.1
gh markdown-preview  yusukebe/gh-markdown-preview  198c536b
gh metrics           hectcastro/gh-metrics         v2.1.0

A few extensions of note include…

To see other community-maintained gh extensions, browse GitHub repositories tagged with the “gh-extension” topic.

(And if you’ve got improvement ideas for gh-dispatch, open a PR!)

How do gh extensions work?

Under the hood, gh CLI extensions are gh--prefixed executables located at a standard path (sidebar: this is similar to how kubectl plugins work too). For example, on Mac OS, the executables live in ~/.local/share/gh/extensions:

ls ~/.local/share/gh/extensions
gh-actions-importer gh-dispatch gh-metrics
gh-actions-status   gh-dash     gh-markdown-preview

With this in mind, a rudimentary extension might be a simple bash script named and located accordingly:

cat ~/.local/share/gh/extensions/gh-hello/gh-hello
#!/bin/bash

echo "hello"

…and thereby executable as a gh subcommand:

gh hello
hello

So, returning to the gh-dispatch example…

gh extension install mdb/gh-dispatch fetches the gh-dispatch extension from GitHub and, at least in the case of Mac OS, installs it to the ~/.local/share/extensions/gh-dispatch path. Because gh-dispatch’s own build process precompiles the extension for various OS/architecture combos and publishes the resulting binaries to a GitHub release, gh extension install knows to download the correct binary associated with the targeted release version (by default, it fetches the latest release, though specific versions can be targeted with a --pin option).

(Arguably, all this becomes especially compelling when considering the growing ubiquity of GitHub, the widespread popularity of the gh CLI, and that gh extension install requires no additional package managers, local runtimes, or developer environment setup, at least in the case of precompiled extensions like gh-dispatch. So, with this in mind, perhaps a custom gh extension could be leveraged as a sensible entrypoint when bootsrapping higher level developer experiences for an organization – think platform onboarding, environment setup, repository generation, secrets management, build utilities, etc. But, more on all that later.)

Implementation tips, suggestions, etc.

While a gh extension can be authored in any language, Go is an especially good fit for a few reasons:

  1. The gh CLI itself is authored in Go.

    Because gh itself is written in Go using the Cobra framework, its source code offers lotsa helpful examples and patterns. For example, the gh pr ls implementation reveals how gh subcommands are declared as a &cobra.Command.

    Furthermore, the packages homed in https://github.com/cli/cli/ can also be imported by extension source code, which enables helpful reuse patterns. As an example, gh-dispatch leverages upstream https://github.com/cli/cli/ packages, particularly when rendering output. A few other packages of note include…

    Disclaimer: I’m not actually sure the gh maintainers intend for these packages to be used outside the CLI codebase, but it can be done.

  2. github.com/cli/go-gh can be used by Go-based extensions.

    In addition to the https://github.com/cli/cli/ packages, github.com/cli/go-gh is also available to Go-based extension developers. According to go-gh’s own README:

    go-gh is a collection of Go modules to make authoring GitHub CLI extensions easier.

    For example, gh-dispatch uses github.com/cli/go-gh/pkg/auth and github.com/cli/go-gh/pkg/config to ensure the extension authenticates users to GitHub via the same patterns used by the core gh CLI.

  3. Go programs can be easily precompiled across OS/architecture combos.

    By precompiling a CLI extension, users can install and run the extension without additional run time dependencies (i.e. bash, Ruby, Python, etc.) beyond the gh CLI itself, nor is it necessary to provide users any per-OS/per-architecture installation instructions; gh extension install <OWNER>/<REPO> simply downloads the appropriate binary from the <OWNER>/<REPO> GitHub releases (For example, note the precompiled binaries associated with each of gh-dispatch’s GitHub releases).

    Also helpful: gh-extension-precompile is a reusable GitHub Action for automating the publication of such binaries to GitHub releases, saving Go-based extension authors from writing their own CI/CD automation. However, if you’d prefer to write your own release automation – or to use goreleaser directly, as gh-dispatch’s release process does – that’s not too hard, either (just make sure the precompiled binaries are named correctly; gh extension install assumes a naming convention. When in doubt, emulate a known working example, such as gh-dispatch).

Bonus experimental idea: bootstrapping developer experience and platform engineering

While gh extensions can be useful in and of themselves (I use gh-dash every day!), the ecosystem teases some broader possibilities: could a gh extension sensibly serve as the cornerstone of an organization’s developer platform experience?

In my experience, it’s common for engineering leadership to fantasize about a cohesive internal developer platform, often facilitated via a purpose-built CLI, web portal, and/or API, and enabling developer efficiency via stuff like…

  • a standard interface to centralized automation
  • automated software project setup and repository scaffolding
  • the generation and maintenance of standard, templated CI/CD pipelines
  • the maintenance of source code repository best practices, such as standard required status checks and required PR code review approvals
  • per-team public cloud provider account provisioning and infrastructure configuration
  • sensible management of role-based access control, secrets (such as API credentials), adherence to the principle of least privilege and similar security-sensitive safeguards, etc.
  • a common interface to otherwise disparate tools, such as documentation, project management, dashboards, etc.
  • a general purpose mechanism for self-service and the discovery of ever-evolving internal platform capabilities

However, depending on context (org size, engineering practices, priorities, etc.), building such a developer platform involves labor and complexity that may jeopardize more business-critical efforts, especially when doing so invests in proprietary solutions to common needs. To varying degrees, open tools like Backstage and various Internal Developer Platforms aim to help. Zooming out, even Kubernetes itself – despite its complexity – aspires to be an open, common, standard-ish platform. Technologies like Kubevela, Argo, and Flux further enhance the Kubernetes platform experience, each in their own way. These are promising tools. But, perhaps building on the ubiquity of the existing GitHub ecosystem via a custom gh extension might be a relatively low-effort, low-risk compliment to your organization’s developer experience as well? Or maybe a sensible entrypoint before tools beyond GitHub are implicated?

(In my experience, an organization-specific gh extension becomes especially attractive when it serves as an interface to other commonly-used GitHub features: GitHub Actions workflows, infrastructure-as-code automation, GitHub pages-hosted JSON endpoints, GitHub secrets, GitHub repository environments, GitHub template repositories, GitHub teams, etc. From this perspective, perhaps GitHub itself emerges as a practical foundation to a robust internal developer platform ecosystem, with a custom gh extension tying it all together as a cohesive user experience.)

A big disclaimer, though: all this gh-extension-facilitated developer experience talk is bit experimental and largely intended as food for thought. Context matters in nuanced ways, do your own thinkin’/assessin’, your mileage may vary, maybe YAGNI, etc. Plus, even in absence of big developer experience ambitions, the gh CLI and its community-maintained extensions have clear small-scale utility.

Further reading