@octokit/rest, @octokit/app, and @octokit/webhooks.
At a glance
Start
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:
page/per_page with Link headers.
Auth
Tokens from the seed config go inAuthorization: Bearer <token> or Authorization: token <token> (see Authentication). 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’sdefault_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 validatesclient_id, client_secret, and redirect_uri during the OAuth flow:
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:GET /app— Get the authenticated app: JWT requiredGET /app/installations— List app installationsGET /app/installations/:id— Get installationPOST /app/installations/:id/access_tokens— Create installation access tokenGET /repos/:owner/:repo/installation— Get repo installationGET /orgs/:org/installation— Get org installationGET /users/:username/installation— Get user installation
{ 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). 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 programmaticcreateEmulator API can generate a GitHub App key when private_key is omitted:
reset() calls. Explicit keys are never included in generatedSecrets.
The CLI can generate omitted keys when you request a private delivery file:
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:
Installation token inspection
GET /_baseline/installation-tokens— Inspect minted installation tokens: secret-free metadata
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 userPATCH /user— Update profileGET /users/:username— Get userGET /users— List usersGET /users/:username/repos— List user reposGET /users/:username/orgs— List user orgsGET /users/:username/followers— List followersGET /users/:username/following— List following
Repositories
GET /repos/:owner/:repo— Get repoGET /repositories/:id— Get repo by numeric IDPOST /user/repos— Create user repoGET /orgs/:org/repos— List org repos (type: all, public, private, forks, sources, member;sort,direction); private ones only for people who can see themPOST /orgs/:org/repos— Create org repoPATCH /repos/:owner/:repo— Update repoDELETE /repos/:owner/:repo— Delete repo (cascades)GET/PUT /repos/:owner/:repo/topics— Get or replace topicsGET /repos/:owner/:repo/languages— List languagesGET /repos/:owner/:repo/contributors— List contributorsGET /repos/:owner/:repo/forks— List forksPOST /repos/:owner/:repo/forks— Create fork (with the parent’s history;default_branch_onlycopies only the default branch)GET/PUT/DELETE /repos/:owner/:repo/collaborators/:username— Check, add, or remove a collaboratorGET /repos/:owner/:repo/collaborators/:username/permission— Get a collaborator’s permissionPOST /repos/:owner/:repo/transfer— Transfer repo (202 Accepted)GET /repos/:owner/:repo/tags— List tags: the refs underrefs/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 READMEGET /repos/:owner/:repo/contents/:path— Get a file or list a directory: at a ref- Send
Accept: application/vnd.github.raworapplication/vnd.github.raw+jsonto 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 negotiationPUT/DELETE /repos/:owner/:repo/contents/:path— Write or delete a file: create, update, or delete a file and commit the changeGET /repos/:owner/:repo/commits— List commits: with ref, path, author, and date filtersGET /repos/:owner/:repo/commits/:ref— Get a commit: with file diffs and statsGET /repos/:owner/:repo/compare/:base...:head— Compare two refsPOST /repos/:owner/:repo/statuses/:sha— Create a commit status (state,context,target_url,description; sendsstatus)GET /repos/:owner/:repo/commits/:ref/statuses— List statuses: newest firstGET /repos/:owner/:repo/commits/:ref/status— Get combined status (latest per context;failure,pending, orsuccess)
Issues
GET /repos/:owner/:repo/issues— List issues (filter by state, labels, assignee, milestone, creator, since)POST /repos/:owner/:repo/issues— Create issueGET /repos/:owner/:repo/issues/:number— Get issuePATCH /repos/:owner/:repo/issues/:number— Update issue (state transitions, events)PUT/DELETE /repos/:owner/:repo/issues/:number/lock— Lock or unlock issueGET /repos/:owner/:repo/issues/:number/timeline— List timeline eventsGET /repos/:owner/:repo/issues/:number/events— List issue eventsPOST/DELETE /repos/:owner/:repo/issues/:number/assignees— Manage assignees
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 requestGET /repos/:owner/:repo/pulls/:number— Get pull requestPATCH /repos/:owner/:repo/pulls/:number— Update pull requestPUT /repos/:owner/:repo/pulls/:number/merge— Merge pull request: withmerge,squash, orrebase(with branch protection enforcement)GET /repos/:owner/:repo/pulls/:number/commits— List pull request commits: the commits on head that base does not haveGET /repos/:owner/:repo/pulls/:number/files— List changed files: with patches (three-dot diff)POST/DELETE /repos/:owner/:repo/pulls/:number/requested_reviewers— Manage reviewersPUT /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, andchanged_filescount the changes since head left base.mergeableisfalsewhen 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_shais the test merge commit (Merge <head> into <base>), andnullwhen the merge conflicts.rebaseableisfalsewhen replaying the commits one at a time conflicts, or would give a different result than the merge.mergeable_stateisdirty(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), orclean. A merged pull request reportsmergeable: nullandunknown.
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 decidesblocked and behind, and the merge endpoint enforces it:
- Required status checks. Each context passes with a successful check run (
success,neutral, orskipped) or asuccesscommit status of that name on the head commit; the latest of each counts. Withstrict, 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_reviewsdismisses approvals when new commits reach the head (sendingpull_request_reviewdismissed).require_code_owner_reviewsneeds an approval from an owner of each changed file, read fromCODEOWNERSon the base branch (.github/, the root, ordocs/; 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
squashandrebasemerges. - Admins. Repository admins merge without meeting the rule unless
enforce_adminsis on; the pull request still reportsblocked.
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
mergemakes a merge commit with the base and head as parents (default messageMerge pull request #N from owner:branch, then the title).squashmakes one commit on the base, authored by the pull request’s author (default title<title> (#N), then the commits as a list).rebasereplays each commit onto the base, keeping its author. GitHub ([email protected]) is the committer.- Errors follow GitHub: 405
Pull Request is not mergeablefor conflicts or a closed pull request, 405 for a draft, a merge method the repository turned off, an unmet protection rule, or a rebase that cannot be done (This branch can't be rebased), and 409Head branch was modified. Review and try the merge again.whenshais stale. delete_branch_on_mergedeletes 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 within_reply_to(orin_reply_to_id) orPOST /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 reviewsPOST /repos/:owner/:repo/pulls/:number/reviews— Create review (with inline comments)GET/PUT /repos/:owner/:repo/pulls/:number/reviews/:id— Get or update reviewPOST /repos/:owner/:repo/pulls/:number/reviews/:id/events— Submit reviewPUT /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(withcreated,deleted,forced, and the pushedcommitswith their added, removed, and modified files); creating and deleting also sendcreateanddelete, which name the branch or tag without therefs/heads/orrefs/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;
maintainersandrepo_namesadd more at creation. Maintaining a team does not make someone an organization owner - Team repositories:
PUT /orgs/:org/teams/:team_slug/repos/:owner/:repotakes apermission(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 orgsGET /search/code— Search code: blob contentGET /search/commits— Search commits: by messageGET /search/topics— Search topicsGET /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); aselectedsecret lists its repositories throughGET/PUT /orgs/:org/actions/secrets/:name/repositoriesandPUT/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 fromPOST /_baseline/actions/jobs. The response holds the environment a hosted runner sets for that job:
- Body.
repository(required,owner/name),workflow,ref,sha,event(defaultpush),job(defaultbuild), andrun_idto 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 oneworkflow_dispatchcreates, is taken over. The caller needs push access. - Response.
run_id,job_id,run_attempt,token_expires_at, andenv: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 otherGITHUB_*variables. A run onpull_requestusesrefs/pull/<number>/merge. LeaveGITHUB_SERVER_URLunset: 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_progressand the job toin_progress, withworkflow_runandworkflow_jobwebhooks.POST /_baseline/actions/jobs/:job_id/completewith{"conclusion": "success"}(orfailure,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>inscp) and the job’s cache scopes (ac). It lasts six hours and stops working when the job completes.
@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
retentionDayswhen that is shorter.digestissha256:plus the hash of the uploaded bytes; finalizing with a different hash or size fails. skipArchiveuploads 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 410Artifact has expiredafterexpires_at. Downloading another run’s artifact withfindByuses this route.- Deleting an artifact needs push access (
actions: writefor 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.
- 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 itsacclaim. - 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/cacheslists saved entries withkey(prefix),ref,sort(created_at,last_accessed_at,size_in_bytes), anddirection.DELETE .../actions/caches?key=...&ref=...deletes by exact key and returns what it deleted.DELETE .../actions/caches/:iddeletes one.GET .../actions/cache/usage,GET /orgs/:org/actions/cache/usage, andGET /orgs/:org/actions/cache/usage-by-repositoryreport 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
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:
- 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 asField 'nope' doesn't exist on type 'User'(extensions.codeisundefinedField). - 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 withpath, authors with co-authors, parents,file), trees, blobs (object(expression: "main:README.md")), annotated tags, releases, check runs, commit statuses, andstatusCheckRollup. - Not emulated: a field without a resolver fails with
<Type>.<field> is not emulated by baseline(extensions.codeisNOT_EMULATED) instead of reading as null. Projects, sub-issues, and reactions are not modeled, soprojectItems,subIssues, andreactionGroupsread as empty. - IDs. The
node_idof every REST object is its GraphQLid, andnode(id:)andnodes(ids:)resolve them. - Pagination follows GitHub:
nodesoredgesneedfirstorlast(at most 100;MISSING_PAGINATION_BOUNDARIES,EXCESSIVE_PAGINATION), andtotalCountalone 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 integrationfor 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, andresources.graphqlinGET /rate_limit), one point per request.
Meta
GET /rate_limit— Get rate limit statusGET /meta— Get server metadataGET /octocat— Get Octocat: ASCII artGET /emojis— List emoji URLsGET /zen— Get a random zen phraseGET /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
pushwith the full payload (before,after,commits,head_commit,pusher,compare)
Seed config
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).
Conformance
See CONFORMANCE.md for every provider.