# Understanding the ClientOps Interface in gh-stack: How It Wraps GitHub API Calls

> Discover the ClientOps interface in gh-stack, a Go abstraction that unifies and simplifies GitHub API calls for efficient stacked PR management and testing.

- Repository: [GitHub/gh-stack](https://github.com/github/gh-stack)
- Tags: deep-dive
- Published: 2026-08-03

---

**The `ClientOps` interface is a Go abstraction declared in [`internal/github/client_interface.go`](https://github.com/github/gh-stack/blob/main/internal/github/client_interface.go) (lines 6–25) that encapsulates all GitHub API interactions—both GraphQL queries for pull requests and REST calls for stack management—enabling the gh-stack CLI to operate on stacked PRs through a unified, testable contract.**

The `gh-stack` extension orchestrates stacked pull requests by interacting with GitHub's GraphQL and REST APIs through a thin abstraction layer. At the core of this architecture lies the **`ClientOps` interface**, which defines the complete set of operations required to query pull requests, manage stacks, and handle merge queues. By programming against this interface rather than concrete HTTP clients, gh-stack achieves clean separation between business logic and API transport concerns.

## The ClientOps Interface Declaration

The interface is defined in [`internal/github/client_interface.go`](https://github.com/github/gh-stack/blob/main/internal/github/client_interface.go) and comprises seventeen methods spanning three functional domains:

```go
type ClientOps interface {
    FindPRForBranch(branch string) (*PullRequest, error)
    FindPRByNumber(number int) (*PullRequest, error)
    FindPRDetailsForBranch(branch string) (*PRDetails, error)
    CreatePR(base, head, title, body string, draft bool) (*PullRequest, error)
    UpdatePRBase(number int, base string) error
    MarkPRReadyForReview(prID string) error
    DisableAutoMerge(prID string) error
    ListStacks() ([]RemoteStack, error)
    FindStackForPR(prNumber int) (*RemoteStack, error)
    GetStack(stackNumber int) (*RemoteStack, error)
    CreateStack(prNumbers []int) (*RemoteStack, error)
    AddToStack(stackNumber int, prNumbers []int) (*RemoteStack, error)
    Unstack(stackNumber int) (*RemoteStack, bool, error)
    RepoMergeConfig() (*RepoMergeConfig, error)
    MergeStackAsync(prNumber int, method, mergeAction string) (*AsyncMergeResult, error)
    GetAsyncMergeResult(prNumber int, uuid string) (*AsyncMergeResult, error)
    PRTitles(numbers []int) (map[int]string, error)
    BaseBranchUsesMergeQueue(baseRef string) (bool, error)
}

```

### GraphQL Query Methods

Methods like `FindPRForBranch` and `FindPRByNumber` execute GraphQL queries against GitHub's API. For example, `FindPRForBranch` constructs a query filtering by `headRefName` to locate the first open pull request for a given branch. Similarly, `RepoMergeConfig` and `BaseBranchUsesMergeQueue` query repository merge queue settings through GraphQL.

### GraphQL Mutation Methods

Write operations including `CreatePR`, `UpdatePRBase`, `MarkPRReadyForReview`, and `DisableAutoMerge` use GraphQL mutations. `CreatePR` accepts parameters for base branch, head branch, title, body, and draft status to instantiate new pull requests.

### REST API Methods

Stack management and async merge functionality utilize GitHub's REST endpoints:

- **Stack operations**: `ListStacks` performs a GET request to `/repos/{owner}/{repo}/stacks`, while `CreateStack` sends a POST to the same endpoint. `AddToStack` uses PATCH on `/stacks/:id`, and `Unstack` sends DELETE.
- **Async merge**: `MergeStackAsync` POSTs to `/pulls/:n/merge-async`, and `GetAsyncMergeResult` polls GET `/pulls/:n/merge-async/:uuid`.
- **Batch operations**: `PRTitles` retrieves pull request titles in batch via REST.

## Concrete Implementation with the Client Type

The production implementation resides in [`internal/github/github.go`](https://github.com/github/gh-stack/blob/main/internal/github/github.go) (lines 53–61) as the `Client` struct:

```go
type Client struct {
    gql   *api.GraphQLClient   // go-gh GraphQL client
    rest  *api.RESTClient      // go-gh REST client
    host  string
    owner string
    repo  string
    slug  string // "owner/repo"
}

```

Each `ClientOps` method is implemented on this struct. **GraphQL operations** invoke `c.gql.Query` or `c.gql.Mutate`, while **REST operations** call `c.rest.Request`. The underlying `go-gh` library (`github.com/cli/go-gh/v2/pkg/api`) automatically handles OAuth authentication, rate limiting, and response pagination.

## Dependency Injection and Usage Patterns

Commands throughout gh-stack consume the interface through `config.Config.GitHubClientOverride`, enabling both production and test implementations.

### Production Initialization

In commands like [`cmd/submit.go`](https://github.com/github/gh-stack/blob/main/cmd/submit.go), the extension instantiates a real client:

```go
cfg.GitHubClientOverride, err = github.NewClient(host, owner, repo)

```

This constructor configures the GraphQL and REST clients with the target repository's host, owner, and name (stored in the `slug` field as `"owner/repo"`).

### Testing with MockClient

The `MockClient` type in [`internal/github/mock_client.go`](https://github.com/github/gh-stack/blob/main/internal/github/mock_client.go) satisfies the `ClientOps` interface, allowing unit tests to inject predetermined responses without network I/O.

### Command Execution Flow

A typical workflow such as `gh stack submit` demonstrates the interface in action:

1. **Discovery**: `client.FindPRForBranch(currentBranch)` locates the existing PR for the current branch via GraphQL.
2. **Stack creation**: `client.CreateStack([]int{pr.Number})` sends a REST POST to establish a new stack entity.
3. **Modification**: `client.AddToStack(stackID, []int{newPR.Number})` PATCHes the stack to include additional pull requests.
4. **Merge initiation**: `client.MergeStackAsync(prNumber, method, mergeAction)` triggers the async merge process via REST.

## Code Examples

### Querying a Pull Request by Branch

```go
func getPRForBranch(cfg *config.Config, branch string) (*github.PullRequest, error) {
    // cfg.GitHubClientOverride satisfies ClientOps
    client := cfg.GitHubClientOverride
    return client.FindPRForBranch(branch)
}

```

### Creating and Populating a Stack

```go
func createAndPopulateStack(cfg *config.Config, prNumber int, additionalPRs []int) (*github.RemoteStack, error) {
    client := cfg.GitHubClientOverride
    
    // Create initial stack
    stack, err := client.CreateStack([]int{prNumber})
    if err != nil {
        return nil, err
    }
    
    // Add remaining PRs
    return client.AddToStack(stack.Number, additionalPRs)
}

```

### Checking Merge Queue Status

```go
func checkMergeQueue(cfg *config.Config, baseRef string) (bool, error) {
    client := cfg.GitHubClientOverride
    return client.BaseBranchUsesMergeQueue(baseRef)
}

```

### Initiating Async Merge

```go
func asyncMerge(cfg *config.Config, prNumber int, method, action string) (*github.AsyncMergeResult, error) {
    client := cfg.GitHubClientOverride
    return client.MergeStackAsync(prNumber, method, action)
}

```

## Summary

- The **`ClientOps` interface** in [`internal/github/client_interface.go`](https://github.com/github/gh-stack/blob/main/internal/github/client_interface.go) (lines 6–25) defines the complete contract for GitHub API operations in gh-stack.
- The **`Client`** struct in [`internal/github/github.go`](https://github.com/github/gh-stack/blob/main/internal/github/github.go) provides the concrete implementation using `go-gh` GraphQL and REST clients.
- **GraphQL methods** handle pull request queries and mutations, while **REST methods** manage the Stacks API and asynchronous merge endpoints.
- The interface enables **dependency injection**, allowing production commands to use real API clients and tests to use `MockClient` for isolation.
- All high-level commands access GitHub through `config.GitHubClientOverride`, ensuring consistent authentication and error handling across the codebase.

## Frequently Asked Questions

### What specific GitHub API endpoints does ClientOps wrap?

The interface wraps GraphQL queries for pull request data (filtering by `headRefName` and PR number) and REST endpoints for stack management (`/repos/{owner}/{repo}/stacks`) and async merges (`/pulls/:n/merge-async`). Specific methods like `ListStacks` use REST GET operations, while `CreatePR` executes GraphQL mutations.

### How does the Client implementation handle authentication?

The concrete `Client` type relies on the `go-gh` library (`github.com/cli/go-gh/v2/pkg/api`), which automatically manages OAuth tokens, request headers, rate limiting, and pagination. The `Client` struct stores pre-configured `*api.GraphQLClient` and `*api.RESTClient` instances initialized with the user's GitHub CLI credentials.

### Why does gh-stack use an interface for GitHub operations?

Using the `ClientOps` interface decouples command logic from HTTP implementation details. This abstraction allows commands in `cmd/` packages to remain agnostic of whether they're calling real GitHub APIs or mock implementations, facilitating unit testing through `github.MockClient` and insulating business logic from changes in the underlying API transport layer.

### Where can I find the implementation of specific methods like MergeStackAsync?

Method-specific implementations are distributed across the `internal/github/` directory. The core `Client` struct and common methods reside in [`internal/github/github.go`](https://github.com/github/gh-stack/blob/main/internal/github/github.go), while async merge specific logic appears in [`internal/github/merge_async.go`](https://github.com/github/gh-stack/blob/main/internal/github/merge_async.go). The interface definition in [`internal/github/client_interface.go`](https://github.com/github/gh-stack/blob/main/internal/github/client_interface.go) links all implementations through the `ClientOps` contract.