This is the GitHub CLI (gh), a command-line tool for interacting with GitHub. The module path is github.com/cli/cli/v2.
make # Build (Unix) — outputs bin/gh
go run script/build.go # Build (Windows)
go test ./... # All unit tests
go test ./pkg/cmd/issue/list/... -run TestIssueList_nontty # Single test
go test -tags acceptance ./acceptance # Acceptance tests
make lint # golangci-lint (same as CI)Entry point: cmd/gh/main.go → internal/ghcmd.Main() → pkg/cmd/root.NewCmdRoot().
Key packages:
pkg/cmd/<command>/<subcommand>/— CLI command implementationspkg/cmdutil/— Factory, error types, flag helpers (NilStringFlag,NilBoolFlag,StringEnumFlag)pkg/iostreams/— I/O abstraction with TTY detection, color, pagerpkg/httpmock/— HTTP mocking for testsapi/— GitHub API client (GraphQL + REST)internal/featuredetection/— GitHub.com vs GHES capability detectioninternal/tableprinter/— Table output for list commands
A command gh foo bar lives in pkg/cmd/foo/bar/ with bar.go, bar_test.go, and optionally http.go/http_test.go.
- Command + tests:
pkg/cmd/issue/list/list.goandlist_test.go - Factory wiring:
pkg/cmd/factory/default.go - Unit tests:
internal/agents/detect_test.go
Every command follows this structure (see pkg/cmd/issue/list/list.go):
Optionsstruct withIO,HttpClient,Config,BaseRepo+ flagsNewCmdFoo(f *cmdutil.Factory, runF func(*FooOptions) error)constructor —runFis the test injection point- Separate
fooRun(opts)function with the business logic
Key rules:
- Lazy-init
BaseRepo,Remotes,BranchinsideRunE, not the constructor - Commands register in
pkg/cmd/root/root.go; subcommand groups usecmdutil.AddGroup()
Use heredoc.Doc for examples with # comment lines and $ command prefixes:
Example: heredoc.Doc(`
# Do the thing
$ gh foo bar --flag value
`),Add --json, --jq, --template flags via cmdutil.AddJSONFlags(cmd, &opts.Exporter, fieldNames). In the run function: if opts.Exporter != nil { return opts.Exporter.Write(opts.IO, data) }. See pkg/cmd/pr/list/list.go.
Use httpmock.Registry with defer reg.Verify(t) to ensure all stubs are called:
reg := &httpmock.Registry{}
defer reg.Verify(t)
reg.Register(
httpmock.REST("GET", "repos/OWNER/REPO"),
httpmock.JSONResponse(someData),
)
reg.Register(
httpmock.GraphQL(`query PullRequestList\b`),
httpmock.FileResponse("./fixtures/prList.json"),
)
client := &http.Client{Transport: reg}Common: REST(method, path), GraphQL(pattern), JSONResponse(body), FileResponse(path). See pkg/httpmock/ for all matchers/responders.
ios, stdin, stdout, stderr := iostreams.Test()
ios.SetStdoutTTY(true) // simulate terminalUse testify. Always use require (not assert) for error checks so the test halts immediately:
require.NoError(t, err)
require.Error(t, err)
assert.Equal(t, "expected", actual)Interfaces use moq: //go:generate moq -rm -out prompter_mock.go . Prompter. Run go generate ./... after interface changes.
Use table-driven tests for functions with multiple input/output scenarios. See internal/agents/detect_test.go or pkg/cmd/issue/list/list_test.go for examples:
tests := []struct {
name string
// inputs and expected outputs
}{
{name: "descriptive case name", ...},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// arrange, act, assert
})
}- Add godoc comments to all exported functions, types, and constants
- Avoid unnecessary code comments — only comment when the why isn't obvious from the code
- Do not comment just to restate what the code does
Error types in pkg/cmdutil/errors.go:
FlagErrorf(...)— flag validation (prints usage)cmdutil.SilentError— exit 1, no messagecmdutil.CancelError— user cancelledcmdutil.PendingError— outcome pendingcmdutil.NoResultsError— empty results
Use cmdutil.MutuallyExclusive("message", cond1, cond2) for mutually exclusive flags.
Commands using feature detection must include a // TODO <cleanupIdentifier> comment directly above the if-statement for linter compliance:
// TODO someFeatureCleanup
if features.SomeCapability {
// use new API
} else {
// fallback for older GHES
}client := api.NewClientFromHTTP(httpClient)
client.GraphQL(hostname, query, variables, &data)
client.REST(hostname, "GET", "repos/owner/repo", nil, &data)For host resolution, use cfg.Authentication().DefaultHost() — not ghinstance.Default() which always returns github.com.