github
GitHub提供本地化的 GitHub REST API 模拟服务,支持状态管理、OAuth 流程及 App JWT 认证。用于在开发测试阶段无需连接真实 GitHub 即可进行集成测试和 Webhook 调试。
Trigger Scenarios
Install
npx skills add vercel-labs/emulate --skill github -g -y
SKILL.md
Frontmatter
{
"name": "github",
"description": "Emulated GitHub REST API for local development and testing. Use when the user needs to interact with GitHub API endpoints locally, test GitHub integrations, emulate repos\/issues\/PRs, set up GitHub OAuth flows, configure GitHub Apps, test webhooks, or work with actions\/checks without hitting the real GitHub API. Triggers include \"GitHub API\", \"emulate GitHub\", \"mock GitHub\", \"test GitHub OAuth\", \"GitHub App JWT\", \"local GitHub\", or any task requiring a local GitHub API.",
"allowed-tools": "Bash(npx emulate:*), Bash(emulate:*), Bash(curl:*)"
}
GitHub API Emulator
Fully stateful GitHub REST API emulation. Creates, updates, and deletes persist in memory and affect related entities.
Start
# GitHub only
npx emulate --service github
# Default port
# http://localhost:4001
Or programmatically:
import { createEmulator } from 'emulate'
const github = await createEmulator({ service: 'github', port: 4001 })
// github.url === 'http://localhost:4001'
For a programmatic GitHub App, omit private_key and read the generated RSA key from the instance:
const github = await createEmulator({
service: 'github',
port: 4001,
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 key remains stable across github.reset(). Explicit keys are not included in generatedSecrets.
For the CLI, omit private_key only when requesting a private delivery file:
npx emulate start --service github --seed emulate.config.yaml \
--generated-secrets-file .emulate-secrets.json
The destination must not exist. emulate removes inherited ACLs, verifies effective owner-only access, and publishes complete JSON before any listener or portless alias starts. 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. Read generatedSecrets from the artifact, then keep the file out of source control. 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.
Auth
Pass tokens as Authorization: Bearer <token> or Authorization: token <token>.
curl http://localhost:4001/user \
-H "Authorization: Bearer test_token_admin"
Public repo endpoints work without auth. Private repos and write operations require a valid token. When no token is provided, requests fall back to the first seeded user.
GitHub App JWT
Configure apps in the seed config with a private key. Sign a JWT with { iss: "<app_id>" } using RS256. The emulator verifies the signature and resolves the app.
github:
apps:
- app_id: 12345
slug: my-github-app
name: My GitHub App
private_key: |
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
permissions:
contents: read
issues: write
events: [push, pull_request]
webhook_url: http://localhost:8080/github/webhook
webhook_secret: my-webhook-secret
description: My CI/CD bot
installations:
- installation_id: 100
account: my-org
repository_selection: all
permissions:
contents: read
events: [push]
repositories: [my-org/org-repo]
Pointing Your App at the Emulator
Environment Variable
GITHUB_EMULATOR_URL=http://localhost:4001
Octokit
import { Octokit } from '@octokit/rest'
const octokit = new Octokit({
baseUrl: process.env.GITHUB_EMULATOR_URL ?? 'https://api.github.com',
auth: 'test_token_admin',
})
OAuth URL Mapping
| Real GitHub URL | Emulator URL |
|---|---|
https://github.com/login/oauth/authorize |
$GITHUB_EMULATOR_URL/login/oauth/authorize |
https://github.com/login/oauth/access_token |
$GITHUB_EMULATOR_URL/login/oauth/access_token |
https://api.github.com/user |
$GITHUB_EMULATOR_URL/user |
Auth.js / NextAuth.js
import GitHub from '@auth/core/providers/github'
GitHub({
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
authorization: {
url: `${process.env.GITHUB_EMULATOR_URL}/login/oauth/authorize`,
},
token: {
url: `${process.env.GITHUB_EMULATOR_URL}/login/oauth/access_token`,
},
userinfo: {
url: `${process.env.GITHUB_EMULATOR_URL}/user`,
},
})
Seed Config
tokens:
test_token_admin:
login: admin
scopes: [repo, user, admin:org, admin:repo_hook]
github:
users:
- login: octocat
name: The Octocat
email: octocat@github.com
bio: I am the Octocat
company: GitHub
location: San Francisco
blog: https://github.blog
twitter_username: github
site_admin: false
orgs:
- login: my-org
name: My Organization
description: A test organization
email: org@example.com
repos:
- owner: octocat
name: hello-world
description: My first repository
language: JavaScript
topics: [hello, world]
default_branch: main
private: false
- owner: my-org
name: org-repo
description: An organization repository
language: TypeScript
oauth_apps:
- client_id: Iv1.abc123
client_secret: secret_abc123
name: My Web App
redirect_uris:
- http://localhost:3000/api/auth/callback/github
Repos are auto-initialized with a commit, branch, and README unless auto_init: false is set.
Pagination
All list endpoints support page and per_page query params with Link headers:
curl "http://localhost:4001/repos/octocat/hello-world/issues?page=1&per_page=10" \
-H "Authorization: Bearer $TOKEN"
API Endpoints
Users
# Authenticated user
curl http://localhost:4001/user -H "Authorization: Bearer $TOKEN"
# Update profile
curl -X PATCH http://localhost:4001/user \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bio": "Hello!"}'
# Get user by username
curl http://localhost:4001/users/octocat
# List users
curl http://localhost:4001/users
# User repos / orgs / followers / following
curl http://localhost:4001/users/octocat/repos
curl http://localhost:4001/users/octocat/orgs
curl http://localhost:4001/users/octocat/followers
curl http://localhost:4001/users/octocat/following
# User hovercard
curl http://localhost:4001/users/octocat/hovercard
# User emails
curl http://localhost:4001/user/emails -H "Authorization: Bearer $TOKEN"
Repositories
# Get repo
curl http://localhost:4001/repos/octocat/hello-world
# Get repo by numeric ID
curl http://localhost:4001/repositories/1
# Create user repo
curl -X POST http://localhost:4001/user/repos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "new-repo", "private": false}'
# Create org repo
curl -X POST http://localhost:4001/orgs/my-org/repos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "org-project"}'
# Update repo
curl -X PATCH http://localhost:4001/repos/octocat/hello-world \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "Updated description"}'
# Delete repo (cascades issues, PRs, etc.)
curl -X DELETE http://localhost:4001/repos/octocat/hello-world \
-H "Authorization: Bearer $TOKEN"
# Topics, languages, contributors, forks, collaborators, tags, transfer
Contents & Commit History
# Read a file or list a directory at a branch, tag, or commit
curl "http://localhost:4001/repos/octocat/hello-world/contents/README.md?ref=main"
# Download raw file content from the URL advertised by contents and commit responses
curl http://localhost:4001/octocat/hello-world/raw/main/README.md
# Create or update a file and commit the change
curl -X PUT http://localhost:4001/repos/octocat/hello-world/contents/notes.txt \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"message": "Update notes", "content": "aGVsbG8K"}'
# List commits, get a commit with file stats, or compare refs
curl http://localhost:4001/repos/octocat/hello-world/commits
curl http://localhost:4001/repos/octocat/hello-world/commits/main
curl http://localhost:4001/repos/octocat/hello-world/compare/v1.0.0...main
Issues
# List issues (filter by state, labels, assignee, milestone, creator, since)
curl "http://localhost:4001/repos/octocat/hello-world/issues?state=open&labels=bug"
# Create issue
curl -X POST http://localhost:4001/repos/octocat/hello-world/issues \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Bug report", "body": "Details here", "labels": ["bug"]}'
# Get / update / lock / unlock / timeline / events / assignees
Pull Requests
# List PRs
curl "http://localhost:4001/repos/octocat/hello-world/pulls?state=open"
# Create PR
curl -X POST http://localhost:4001/repos/octocat/hello-world/pulls \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "Feature", "head": "feature-branch", "base": "main"}'
# Merge PR (enforces branch protection)
curl -X PUT http://localhost:4001/repos/octocat/hello-world/pulls/1/merge \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"merge_method": "squash"}'
# Commits, files, requested reviewers, update branch
Comments
# Issue comments: full CRUD
curl http://localhost:4001/repos/octocat/hello-world/issues/1/comments
curl -X POST http://localhost:4001/repos/octocat/hello-world/issues/1/comments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "Looks good!"}'
# Comment by ID (cross-resource)
curl http://localhost:4001/repos/octocat/hello-world/issues/comments/1
# PR review comments
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/comments
# Commit comments
curl http://localhost:4001/repos/octocat/hello-world/commits/abc123/comments
# Repo-wide comment listings
curl http://localhost:4001/repos/octocat/hello-world/issues/comments
curl http://localhost:4001/repos/octocat/hello-world/pulls/comments
curl http://localhost:4001/repos/octocat/hello-world/comments
Reviews
# List / create / get / update / submit / dismiss reviews
curl http://localhost:4001/repos/octocat/hello-world/pulls/1/reviews
curl -X POST http://localhost:4001/repos/octocat/hello-world/pulls/1/reviews \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"event": "APPROVE", "body": "LGTM"}'
Labels & Milestones
Full CRUD for labels and milestones. Add/remove labels from issues, replace all labels. List labels for a milestone.
Branches & Git Data
# List branches
curl http://localhost:4001/repos/octocat/hello-world/branches
# Branch protection CRUD (status checks, PR reviews, enforce admins)
curl -X PUT http://localhost:4001/repos/octocat/hello-world/branches/main/protection \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"required_status_checks": {"strict": true, "contexts": ["ci"]}}'
# Refs, commits, trees (recursive), blobs, tags, matching-refs
Organizations & Teams
# List all orgs / user's orgs / get org / update org
curl http://localhost:4001/organizations
curl http://localhost:4001/user/orgs -H "Authorization: Bearer $TOKEN"
curl http://localhost:4001/orgs/my-org
curl -X PATCH http://localhost:4001/orgs/my-org \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "Updated org"}'
# Org members: list, get, remove
curl http://localhost:4001/orgs/my-org/members
curl http://localhost:4001/orgs/my-org/members/octocat
curl -X DELETE http://localhost:4001/orgs/my-org/members/octocat \
-H "Authorization: Bearer $TOKEN"
# Org memberships: get, set (invite/update role)
curl http://localhost:4001/orgs/my-org/memberships/octocat -H "Authorization: Bearer $TOKEN"
curl -X PUT http://localhost:4001/orgs/my-org/memberships/octocat \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "admin"}'
# Teams: CRUD
curl http://localhost:4001/orgs/my-org/teams
curl -X POST http://localhost:4001/orgs/my-org/teams \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "engineering", "privacy": "closed"}'
# Team members and memberships
curl http://localhost:4001/orgs/my-org/teams/engineering/members
curl -X PUT http://localhost:4001/orgs/my-org/teams/engineering/memberships/octocat \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "maintainer"}'
# Team repos: list, add, remove
curl http://localhost:4001/orgs/my-org/teams/engineering/repos
curl -X PUT http://localhost:4001/orgs/my-org/teams/engineering/repos/my-org/org-repo \
-H "Authorization: Bearer $TOKEN"
# Legacy team endpoints by ID
curl http://localhost:4001/teams/1
curl http://localhost:4001/teams/1/members
GitHub Apps
# Get authenticated app (requires JWT auth)
curl http://localhost:4001/app \
-H "Authorization: Bearer <jwt>"
# List app installations
curl http://localhost:4001/app/installations \
-H "Authorization: Bearer <jwt>"
# Get installation
curl http://localhost:4001/app/installations/100 \
-H "Authorization: Bearer <jwt>"
# Create installation access token (mints ghs_... token)
curl -X POST http://localhost:4001/app/installations/100/access_tokens \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"permissions": {"contents": "read"}}'
# Find installation for repo / org / user
curl http://localhost:4001/repos/my-org/org-repo/installation
curl http://localhost:4001/orgs/my-org/installation
curl http://localhost:4001/users/octocat/installation
App webhook delivery: when events occur, the emulator POSTs event_callback payloads to configured webhook_url with X-GitHub-Event and X-Hub-Signature-256 headers.
Releases
# Create release
curl -X POST http://localhost:4001/repos/octocat/hello-world/releases \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"tag_name": "v1.0.0", "name": "v1.0.0"}'
# List, get, latest, by tag, generate notes
# Release assets: list, upload
curl http://localhost:4001/repos/octocat/hello-world/releases/1/assets
curl -X POST http://localhost:4001/repos/octocat/hello-world/releases/1/assets \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/octet-stream" \
-H "name: binary.zip" \
--data-binary @binary.zip
Webhooks
# Create webhook (real HTTP delivery on state changes)
curl -X POST http://localhost:4001/repos/octocat/hello-world/hooks \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"config": {"url": "http://localhost:8080/webhook"}, "events": ["push", "pull_request"]}'
# Full CRUD, ping, test, deliveries
# Org webhooks also supported
Search
# Search repositories
curl "http://localhost:4001/search/repositories?q=language:JavaScript+user:octocat"
# Search issues and PRs
curl "http://localhost:4001/search/issues?q=repo:octocat/hello-world+is:open"
# Search users, code, commits, topics, labels
Actions
# Workflows: list, get, enable/disable, dispatch
# Workflow runs: list, get, cancel, rerun, delete, logs
# Jobs: list, get, logs
# Artifacts: list, get, delete
# Secrets: repo + org CRUD
Checks
# Create check run
curl -X POST http://localhost:4001/repos/octocat/hello-world/check-runs \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "CI", "head_sha": "abc123", "status": "completed", "conclusion": "success"}'
# Check suites: create, get, rerequest, preferences, list by ref
# Check runs: list for suite, annotations
# Automatic suite status rollup from check run results
OAuth
# Authorize (browser flow, shows user picker)
# GET /login/oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...
# Token exchange
curl -X POST http://localhost:4001/login/oauth/access_token \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"client_id": "Iv1.abc123", "client_secret": "secret_abc123", "code": "<code>"}'
# User emails
curl http://localhost:4001/user/emails -H "Authorization: Bearer $TOKEN"
# OAuth app management (settings)
curl http://localhost:4001/settings/applications -H "Authorization: Bearer $TOKEN"
curl http://localhost:4001/settings/connections/applications/Iv1.abc123 -H "Authorization: Bearer $TOKEN"
# Revoke OAuth app
curl -X POST http://localhost:4001/settings/connections/applications/Iv1.abc123/revoke \
-H "Authorization: Bearer $TOKEN"
Misc
curl http://localhost:4001/rate_limit
curl http://localhost:4001/meta
curl http://localhost:4001/emojis
curl http://localhost:4001/versions
curl http://localhost:4001/octocat
curl http://localhost:4001/zen
Common Patterns
Create Repo, Issue, and PR
TOKEN="test_token_admin"
BASE="http://localhost:4001"
# Create repo
curl -X POST $BASE/user/repos \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "my-project"}'
# Create issue
curl -X POST $BASE/repos/admin/my-project/issues \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "First issue"}'
# Create PR
curl -X POST $BASE/repos/admin/my-project/pulls \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"title": "First PR", "head": "feature", "base": "main"}'
GitHub App Installation Token Flow
# 1. Sign a JWT with { iss: "12345" } using the app's private key (RS256)
# 2. Create an installation access token
curl -X POST $BASE/app/installations/100/access_tokens \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"permissions": {"contents": "read", "issues": "write"}}'
# Returns { "token": "ghs_...", ... }
# 3. Use the installation token to call API endpoints
curl $BASE/repos/my-org/org-repo \
-H "Authorization: Bearer ghs_..."
OAuth Flow
- Redirect user to
$GITHUB_EMULATOR_URL/login/oauth/authorize?client_id=...&redirect_uri=...&scope=user+repo&state=... - User picks a seeded user on the emulator's UI
- Emulator redirects back with
?code=...&state=... - Exchange code for token via
POST /login/oauth/access_token - Use token to call API endpoints
Version History
-
d0219d0
Current 2026-08-20 03:00
新增 CLI 安全交付生成密钥功能;为 createEmulator 程序化接口增加应用密钥生成功能;增强内容写入授权验证。
- 1e4b71a 2026-07-25 08:56


