Open-Source Wikis

/

GitHub CLI

/

How to contribute

/

Patterns and conventions

cli/cli

Patterns and conventions

The conventions in this section are repeated across hundreds of command packages. Following them is mandatory; CI lint catches most deviations.

The Options + Factory pattern

Every gh foo bar lives in pkg/cmd/foo/bar/bar.go and exposes:

type BarOptions struct {
    IO         *iostreams.IOStreams
    HttpClient func() (*http.Client, error)
    Config     func() (gh.Config, error)
    BaseRepo   func() (ghrepo.Interface, error)

    // ...flags...
    Limit int
    State string
    Web   bool
}

func NewCmdBar(f *cmdutil.Factory, runF func(*BarOptions) error) *cobra.Command {
    opts := &BarOptions{
        IO:         f.IOStreams,
        HttpClient: f.HttpClient,
        Config:     f.Config,
    }
    cmd := &cobra.Command{
        Use:     "bar [<flags>]",
        Short:   "Do the thing",
        Example: heredoc.Doc(`
            # Do the thing
            $ gh foo bar --flag value
        `),
        RunE: func(cmd *cobra.Command, args []string) error {
            opts.BaseRepo = f.BaseRepo // lazy: must happen here, not above
            if runF != nil {
                return runF(opts)
            }
            return barRun(opts)
        },
    }
    cmd.Flags().IntVarP(&opts.Limit, "limit", "L", 30, "Maximum results")
    return cmd
}

func barRun(opts *BarOptions) error {
    // business logic
}

Rules that come up in code review:

  • opts.BaseRepo, opts.Branch, and opts.Remotes are assigned inside RunE, never inside NewCmdBar. The factory closures capture state that is not safe to evaluate at construction time (for gh version, gh completion, etc.).
  • runF is the test injection point. Tests pass a stub that captures opts and asserts against it. Production callers leave it nil.
  • Business logic lives in barRun (or similar). The constructor only handles flag parsing and validation.

Help text style

Use heredoc.Doc for Long and Example. Examples use # comment lines plus $ command lines. Example from gh issue list:

Example: heredoc.Doc(`
    $ gh issue list -l "bug" -l "help wanted"
    $ gh issue list -A monalisa
    $ gh issue list -a "@me"
    $ gh issue list -s "open"
`),

JSON output

For list-style commands, add JSON flags via the shared helper:

cmdutil.AddJSONFlags(cmd, &opts.Exporter, []string{
    "id", "number", "title", "state", "createdAt",
})

In the run function:

if opts.Exporter != nil {
    return opts.Exporter.Write(opts.IO, data)
}

Pair the change with an entry in the jsonfieldstest so each declared field is asserted to exist on the struct.

Error types

pkg/cmdutil/errors.go defines the project's typed errors:

Error Use
cmdutil.FlagErrorf("...") Flag validation. Cobra prints usage.
cmdutil.SilentError Non-zero exit, no message (the command already printed).
cmdutil.CancelError User cancelled. Maps to exit code 2.
cmdutil.PendingError Asynchronous result pending. Maps to exit code 8.
cmdutil.NoResultsError "no found"; treated as silent in some contexts.

Mutually exclusive flags use cmdutil.MutuallyExclusive("message", cond1, cond2).

Feature detection

Capability differences between GitHub.com and GHES are handled at runtime with internal/featuredetection. Every detected branch needs a tracked cleanup ID:

// TODO ApiActorsSupported
if features.ApiActorsSupported {
    // new path
} else {
    // GHES fallback
}

The // TODO <cleanupIdentifier> comment is required by the lint; it makes it trivial to grep for the cleanup work after the older GHES is no longer supported.

API access

client := api.NewClientFromHTTP(httpClient)
client.GraphQL(hostname, query, variables, &data)
client.REST(hostname, "GET", "repos/owner/repo", nil, &data)

For host resolution, prefer cfg.Authentication().DefaultHost() over ghinstance.Default(), because the former honours GH_HOST and per-user defaults.

Code style

  • Add godoc comments to all exported functions, types, and constants.
  • Do not write inline comments that restate the code. Comment the why, not the what.
  • Never use em dashes (-) in code, comments, or docs. The lint enforces this via a custom forbidigo rule.
  • Prefer small, composable functions; commands should be testable end-to-end with mocked dependencies.

Cross-cutting subsystems

Pages that go deeper on shared infrastructure:

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – GitHub CLI wiki | Factory