|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +This is the GitHub CLI (`gh`), a command-line tool for interacting with GitHub. The module path is `github.com/cli/cli/v2`. |
| 4 | + |
| 5 | +## Build, Test, and Lint |
| 6 | + |
| 7 | +```bash |
| 8 | +make # Build (Unix) — outputs bin/gh |
| 9 | +go run script/build.go # Build (Windows) |
| 10 | +go test ./... # All unit tests |
| 11 | +go test ./pkg/cmd/issue/list/... -run TestIssueList_nontty # Single test |
| 12 | +go test -tags acceptance ./acceptance # Acceptance tests |
| 13 | +make lint # golangci-lint (same as CI) |
| 14 | +``` |
| 15 | + |
| 16 | +## Architecture |
| 17 | + |
| 18 | +Entry point: `cmd/gh/main.go` → `internal/ghcmd.Main()` → `pkg/cmd/root.NewCmdRoot()`. |
| 19 | + |
| 20 | +Key packages: |
| 21 | +- `pkg/cmd/<command>/<subcommand>/` — CLI command implementations |
| 22 | +- `pkg/cmdutil/` — Factory, error types, flag helpers (`NilStringFlag`, `NilBoolFlag`, `StringEnumFlag`) |
| 23 | +- `pkg/iostreams/` — I/O abstraction with TTY detection, color, pager |
| 24 | +- `pkg/httpmock/` — HTTP mocking for tests |
| 25 | +- `api/` — GitHub API client (GraphQL + REST) |
| 26 | +- `internal/featuredetection/` — GitHub.com vs GHES capability detection |
| 27 | +- `internal/tableprinter/` — Table output for list commands |
| 28 | + |
| 29 | +## Command Structure |
| 30 | + |
| 31 | +A command `gh foo bar` lives in `pkg/cmd/foo/bar/` with `bar.go`, `bar_test.go`, and optionally `http.go`/`http_test.go`. |
| 32 | + |
| 33 | +### Canonical Examples |
| 34 | + |
| 35 | +- **Command + tests**: `pkg/cmd/issue/list/list.go` and `list_test.go` |
| 36 | +- **Factory wiring**: `pkg/cmd/factory/default.go` |
| 37 | +- **Unit tests**: `internal/agents/detect_test.go` |
| 38 | + |
| 39 | +### The Options + Factory Pattern |
| 40 | + |
| 41 | +Every command follows this structure (see `pkg/cmd/issue/list/list.go`): |
| 42 | + |
| 43 | +1. `Options` struct with `IO`, `HttpClient`, `Config`, `BaseRepo` + flags |
| 44 | +2. `NewCmdFoo(f *cmdutil.Factory, runF func(*FooOptions) error)` constructor — `runF` is the test injection point |
| 45 | +3. Separate `fooRun(opts)` function with the business logic |
| 46 | + |
| 47 | +Key rules: |
| 48 | +- Lazy-init `BaseRepo`, `Remotes`, `Branch` inside `RunE`, not the constructor |
| 49 | +- Commands register in `pkg/cmd/root/root.go`; subcommand groups use `cmdutil.AddGroup()` |
| 50 | + |
| 51 | +### Command Examples and Help Text |
| 52 | + |
| 53 | +Use `heredoc.Doc` for examples with `#` comment lines and `$ ` command prefixes: |
| 54 | +```go |
| 55 | +Example: heredoc.Doc(` |
| 56 | + # Do the thing |
| 57 | + $ gh foo bar --flag value |
| 58 | +`), |
| 59 | +``` |
| 60 | + |
| 61 | +### JSON Output |
| 62 | + |
| 63 | +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`. |
| 64 | + |
| 65 | +## Testing |
| 66 | + |
| 67 | +### HTTP Mocking |
| 68 | + |
| 69 | +Use `httpmock.Registry` with `defer reg.Verify(t)` to ensure all stubs are called: |
| 70 | + |
| 71 | +```go |
| 72 | +reg := &httpmock.Registry{} |
| 73 | +defer reg.Verify(t) |
| 74 | + |
| 75 | +reg.Register( |
| 76 | + httpmock.REST("GET", "repos/OWNER/REPO"), |
| 77 | + httpmock.JSONResponse(someData), |
| 78 | +) |
| 79 | +reg.Register( |
| 80 | + httpmock.GraphQL(`query PullRequestList\b`), |
| 81 | + httpmock.FileResponse("./fixtures/prList.json"), |
| 82 | +) |
| 83 | +client := &http.Client{Transport: reg} |
| 84 | +``` |
| 85 | + |
| 86 | +Common: `REST(method, path)`, `GraphQL(pattern)`, `JSONResponse(body)`, `FileResponse(path)`. See `pkg/httpmock/` for all matchers/responders. |
| 87 | + |
| 88 | +### IOStreams in Tests |
| 89 | + |
| 90 | +```go |
| 91 | +ios, stdin, stdout, stderr := iostreams.Test() |
| 92 | +ios.SetStdoutTTY(true) // simulate terminal |
| 93 | +``` |
| 94 | + |
| 95 | +### Assertions |
| 96 | + |
| 97 | +Use `testify`. Always use `require` (not `assert`) for error checks so the test halts immediately: |
| 98 | + |
| 99 | +```go |
| 100 | +require.NoError(t, err) |
| 101 | +require.Error(t, err) |
| 102 | +assert.Equal(t, "expected", actual) |
| 103 | +``` |
| 104 | + |
| 105 | +### Generated Mocks |
| 106 | + |
| 107 | +Interfaces use `moq`: `//go:generate moq -rm -out prompter_mock.go . Prompter`. Run `go generate ./...` after interface changes. |
| 108 | + |
| 109 | +### Table-Driven Tests |
| 110 | + |
| 111 | +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: |
| 112 | + |
| 113 | +```go |
| 114 | +tests := []struct { |
| 115 | + name string |
| 116 | + // inputs and expected outputs |
| 117 | +}{ |
| 118 | + {name: "descriptive case name", ...}, |
| 119 | +} |
| 120 | +for _, tt := range tests { |
| 121 | + t.Run(tt.name, func(t *testing.T) { |
| 122 | + // arrange, act, assert |
| 123 | + }) |
| 124 | +} |
| 125 | +``` |
| 126 | + |
| 127 | +## Code Style |
| 128 | + |
| 129 | +- Add godoc comments to all exported functions, types, and constants |
| 130 | +- Avoid unnecessary code comments — only comment when the *why* isn't obvious from the code |
| 131 | +- Do not comment just to restate what the code does |
| 132 | + |
| 133 | +## Error Handling |
| 134 | + |
| 135 | +Error types in `pkg/cmdutil/errors.go`: |
| 136 | +- `FlagErrorf(...)` — flag validation (prints usage) |
| 137 | +- `cmdutil.SilentError` — exit 1, no message |
| 138 | +- `cmdutil.CancelError` — user cancelled |
| 139 | +- `cmdutil.PendingError` — outcome pending |
| 140 | +- `cmdutil.NoResultsError` — empty results |
| 141 | + |
| 142 | +Use `cmdutil.MutuallyExclusive("message", cond1, cond2)` for mutually exclusive flags. |
| 143 | + |
| 144 | +## Feature Detection |
| 145 | + |
| 146 | +Commands using feature detection must include a `// TODO <cleanupIdentifier>` comment directly above the if-statement for linter compliance: |
| 147 | + |
| 148 | +```go |
| 149 | +// TODO someFeatureCleanup |
| 150 | +if features.SomeCapability { |
| 151 | + // use new API |
| 152 | +} else { |
| 153 | + // fallback for older GHES |
| 154 | +} |
| 155 | +``` |
| 156 | + |
| 157 | +## API Patterns |
| 158 | + |
| 159 | +```go |
| 160 | +client := api.NewClientFromHTTP(httpClient) |
| 161 | +client.GraphQL(hostname, query, variables, &data) |
| 162 | +client.REST(hostname, "GET", "repos/owner/repo", nil, &data) |
| 163 | +``` |
| 164 | + |
| 165 | +For host resolution, use `cfg.Authentication().DefaultHost()` — not `ghinstance.Default()` which always returns `github.com`. |
0 commit comments