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.
- What?
- Extensions
- How do gh extensions work?
- Implementation tips, suggestions, etc.
- Bonus experimental idea: bootstrapping developer experience and platform engineering
- 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
:
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:
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…
- gh-dash - a Terminal dashboard for GitHub issues and pull requests
- gh-install - install GitHub release binaries
- gh-changelog - create keep a changelog-style changelogs
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:
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, thegh pr ls
implementation reveals howgh
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…- github.com/cli/cli/v2/api - a GitHub API client.
- github.com/cli/cli/v2/pkg/httpmock - utilities for mocking HTTP transactions during automated tests (see gh-dispatch’s own tests for example usage).
- github.com/cli/cli/v2/pkg/iostreams - utilities for working with IO and rendering CLI extension output.
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.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 togo-gh
’s ownREADME:
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 coregh
CLI.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.