Skip to content

Commit dedb228

Browse files
Merge pull request cli#13024 from cli/agents-md
Add AGENTS.md
2 parents 1ea2952 + d10b56e commit dedb228

1 file changed

Lines changed: 165 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
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

Comments
 (0)