> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub

> REST and GraphQL: repositories, issues, pull requests, organizations, webhooks, Actions, Checks, OAuth Apps, and GitHub Apps.

Stateful GitHub REST and GraphQL emulation: users, repositories, contents and git data, issues, pull requests with real merges and branch protection, organizations and teams, releases, search, Actions, Checks, webhooks, OAuth Apps, and GitHub Apps. Creates, updates, and deletes persist in memory and affect related entities. The tests drive it with `@octokit/rest`, `@octokit/app`, and `@octokit/webhooks`.

## At a glance

| | |
| - | - |
| Package | `@deploybaseline/github` |
| Port | `4001` in the default set, `4000` when started alone |
| Config key | `github` |
| HTTP operations | 263, browsable at `http://localhost:3999/docs/github` while it runs ([API reference](/api-reference.md)) |
| GraphQL root fields | 8 |
| Vendor spec | [github/rest-api-description](https://github.com/github/rest-api-description) at `755ee33` |

```bash CLI theme={null}
npx @deploybaseline/cli start --service github
```

```ts Programmatic theme={null}
import { createEmulator } from "@deploybaseline/cli";

const github = await createEmulator({ service: "github" });
// github.url is http://localhost:4000
```

```json baseline.config.json theme={null}
{
  "github": {
    "users": [
      {
        "login": "octocat",
        "name": "The Octocat",
        "email": "octocat@github.com",
        "bio": "I am the Octocat",
        "company": "GitHub",
        "location": "San Francisco"
      }
    ],
    "orgs": [
      {
        "login": "my-org",
        "name": "My Organization",
        "description": "A test organization",
        "members": [
          {
            "login": "octocat",
            "role": "admin"
          }
        ]
      }
    ],
    "repos": [
      {
        "owner": "octocat",
        "name": "hello-world",
        "description": "My first repository",
        "language": "JavaScript",
        "topics": ["hello", "world"],
        "auto_init": true
      },
      {
        "owner": "my-org",
        "name": "org-repo",
        "description": "An organization repository",
        "language": "TypeScript",
        "auto_init": true
      }
    ],
    "oauth_apps": [
      {
        "client_id": "Iv1.example_client_id",
        "client_secret": "example_client_secret",
        "name": "My App",
        "redirect_uris": ["http://localhost:3000/api/auth/callback/github"]
      }
    ]
  }
}
```

## Start

```bash theme={null}
npx @deploybaseline/cli start --service github
```

GitHub is a default provider and listens on `http://localhost:4001`. Without a seed config there are two users, `admin` and `ghost`, and the token `test_token_admin` signs in as `admin`.

Point Octokit at the emulator with `baseUrl`:

```ts theme={null}
import { Octokit } from "@octokit/rest";

const octokit = new Octokit({ baseUrl: "http://localhost:4001", auth: "test_token_admin" });
const { data: repo } = await octokit.rest.repos.get({ owner: "octocat", repo: "hello-world" });
```

Pagination uses `page`/`per_page` with `Link` headers.

## Auth

Tokens from the seed config go in `Authorization: Bearer <token>` or `Authorization: token <token>` (see [Authentication](/authentication.md#tokens-from-the-config-file)). Public repo endpoints work without auth. Private repos and write operations require a valid token.

### Repository roles

Repository access follows GitHub's roles. Owners of a personal repository and organization owners are admins. A plain organization member gets the organization's `default_repository_permission` (`read` unless changed with `PATCH /orgs/:org`), raised by the role of any team that has the repository (a child team inherits its parent's) and by a collaborator grant. The repository's `permissions` and `GET /repos/:owner/:repo/collaborators/:username/permission` report that role; `permission` uses the classic names (`maintain` reads as `write`, `triage` as `read`) and `role_name` the role itself.

What each role may write, as on GitHub: anyone signed in may comment, review, and open pull requests on a repository they can see. Editing or deleting a comment (issue, review, or commit comment) or editing a pull request needs its author or push access; triage may only close or reopen a pull request. A locked conversation takes comments only from people with push access (`403 Unable to create comment because issue is locked.`). Requesting or removing reviewers needs triage. Merging, updating a pull request branch, dismissing a review, and writing check runs and check suites need push access, and check suite preferences need admin. A pending review is visible only to its author, who alone may update or submit it, and each person may have one per pull request; approving or requesting changes on your own pull request is refused with 422 (`Can not approve your own pull request`), as is dismissing a `COMMENTED` review. These 422 responses carry GitHub's `errors` list. An app installation needs the matching permission at `write` instead (`issues`, `pull_requests`, `contents`, or `checks`).

### OAuth Apps

Register OAuth applications so the emulator validates `client_id`, `client_secret`, and `redirect_uri` during the OAuth flow:

```json theme={null}
{
  "github": {
    "oauth_apps": [
      {
        "client_id": "Iv1.abc123",
        "client_secret": "secret_abc123",
        "name": "My Web App",
        "redirect_uris": ["http://localhost:3000/api/auth/callback/github"]
      }
    ]
  }
}
```

If no `oauth_apps` are configured, the emulator accepts any `client_id` (backward-compatible). With apps configured, strict validation is enforced.

### GitHub Apps

Full GitHub App support with JWT authentication and installation access tokens:

```json theme={null}
{
  "github": {
    "apps": [
      {
        "app_id": 12345,
        "slug": "my-github-app",
        "name": "My GitHub App",
        "permissions": {
          "contents": "read",
          "issues": "write"
        },
        "events": ["push", "pull_request"],
        "webhook_url": "http://localhost:3000/webhooks/github",
        "webhook_secret": "my-secret",
        "installations": [
          {
            "installation_id": 100,
            "account": "my-org",
            "repository_selection": "all",
            "permissions": {
              "contents": "read"
            }
          }
        ]
      }
    ]
  }
}
```

* `GET /app` — Get the authenticated app: JWT required
* `GET /app/installations` — List app installations
* `GET /app/installations/:id` — Get installation
* `POST /app/installations/:id/access_tokens` — Create installation access token
* `GET /repos/:owner/:repo/installation` — Get repo installation
* `GET /orgs/:org/installation` — Get org installation
* `GET /users/:username/installation` — Get user installation

Sign a JWT with `{ iss: "<app_id>" }` using the app's private key (RS256). The emulator verifies the signature and resolves the app. With `createEmulator`, omit `private_key` and read the generated key from `generatedSecrets` (see [GitHub App keys](#github-app-keys)). The CLI generates an omitted key only with `--generated-secrets-file <path>`; otherwise it requires an explicit, valid private key. Do not replace the omitted field with a fake PEM placeholder.

Installation access tokens act as the configured GitHub App bot for repository writes. Repository ownership, selected repository access, and requested App permissions remain enforced. Pull request merges require `contents: write` on the base repository. Pull request branch updates require `pull_requests: write` on the pull request repository and `contents: write` on the head repository.

App webhook delivery mirrors GitHub. Every webhook payload on a repository where an app is installed, including repository and organization hooks, carries an `installation` field with `{ id, node_id }`. If the app has a `webhook_url`, the emulator also delivers the event there, with an `X-Hub-Signature-256` header signed with `webhook_secret` when one is set.

### GitHub App keys

The programmatic `createEmulator` API can generate a GitHub App key when `private_key` is omitted:

```typescript theme={null}
const github = await createEmulator({
  service: 'github',
  seed: {
    github: {
      users: [{ login: 'octocat' }],
      apps: [{
        app_id: 12345,
        slug: 'my-github-app',
        name: 'My GitHub App',
        installations: [{ installation_id: 100, account: 'octocat' }],
      }],
    },
  },
})

const privateKey = github.generatedSecrets.find(
  secret => secret.kind === 'github.app_private_key' && secret.id === '12345',
)?.value
```

The generated RSA-2048 PKCS#1 key remains stable across `reset()` calls. Explicit keys are never included in `generatedSecrets`.

The CLI can generate omitted keys when you request a private delivery file:

```bash theme={null}
npx @deploybaseline/cli start --service github --seed baseline.config.json \
  --generated-secrets-file .baseline-secrets.json
```

The destination must not exist. Baseline removes inherited ACLs, verifies effective owner-only access, and publishes complete JSON before opening listeners. Handled startup failures remove the invocation-owned artifact. A hard termination can leave a complete artifact that must be removed manually after confirming no invocation is using it. Only generated keys appear in the artifact. Linux requires `setfacl` and `getfacl` from the `acl` package. The flag fails closed when access controls cannot be verified and is not supported on Windows. Without `--generated-secrets-file`, CLI seed files still require `private_key`. Direct `seedFromConfig` calls always require it. To generate omitted keys before calling `seedFromConfig` yourself, use `materializeGitHubSeedConfig` and keep the returned key material:

```typescript theme={null}
import { materializeGitHubSeedConfig, seedFromConfig } from '@deploybaseline/github'

const materialized = await materializeGitHubSeedConfig({
  apps: [{ app_id: 12345, slug: 'my-github-app', name: 'My GitHub App' }],
})

seedFromConfig(store, baseUrl, materialized.config)
const privateKey = materialized.generatedPrivateKeys[0]?.private_key
```

### Installation token inspection

* `GET /_baseline/installation-tokens` — Inspect minted installation tokens: secret-free metadata

The route lists metadata for GitHub App installation tokens minted by the current emulator state. It includes App, installation, account, permissions, repository access, issuance, expiry, and lifecycle status without exposing token values or token-derived identifiers.

Expiry is informational and does not change authorization behavior. `createEmulator().reset()` clears minted installation-specific authorization and metadata while preserving seeded tokens, generated App keys, and existing fallback-token behavior. Persisted Next.js and Nuxt adapters restore the metadata with their private snapshot.

## Users

* `GET /user` — Get the authenticated user
* `PATCH /user` — Update profile
* `GET /users/:username` — Get user
* `GET /users` — List users
* `GET /users/:username/repos` — List user repos
* `GET /users/:username/orgs` — List user orgs
* `GET /users/:username/followers` — List followers
* `GET /users/:username/following` — List following

## Repositories

* `GET /repos/:owner/:repo` — Get repo
* `GET /repositories/:id` — Get repo by numeric ID
* `POST /user/repos` — Create user repo
* `GET /orgs/:org/repos` — List org repos (`type`: all, public, private, forks, sources, member; `sort`, `direction`); private ones only for people who can see them
* `POST /orgs/:org/repos` — Create org repo
* `PATCH /repos/:owner/:repo` — Update repo
* `DELETE /repos/:owner/:repo` — Delete repo (cascades)
* `GET/PUT /repos/:owner/:repo/topics` — Get or replace topics
* `GET /repos/:owner/:repo/languages` — List languages
* `GET /repos/:owner/:repo/contributors` — List contributors
* `GET /repos/:owner/:repo/forks` — List forks
* `POST /repos/:owner/:repo/forks` — Create fork (with the parent's history; `default_branch_only` copies only the default branch)
* `GET/PUT/DELETE /repos/:owner/:repo/collaborators/:username` — Check, add, or remove a collaborator
* `GET /repos/:owner/:repo/collaborators/:username/permission` — Get a collaborator's permission
* `POST /repos/:owner/:repo/transfer` — Transfer repo (202 Accepted)
* `GET /repos/:owner/:repo/tags` — List tags: the refs under `refs/tags`, newest version first, each with the commit it points at (through an annotated tag); a tag object without a ref is not listed

## Contents and commit history

* `GET /repos/:owner/:repo/readme` — Get the repository README
* `GET /repos/:owner/:repo/contents/:path` — Get a file or list a directory: at a ref
* Send `Accept: application/vnd.github.raw` or `application/vnd.github.raw+json` to file Contents and README requests to receive raw bytes; directory and submodule responses remain JSON
* `GET /:owner/:repo/raw/:ref/:path` — Download raw file content: from advertised raw URLs; this is separate from Accept negotiation
* `PUT/DELETE /repos/:owner/:repo/contents/:path` — Write or delete a file: create, update, or delete a file and commit the change
* `GET /repos/:owner/:repo/commits` — List commits: with ref, path, author, and date filters
* `GET /repos/:owner/:repo/commits/:ref` — Get a commit: with file diffs and stats
* `GET /repos/:owner/:repo/compare/:base...:head` — Compare two refs
* `POST /repos/:owner/:repo/statuses/:sha` — Create a commit status (`state`, `context`, `target_url`, `description`; sends `status`)
* `GET /repos/:owner/:repo/commits/:ref/statuses` — List statuses: newest first
* `GET /repos/:owner/:repo/commits/:ref/status` — Get combined status (latest per context; `failure`, `pending`, or `success`)

## Issues

* `GET /repos/:owner/:repo/issues` — List issues (filter by state, labels, assignee, milestone, creator, since)
* `POST /repos/:owner/:repo/issues` — Create issue
* `GET /repos/:owner/:repo/issues/:number` — Get issue
* `PATCH /repos/:owner/:repo/issues/:number` — Update issue (state transitions, events)
* `PUT/DELETE /repos/:owner/:repo/issues/:number/lock` — Lock or unlock issue
* `GET /repos/:owner/:repo/issues/:number/timeline` — List timeline events
* `GET /repos/:owner/:repo/issues/:number/events` — List issue events
* `POST/DELETE /repos/:owner/:repo/issues/:number/assignees` — Manage assignees

Every pull request is also an issue, as on GitHub. The issue list includes pull requests, each with a `pull_request` key (`url`, `html_url`, `diff_url`, `patch_url`, `merged_at`) and `draft`; filter on that key to keep only issues. The routes above accept a pull request's number, and title, body, state, assignee, label, milestone, and lock changes made through them land on the pull request and send `pull_request` webhooks instead of `issues`. A pull request has no `state_reason`, and a merged one cannot be reopened (422).

## Pull requests

* `GET /repos/:owner/:repo/pulls` — List pull requests (filter by state, head, base)
* `POST /repos/:owner/:repo/pulls` — Create pull request
* `GET /repos/:owner/:repo/pulls/:number` — Get pull request
* `PATCH /repos/:owner/:repo/pulls/:number` — Update pull request
* `PUT /repos/:owner/:repo/pulls/:number/merge` — Merge pull request: with `merge`, `squash`, or `rebase` (with branch protection enforcement)
* `GET /repos/:owner/:repo/pulls/:number/commits` — List pull request commits: the commits on head that base does not have
* `GET /repos/:owner/:repo/pulls/:number/files` — List changed files: with patches (three-dot diff)
* `POST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers` — Manage reviewers
* `PUT /repos/:owner/:repo/pulls/:number/update-branch` — Update branch: merge the base into the head

### Merge state

An open pull request follows its branches. Whenever the head or base branch moves (a contents write, a ref update, a merge), the emulator compares them again with a real three-way merge of the stored git objects:

* `commits`, `additions`, `deletions`, and `changed_files` count the changes since head left base.
* `mergeable` is `false` when the merge conflicts: the same lines (or touching lines) changed differently on both sides, a file deleted on one side and changed on the other, or a binary file changed on both. Changes to different parts of a file merge.
* `merge_commit_sha` is the test merge commit (`Merge <head> into <base>`), and `null` when the merge conflicts.
* `rebaseable` is `false` when replaying the commits one at a time conflicts, or would give a different result than the merge.
* `mergeable_state` is `dirty` (conflicts), `draft`, `blocked` (branch protection unmet), `behind` (strict status checks and the head lacks the latest base), `unstable` (a check run or commit status that is not required failed or is pending), or `clean`. A merged pull request reports `mergeable: null` and `unknown`.

### Closing keywords

A pull request merged into the default branch closes the open issues its body names after a closing keyword: `close`, `closes`, `closed`, `fix`, `fixes`, `fixed`, `resolve`, `resolves`, or `resolved`, followed by `#12` or `owner/repo#12` (an optional colon is allowed). This applies whether it merges through the merge endpoint or because its commits were pushed to the base. A commit that reaches the default branch, pushed directly or merged in, closes the issues its message names in the same way. A pull request into another branch closes nothing.

Each issue closes with `state_reason: completed` and `closed_by` set to whoever merged or pushed. A `closed` issue event carries `commit_id` and `commit_url` for the commit, and `issues` `closed` is sent. Issues that are already closed are left as they are. An issue in another repository closes only when the person merging can push to that repository; GitHub's exact rule for that case is not documented, so this is the emulator's choice.

### Branch protection

The protection rule on the base branch decides `blocked` and `behind`, and the merge endpoint enforces it:

* **Required status checks.** Each context passes with a successful check run (`success`, `neutral`, or `skipped`) or a `success` commit status of that name on the head commit; the latest of each counts. With `strict`, the head must contain the base's latest commit.
* **Required reviews.** Each reviewer's latest review counts, and only from people with write access to the base repository (not the author). A request for changes from such a reviewer blocks until they approve. `dismiss_stale_reviews` dismisses approvals when new commits reach the head (sending `pull_request_review` `dismissed`). `require_code_owner_reviews` needs an approval from an owner of each changed file, read from `CODEOWNERS` on the base branch (`.github/`, the root, or `docs/`; owners are `@user`, `@org/team`, or an email; the last matching rule wins).
* **Restrictions.** Only the listed users and teams can merge.
* **Linear history.** Only `squash` and `rebase` merges.
* **Admins.** Repository admins merge without meeting the rule unless `enforce_admins` is on; the pull request still reports `blocked`.

Merge errors are 405 with GitHub's wording where it is known (`Required status check "ci" is expected.`, `Required status check "ci" is failing.`, `At least 1 approving review is required by reviewers with write access.`, `Waiting on code owner review from @mona.`); the emulator's own wording for the rest is `Head branch is not up to date with the base branch.`, `Changes requested by a reviewer with write access.`, `You're not authorized to push to this branch.`, and `Merge commits are not allowed on this branch.`

A moved head sends `pull_request` with `action: "synchronize"` (`before` and `after`). If the head's commits reach the base some other way (a push to the base), the pull request is marked merged; if the base branch is deleted, it is closed.

### Merging

* `merge` makes a merge commit with the base and head as parents (default message `Merge pull request #N from owner:branch`, then the title). `squash` makes one commit on the base, authored by the pull request's author (default title `<title> (#N)`, then the commits as a list). `rebase` replays each commit onto the base, keeping its author. GitHub (`noreply@github.com`) is the committer.
* Errors follow GitHub: 405 `Pull Request is not mergeable` for conflicts or a closed pull request, 405 for a draft, a merge method the repository turned off, an unmet [protection rule](#branch-protection), or a rebase that cannot be done (`This branch can't be rebased`), and 409 `Head branch was modified. Review and try the merge again.` when `sha` is stale.
* `delete_branch_on_merge` deletes the head branch when it is in the same repository.

`update-branch` merges the base into the head (`Merge branch 'main' into feature`), returns 422 on a conflict, and does nothing when the head already has the base. Pull requests from forks work across repositories: a fork starts with its parent's history, and the head's commits are copied into the base repository for comparing and merging.

Creating a pull request still creates a missing head or base branch at the default branch, which GitHub does not do.

## Comments

* Issue comments: full CRUD on `/repos/:owner/:repo/issues/:number/comments`
* Review comments: full CRUD on `/repos/:owner/:repo/pulls/:number/comments`. Reply with `in_reply_to` (or `in_reply_to_id`) or `POST /repos/:owner/:repo/pulls/:number/comments/:comment_id/replies`; a reply takes its parent's file and line, and replies thread to the top comment
* Commit comments: full CRUD on `/repos/:owner/:repo/commits/:sha/comments`
* Repo-wide listings for each type

## Reviews

* `GET /repos/:owner/:repo/pulls/:number/reviews` — List reviews
* `POST /repos/:owner/:repo/pulls/:number/reviews` — Create review (with inline comments)
* `GET/PUT /repos/:owner/:repo/pulls/:number/reviews/:id` — Get or update review
* `POST /repos/:owner/:repo/pulls/:number/reviews/:id/events` — Submit review
* `PUT /repos/:owner/:repo/pulls/:number/reviews/:id/dismissals` — Dismiss review

## Labels and milestones

* Labels: full CRUD, add/remove from issues, replace all
* Milestones: full CRUD, state transitions, issue counts

## Branches and Git data

* Branches: list, get, protection CRUD (status checks, PR reviews, enforce admins)
* Refs: get, match, create, update, delete. Creating, moving, and deleting a branch or tag sends `push` (with `created`, `deleted`, `forced`, and the pushed `commits` with their added, removed, and modified files); creating and deleting also send `create` and `delete`, which name the branch or tag without the `refs/heads/` or `refs/tags/` prefix, as GitHub does
* Commits: get, create
* Trees: get (with recursive), create (with inline content)
* Blobs: get, create
* Tags: get, create

## Organizations and teams

* Orgs: get, update, list
* Org members: list, check, remove, get/set membership
* Org repositories: `GET /orgs/:org/repos`
* Teams: full CRUD, members, repos. Organization members can create teams, and the creator becomes a maintainer; `maintainers` and `repo_names` add more at creation. Maintaining a team does not make someone an organization owner
* Team repositories: `PUT /orgs/:org/teams/:team_slug/repos/:owner/:repo` takes a `permission` (`pull`, `triage`, `push`, `maintain`, `admin`; the team's own permission when left out) and, like removal, needs admin on the repository

## Releases

* Releases: full CRUD, latest, by tag. Creating, editing, and deleting releases and assets needs push access
* Release assets: full CRUD, upload
* Generate release notes

## Search

* `GET /search/repositories` — Search repositories: full query syntax (user, org, language, topic, stars, forks, etc.)
* `GET /search/issues` — Search issues and pull requests (repo, is, author, label, milestone, state, etc.)
* `GET /search/users` — Search users and orgs
* `GET /search/code` — Search code: blob content
* `GET /search/commits` — Search commits: by message
* `GET /search/topics` — Search topics
* `GET /search/labels` — Search labels

## Actions

* Workflows: list, get, enable/disable, dispatch
* Workflow runs: list, get, cancel, rerun, delete, logs
* Jobs: list, get, logs
* Artifacts: list (filter by `name`), get, download, delete
* Caches: list, delete by key or ID, repository and organization usage
* Secrets: repo + org CRUD. Organization secrets keep their `visibility` (`all`, `private`, `selected`; required on create); a `selected` secret lists its repositories through `GET/PUT /orgs/:org/actions/secrets/:name/repositories` and `PUT/DELETE .../repositories/:repository_id` (409 for another visibility)

### Jobs and artifacts

The emulator does not run workflows. Code that runs inside a job, such as an action or a script using the Actions toolkit, gets its job from `POST /_baseline/actions/jobs`. The response holds the environment a hosted runner sets for that job:

```ts theme={null}
import { Octokit } from "@octokit/rest";

const octokit = new Octokit({ baseUrl: "http://localhost:4001", auth: "test_token_admin" });
const { data } = await octokit.request("POST /_baseline/actions/jobs", {
  repository: "octocat/hello-world",
  workflow: "ci.yml", // optional: ID, path, file name, or name; registered on first use
  ref: "main", // optional: branch, tag, or full ref; defaults to the default branch
  job: "build", // optional
});
Object.assign(process.env, data.env);

// Load the toolkit after GITHUB_API_URL is set: @actions/github reads it once, on import.
const { DefaultArtifactClient } = await import("@actions/artifact");
const artifacts = new DefaultArtifactClient();
await artifacts.uploadArtifact("report", ["out/report.json"], "out");
```

* **Body.** `repository` (required, `owner/name`), `workflow`, `ref`, `sha`, `event` (default `push`), `job` (default `build`), and `run_id` to add a job to a run that is still going. A queued job with the same name that no one has started, such as the one `workflow_dispatch` creates, is taken over. The caller needs push access.
* **Response.** `run_id`, `job_id`, `run_attempt`, `token_expires_at`, and `env`: `ACTIONS_RUNTIME_TOKEN`, `ACTIONS_RESULTS_URL`, `ACTIONS_CACHE_SERVICE_V2`, `GITHUB_API_URL`, `GITHUB_REPOSITORY`, `GITHUB_RUN_ID`, `GITHUB_JOB`, `GITHUB_REF`, `GITHUB_SHA`, `GITHUB_EVENT_NAME`, and the other `GITHUB_*` variables. A run on `pull_request` uses `refs/pull/<number>/merge`. Leave `GITHUB_SERVER_URL` unset: the toolkit treats any host other than github.com as GitHub Enterprise Server, where the artifact client refuses to run.
* **Runs and events.** The run moves to `in_progress` and the job to `in_progress`, with `workflow_run` and `workflow_job` webhooks. `POST /_baseline/actions/jobs/:job_id/complete` with `{"conclusion": "success"}` (or `failure`, `cancelled`, `skipped`, `neutral`, `timed_out`, `action_required`) completes the job. Once every job in the run has completed, the run completes with the worst conclusion.
* **The runtime token** is a JWT naming the run and job (`Actions.Results:<run>:<job>` in `scp`) and the job's cache scopes (`ac`). It lasts six hours and stops working when the job completes.

Artifacts go through the same protocol as on GitHub. `@actions/artifact` calls the results service (`/twirp/github.actions.results.api.v1.ArtifactService/CreateArtifact`, `FinalizeArtifact`, `ListArtifacts`, `GetSignedArtifactURL`, `DeleteArtifact`) and writes to the signed upload URL it returns with the Azure Storage SDK (Put Blob, or Put Block and Put Block List). Downloads read the signed URL; byte ranges work. The rules:

* Names are unique in a run (a second upload fails with `an artifact with this name already exists on the workflow run`). An upload that was never finalized is replaced.
* Artifacts are kept 90 days, or for `retentionDays` when that is shorter. `digest` is `sha256:` plus the hash of the uploaded bytes; finalizing with a different hash or size fails.
* `skipArchive` uploads keep their content type and file name, and download as the file.
* `GET /repos/:owner/:repo/actions/artifacts/:id/zip` — Download artifact: needs a token even on a public repository, answers 302 with a signed URL valid for one minute, and answers 410 `Artifact has expired` after `expires_at`. Downloading another run's artifact with `findBy` uses this route.
* Deleting an artifact needs push access (`actions: write` for apps).

### Caches

`@actions/cache` (what actions/cache\@v4 runs) works with the same job environment. It calls the results service's CacheService (`CreateCacheEntry`, `FinalizeCacheEntryUpload`, `GetCacheEntryDownloadURL`) and moves the archive through signed blob URLs. Also set `GITHUB_WORKSPACE` and `RUNNER_TEMP`, which a runner sets and the emulator cannot know: the client archives paths relative to the workspace and builds archives in the temp directory.

```ts theme={null}
Object.assign(process.env, data.env, { GITHUB_WORKSPACE: process.cwd(), RUNNER_TEMP: "/tmp/runner" });
const cache = await import("@actions/cache");
await cache.saveCache(["node_modules"], "npm-linux-abc123");
const hit = await cache.restoreCache(["node_modules"], "npm-linux-def456", ["npm-linux-"]);
```

* **Scopes.** Each entry belongs to the ref it was saved from (`refs/heads/main`, `refs/pull/12/merge`). A job reads its own ref first, then a pull request's base branch, then the default branch, and writes only to its own ref. The runtime token carries these scopes in its `ac` claim.
* **Matching.** In each scope, in that order: the exact key, then any key starting with the key, then any key starting with each restore key. The newest match wins. The version (a hash of the paths and compression the client computes) must match exactly.
* **Saving** a key, version, and scope that already exist fails with a 409, which the client reports as `Unable to reserve cache with key ..., another job may be creating this cache.` An upload left unfinalized by a job that has completed is dropped.
* **Limits.** Entries not used for 7 days are removed, and a repository holds 10 GB: saving past that evicts the least recently used entries.
* **REST.** `GET /repos/:owner/:repo/actions/caches` lists saved entries with `key` (prefix), `ref`, `sort` (`created_at`, `last_accessed_at`, `size_in_bytes`), and `direction`. `DELETE .../actions/caches?key=...&ref=...` deletes by exact key and returns what it deleted. `DELETE .../actions/caches/:id` deletes one. `GET .../actions/cache/usage`, `GET /orgs/:org/actions/cache/usage`, and `GET /orgs/:org/actions/cache/usage-by-repository` report usage. Deleting needs push access, and organization usage needs an organization admin.

## Checks

* Check runs: create, update, get, annotations, rerequest, list by ref/suite. Ref based lookups accept branch and tag refs containing slashes.
* Check suites: create, get, preferences, rerequest, list by ref. Ref based lookups accept branch and tag refs containing slashes.
* Automatic suite status rollup from check run results

Check-run lists by ref and suite support `page` and `per_page` (default 30, maximum 100) with pagination `Link` headers. Ref lists apply `check_name`, `status`, and `filter` before pagination; `total_count` reports the full filtered count.

Checks created with installation tokens include their GitHub App in API responses and webhooks. Suites are separated by App and commit, and `filter=latest` keeps the newest run for each App and check name. User-token checks return `app: null`; request-body `app_id` cannot set or change attribution.

## GraphQL

`POST /graphql` serves GitHub's GraphQL API over the same data as the REST routes. Octokit's `graphql` client, the `gh` CLI's queries, and other GraphQL tools work against it:

```ts theme={null}
const octokit = new Octokit({ baseUrl: "http://localhost:4001", auth: "test_token_admin" });
const { repository } = await octokit.graphql(`{
  repository(owner: "octocat", name: "hello-world") {
    defaultBranchRef { name target { oid } }
    pullRequests(first: 10, states: [OPEN]) { nodes { number title mergeStateStatus reviewDecision } }
  }
}`);
```

* **Schema.** Queries are validated against GitHub's published schema (from `@octokit/graphql-schema`, refreshed by a weekly drift check), so an unknown field fails the way it does on GitHub: HTTP 200 with graphql-ruby's message, such as `Field 'nope' doesn't exist on type 'User'` (`extensions.code` is `undefinedField`).
* **Root fields:** `viewer`, `user`, `organization`, `repository`, `repositoryOwner`, `node`, `nodes`, `rateLimit`. No mutations yet.
* **What resolves:** users, bots, organizations (members with roles, teams), repositories (settings, owner, fork parent, topics, languages, license, `viewerPermission`), issues and pull requests (filters, ordering, labels, assignees, milestones, comments, reviews, review requests, `mergeable`, `mergeStateStatus`, `reviewDecision`, commits, files, closing references from "Fixes #N"), refs, commits (history with `path`, authors with co-authors, parents, `file`), trees, blobs (`object(expression: "main:README.md")`), annotated tags, releases, check runs, commit statuses, and `statusCheckRollup`.
* **Not emulated:** a field without a resolver fails with `<Type>.<field> is not emulated by baseline` (`extensions.code` is `NOT_EMULATED`) instead of reading as null. Projects, sub-issues, and reactions are not modeled, so `projectItems`, `subIssues`, and `reactionGroups` read as empty.
* **IDs.** The `node_id` of every REST object is its GraphQL `id`, and `node(id:)` and `nodes(ids:)` resolve them.
* **Pagination** follows GitHub: `nodes` or `edges` need `first` or `last` (at most 100; `MISSING_PAGINATION_BOUNDARIES`, `EXCESSIVE_PAGINATION`), and `totalCount` alone needs neither.
* **Errors** carry GitHub's top-level `type`: `NOT_FOUND` (`Could not resolve to a Repository with the name 'octocat/nope'.`), `FORBIDDEN` (`Resource not accessible by integration` for an app without the permission), and the pagination types above. Private repositories are hidden from people without access.
* **Auth and limits.** A token is required (401 `This endpoint requires you to be authenticated.`). GraphQL requests count in their own rate limit bucket (`X-RateLimit-Resource: graphql`, `rateLimit`, and `resources.graphql` in `GET /rate_limit`), one point per request.

## Meta

* `GET /rate_limit` — Get rate limit status
* `GET /meta` — Get server metadata
* `GET /octocat` — Get Octocat: ASCII art
* `GET /emojis` — List emoji URLs
* `GET /zen` — Get a random zen phrase
* `GET /versions` — List API versions

## Webhooks

* Repo webhooks: full CRUD, ping, test, deliveries
* Org webhooks: full CRUD, ping
* Real HTTP delivery to registered URLs on all state changes
* Contents writes, ref updates, merges, and branch updates send `push` with the full payload (`before`, `after`, `commits`, `head_commit`, `pusher`, `compare`)

## Seed config

```json theme={null}
{
  "github": {
    "users": [
      {
        "login": "octocat",
        "name": "The Octocat",
        "email": "octocat@github.com",
        "bio": "I am the Octocat",
        "company": "GitHub",
        "location": "San Francisco"
      }
    ],
    "orgs": [
      {
        "login": "my-org",
        "name": "My Organization",
        "description": "A test organization",
        "members": [
          {
            "login": "octocat",
            "role": "admin"
          }
        ]
      }
    ],
    "repos": [
      {
        "owner": "octocat",
        "name": "hello-world",
        "description": "My first repository",
        "language": "JavaScript",
        "topics": ["hello", "world"],
        "auto_init": true
      },
      {
        "owner": "my-org",
        "name": "org-repo",
        "description": "An organization repository",
        "language": "TypeScript",
        "private": true,
        "auto_init": true
      }
    ]
  }
}
```

| Key | What it seeds |
| - | - |
| `users` | Users by `login`, with optional `name`, `email`, `bio`, `company`, and `location` |
| `orgs` | Organizations by `login`, with `name`, `description`, and optional `members` (`login`, `role`) |
| `repos` | Repositories by `owner` and `name`, with `description`, `language`, `topics`, `private`, and `auto_init` |
| `oauth_apps` | OAuth Apps for strict client validation (see [OAuth Apps](#oauth-apps)) |
| `apps` | GitHub Apps with permissions, events, webhooks, and installations (see [GitHub Apps](#github-apps)) |

Seeded repositories start with a `README.md` commit; set `auto_init: false` to create one with no commits.

Organization `members` are optional. Each entry references a seeded user by `login`; `role` defaults to `member`, and `admin` creates an organization owner. Unknown users are ignored. Seeded memberships use the synthetic `members` team, so they also appear through the team membership endpoints.

Seed tokens under the top-level `tokens:` key (see [Authentication](/authentication.md#tokens-from-the-config-file)).

## Conformance

| | |
| - | - |
| Routes | 263 supported or partial, 0 planned |
| GraphQL root fields | 8 supported or partial, 0 planned |
| Route gate | fail |
| Spec gate | fail, 93 allowed ([github/rest-api-description](https://github.com/github/rest-api-description) at `755ee33`) |
| SDKs tested | `@actions/artifact@6.3.1`, `@actions/cache@6.3.0`, `@octokit/app@16.1.4`, `@octokit/rest@22.0.1`, `@octokit/webhooks@14.2.0` |
| Dual mode | not yet |

See [CONFORMANCE.md](https://github.com/deploybaseline/cli/blob/main/CONFORMANCE.md) for every provider.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.