diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..98024f533 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,165 @@ +# AGENTS.md + +This is the GitHub CLI (`gh`), a command-line tool for interacting with GitHub. The module path is `github.com/cli/cli/v2`. + +## Build, Test, and Lint + +```bash +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) +``` + +## Architecture + +Entry point: `cmd/gh/main.go` → `internal/ghcmd.Main()` → `pkg/cmd/root.NewCmdRoot()`. + +Key packages: +- `pkg/cmd///` — CLI command implementations +- `pkg/cmdutil/` — Factory, error types, flag helpers (`NilStringFlag`, `NilBoolFlag`, `StringEnumFlag`) +- `pkg/iostreams/` — I/O abstraction with TTY detection, color, pager +- `pkg/httpmock/` — HTTP mocking for tests +- `api/` — GitHub API client (GraphQL + REST) +- `internal/featuredetection/` — GitHub.com vs GHES capability detection +- `internal/tableprinter/` — Table output for list commands + +## Command Structure + +A command `gh foo bar` lives in `pkg/cmd/foo/bar/` with `bar.go`, `bar_test.go`, and optionally `http.go`/`http_test.go`. + +### Canonical Examples + +- **Command + tests**: `pkg/cmd/issue/list/list.go` and `list_test.go` +- **Factory wiring**: `pkg/cmd/factory/default.go` +- **Unit tests**: `internal/agents/detect_test.go` + +### The Options + Factory Pattern + +Every command follows this structure (see `pkg/cmd/issue/list/list.go`): + +1. `Options` struct with `IO`, `HttpClient`, `Config`, `BaseRepo` + flags +2. `NewCmdFoo(f *cmdutil.Factory, runF func(*FooOptions) error)` constructor — `runF` is the test injection point +3. Separate `fooRun(opts)` function with the business logic + +Key rules: +- Lazy-init `BaseRepo`, `Remotes`, `Branch` inside `RunE`, not the constructor +- Commands register in `pkg/cmd/root/root.go`; subcommand groups use `cmdutil.AddGroup()` + +### Command Examples and Help Text + +Use `heredoc.Doc` for examples with `#` comment lines and `$ ` command prefixes: +```go +Example: heredoc.Doc(` + # Do the thing + $ gh foo bar --flag value +`), +``` + +### JSON Output + +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`. + +## Testing + +### HTTP Mocking + +Use `httpmock.Registry` with `defer reg.Verify(t)` to ensure all stubs are called: + +```go +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. + +### IOStreams in Tests + +```go +ios, stdin, stdout, stderr := iostreams.Test() +ios.SetStdoutTTY(true) // simulate terminal +``` + +### Assertions + +Use `testify`. Always use `require` (not `assert`) for error checks so the test halts immediately: + +```go +require.NoError(t, err) +require.Error(t, err) +assert.Equal(t, "expected", actual) +``` + +### Generated Mocks + +Interfaces use `moq`: `//go:generate moq -rm -out prompter_mock.go . Prompter`. Run `go generate ./...` after interface changes. + +### Table-Driven Tests + +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: + +```go +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 + }) +} +``` + +## Code Style + +- 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 Handling + +Error types in `pkg/cmdutil/errors.go`: +- `FlagErrorf(...)` — flag validation (prints usage) +- `cmdutil.SilentError` — exit 1, no message +- `cmdutil.CancelError` — user cancelled +- `cmdutil.PendingError` — outcome pending +- `cmdutil.NoResultsError` — empty results + +Use `cmdutil.MutuallyExclusive("message", cond1, cond2)` for mutually exclusive flags. + +## Feature Detection + +Commands using feature detection must include a `// TODO ` comment directly above the if-statement for linter compliance: + +```go +// TODO someFeatureCleanup +if features.SomeCapability { + // use new API +} else { + // fallback for older GHES +} +``` + +## API Patterns + +```go +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`. \ No newline at end of file