# GraphQL and the gh CLI

The GraphQL subset the gh CLI speaks at POST /graphql, and the gh commands that run against the simulator.

`POST /graphql` serves the subset of GitHub's GraphQL API that the `gh` CLI sends, and each mutation writes through the same handler as its REST route. Query the viewer and a repository's pull requests, comment through a mutation and read the comment back over REST:

```bash
H='Authorization: Bearer ghp_anything'
curl -s -X POST "$RYSTIC_URL/graphql" -H "$H" -d '{"query":"{ viewer { login } }"}' | jq -c .
# -> {"data":{"viewer":{"login":"tb-owner"}}}
curl -s -X POST "$RYSTIC_URL/graphql" -H "$H" -d '{"query":"{ repository(owner: \"tb-owner\", name: \"tb-repo\") { pullRequests(first: 2, states: OPEN) { totalCount nodes { number title isDraft } } } }"}' | jq -c '.data.repository.pullRequests'
# -> {"nodes":[{"isDraft":false,"number":44,"title":"Bump vendored dependencies"},{"isDraft":true,"number":43,"title":"WIP: split the store package"}],"totalCount":4}
curl -s -X POST "$RYSTIC_URL/graphql" -H "$H" -d '{"query":"mutation { addComment(input: {subjectId: \"tb-pr-41-node-id\", body: \"Looks good\"}) { commentEdge { node { body } } } }"}' | jq -c .
# -> {"data":{"addComment":{"commentEdge":{"node":{"body":"Looks good"}}}}}
curl -s "$RYSTIC_URL/repos/tb-owner/tb-repo/issues/41/comments" -H "$H" | jq -c '[.[].body]'
# -> ["Looks good"]
curl -s -X POST "$RYSTIC_URL/graphql" -H "$H" -d '{"query":"{ __type(name: \"SearchType\") { kind enumValues { name } } }"}' | jq -c '[.data.__type.enumValues[].name]'
# -> ["ISSUE","ISSUE_ADVANCED","ISSUE_SEMANTIC","ISSUE_HYBRID","REPOSITORY","USER","DISCUSSION"]
curl -s -X POST "$RYSTIC_URL/graphql" -H "$H" -d '{"query":"{ repository(owner: \"tb-owner\", name: \"tb-repo\") { pullRequest(number: 41) { reviewThreads(first: 1) { totalCount } } } }"}' | jq -c '.errors[0].extensions'
# -> {"code":"undefinedField","fieldName":"reviewThreads","typeName":"PullRequest"}
```

A GitHub Enterprise Server client sends GraphQL to `<host>/api/graphql`, which the simulator serves exactly as it serves `/graphql`.

### Queries

The simulator serves these queries, with their reviews, comments and check-run rollup, and a branch's protection rule:

| Where | Queries |
|---|---|
| top level | `node`, `repository`, `search`, `viewer` |
| on a repository | `issue`, `issueOrPullRequest`, `issues`, `pullRequest`, `pullRequests` |

A ref's `branchProtectionRule` and a repository's `branchProtectionRules` read the protection the REST routes write.

A rollup context's `isRequired`, asked with `pullRequestId` or `pullRequestNumber`, is whether that pull request's base requires a context of exactly its name. So `gh pr checks --required` lists the checks a protected base requires.

### Mutations

The simulator serves these mutations:

| Subject | Mutations |
|---|---|
| comments and reviews | `addComment`, `addPullRequestReview` |
| issues | `closeIssue`, `createIssue`, `reopenIssue`, `updateIssue` |
| pull requests | `closePullRequest`, `convertPullRequestToDraft`, `createPullRequest`, `markPullRequestReadyForReview`, `mergePullRequest`, `reopenPullRequest` |

Every mutation with a REST route writes through that route's handler, so what `gh` does shows on the REST surface. `convertPullRequestToDraft` and `markPullRequestReadyForReview` have no REST route and write the pull request directly.

### Introspection

`__type` returns what `gh`'s feature detection reads. That is a type's `name`, `kind`, `fields` and `enumValues`, and each field's `args`.

**Differs from GitHub:** `__type` fills only those fields: `description` reads `null`, and others such as `interfaces` return `undefinedField`. The simulator doesn't serve `__schema` either, which returns `undefinedField` too.

### Outside the subset

A field outside the subset returns GitHub's `undefinedField` error.

**Differs from GitHub:** hiding a comment (`minimizeComment`), resolving a review thread (`resolveReviewThread`), adding a thread to a pending review (`addPullRequestReviewThread`) and a pull request's `reviewThreads` return `undefinedField`.

**Differs from GitHub:** project items return empty.

**Differs from GitHub:** repository administration is not modeled.

The merge queue and auto-merge fields on a pull request (`autoMergeRequest`, `isInMergeQueue`, `isMergeQueueEnabled` and `mergeQueueEntry`) are covered by [Auto-merge](https://www.rystic.ai/docs/simulators/github-pulls/features/auto-merge.md) and [Merge queues](https://www.rystic.ai/docs/simulators/github-pulls/features/merge-queue.md).

### Checks and status contexts

`statusCheckRollup` carries a commit's check runs and, as `StatusContext`, the latest commit status per context.

### The gh CLI

`gh api` is REST, and every `gh pr` and `gh issue` command is GraphQL. The simulator serves both. With `GH_HOST=github.localhost`, `gh` speaks plain HTTP to `api.github.localhost` and honors `HTTP_PROXY`, so the simulator's URL is the proxy:

```sh
export GH_HOST=github.localhost GH_TOKEN=ghp_anything HTTP_PROXY=$RYSTIC_URL
gh pr list -R tb-owner/tb-repo
gh pr view 41 -R tb-owner/tb-repo --comments
gh pr review 41 -R tb-owner/tb-repo --approve
gh pr merge 41 -R tb-owner/tb-repo --squash
gh issue create -R tb-owner/tb-repo --title "An issue" --body "body"
gh pr list -R tb-owner/tb-repo --author tb-contributor --label bug
gh api repos/tb-owner/tb-repo/pulls/41 --jq '.state, .merged'
```

`gh pr list` and `gh issue list` turn these flags into one GraphQL `search` query in the advanced syntax, after reading `SearchType`'s values by introspection:

- `--author`, `--assignee` and `--label`
- `--draft`, which lists drafts only
- `--search`, where `"merged:2026-09-01..2026-09-07"` counts that window and free text narrows the list

So they filter by the grammar in [Issue and pull request search](https://www.rystic.ai/docs/simulators/github-pulls/features/search.md). A qualifier the simulator cannot evaluate fails the command with the error [Refusals](https://www.rystic.ai/docs/simulators/github-pulls/features/search.md#refusals) shows, which doesn't name the qualifier.

`gh` acts as `member`, since it has no way to send `X-Rystic-Actor`.

---

Source: https://www.rystic.ai/docs/simulators/github-pulls/features/graphql · Markdown: https://www.rystic.ai/docs/simulators/github-pulls/features/graphql.md
