Skip to main content
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

Start

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:
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). 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:
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:
  • 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). 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:
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:
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:

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 ([email protected]) 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, 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
  • 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:
  • 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.
  • 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:
  • 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

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.