mcpField
G
github

github

github.com/vercel-labs测试与质量2.0k211 次安装1 天前更新
代码仓库

关于 github

github 是面向应用测试的有状态 GitHub REST API 模拟器,创建、更新和删除操作会保存在内存中并影响相关实体。它覆盖仓库、议题、拉取请求、Actions 等 API,可用于测试依赖 GitHub 的应用与集成。

主要功能

  1. 01模拟 GitHub REST API
  2. 02内存状态及实体关联更新
  3. 03支持仓库、议题和拉取请求
  4. 04涵盖 Actions、Checks 与 Releases
  5. 05支持 OAuth、应用令牌和 Webhook

适用场景

  1. 01测试 GitHub API 客户端
  2. 02验证议题与拉取请求工作流
  3. 03测试 OAuth 登录集成
  4. 04检查 GitHub App 和 Webhook 行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vercel-labs/emulate --skill github

安装后在 Claude Code 中输入/github确认已加载。

详细介绍

GitHub API 模拟器

完整的有状态 GitHub REST API 模拟。创建、更新和删除操作会在内存中持久保存,并影响相关实体。

启动

本地监听器默认绑定到 127.0.0.1。若要从容器或其他机器访问,请在 CLI 中传入 --host 0.0.0.0,或向 createEmulator 传入 hostname: '0.0.0.0'。配置 --base-url 或 baseUrl,以设置这些客户端可访问的对外公布 URL。

# GitHub only
npx emulate --service github

# Default port
# http://localhost:4001

也可以通过编程方式启动:

import { createEmulator } from 'emulate'

const github = await createEmulator({ service: 'github', port: 4001 })
// github.url === 'http://localhost:4001'

对于通过编程方式配置的 GitHub App,省略 private_key,并从实例读取生成的 RSA 密钥:

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

调用 github.reset() 后,密钥仍保持不变。显式指定的密钥不会包含在 generatedSecrets 中。

Next.js 和 Nuxt 适配器也会生成省略的密钥。保留返回的处理程序,并调用其仅限服务器端使用的 generatedSecrets() 方法。配置持久化后,即使冷启动,身份信息也会保留。请妥善保管快照,因为其中包含签名密钥;并要求自定义持久化后端实现原子性的 initialize() 语义。

对于 CLI,仅在请求私有交付文件时省略 private_key:

npx emulate start --service github --seed emulate.config.yaml \
  --generated-secrets-file .emulate-secrets.json

目标位置必须不存在。emulate 会移除继承的 ACL,验证有效权限仅限所有者访问,并在任何监听器或 portless 别名启动前发布完整的 JSON。已处理的启动失败会移除由本次调用创建的文件。若进程被强制终止,可能会留下完整文件;确认没有任何调用正在使用后,必须手动移除该文件。从文件中读取 generatedSecrets,然后将该文件排除在源代码管理之外。Linux 需要 acl 包中的 setfacl 和 getfacl。如果无法验证访问控制,此标志将安全关闭;Windows 不支持此标志。不使用 --generated-secrets-file 时,CLI 种子文件仍必须包含 private_key。

身份验证

以 Authorization: Bearer <token> 或 Authorization: token <token> 的形式传入令牌。

curl http://localhost:4001/user \
  -H "Authorization: Bearer test_token_admin"

公开仓库的端点无需身份验证即可使用。私有仓库和写入操作需要有效令牌。未提供令牌时,请求会回退到第一个种子用户。

安装访问令牌在写入仓库时会以已配置的 GitHub App 机器人身份执行。系统仍会强制检查仓库所有权、所选仓库的访问权限以及请求的 App 权限。合并拉取请求需要基础仓库上的 contents: write 权限。更新拉取请求分支需要拉取请求所属仓库上的 pull_requests: write 权限,以及头部分支所属仓库上的 contents: write 权限。

GitHub App JWT

使用 CLI 且未生成密钥时,请在种子配置中为应用配置显式且有效的私钥。使用 { iss: "<app_id>" } 通过 RS256 签署 JWT。模拟器会验证签名并解析应用。

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: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]

此示例有意省略 private_key,适用于通过编程方式和适配器使用;在这些场景中,模拟器会生成 RSA 密钥,并通过 generatedSecrets 提供该密钥。使用 CLI 时,请通过 --generated-secrets-file <path> 请求私有交付文件,或提供自己的有效密钥。不使用该标志时,CLI 种子文件必须包含 private_key;请勿使用占位 PEM。

将应用指向模拟器

环境变量
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 映射
真实 GitHub URL模拟器 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`,
  },
})

种子配置

tokens:
  test_token_admin:
    login: admin
    scopes: [repo, user, admin:org, admin:repo_hook]

github:
  users:
    - login: octocat
      name: The Octocat
      email: [email protected]
      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: [email protected]
      members:
        - login: octocat
          role: admin
  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

组织的 members 为可选项。每个条目通过 login 引用一个已设种子的用户;role 默认为 member,而 admin 会映射为组织管理员角色。未知用户会被忽略。已设种子的成员关系会使用合成的 members 团队,并授予对组织私有仓库的访问权限。

除非设置 auto_init: false,否则仓库会自动初始化,包含一次提交、一个分支和一个 README。

分页

所有列表端点都支持通过 page 和 per_page 查询参数进行分页,并返回 Link 标头:

curl "http://localhost:4001/repos/octocat/hello-world/issues?page=1&per_page=10" \
  -H "Authorization: Bearer $TOKEN"

API 端点

用户
# 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"
仓库
# 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
内容与提交历史
# 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"

# Request raw bytes from a file Contents or README response with GitHub's raw media type
curl "http://localhost:4001/repos/octocat/hello-world/contents/README.md?ref=main" \
  -H "Accept: application/vnd.github.raw+json"

# 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
问题
# 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
拉取请求
# 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
评论
# 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
审查
# 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"}'
标签与里程碑

提供标签和里程碑的完整 CRUD 操作。为问题添加或移除标签、替换所有标签。列出里程碑的标签。

分支与 Git 数据
# 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
组织与团队
# 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 投递:发生事件时,模拟器会向配置的 webhook_url 发送包含 event_callback 负载的 POST 请求,并附带 X-GitHub-Event 和 X-Hub-Signature-256 标头。

发布版本
# 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
Webhook
# 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 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. Ref based lookups accept branch and tag refs containing slashes.
# Check runs: list for suite, annotations. Ref based lookups accept branch and tag refs containing slashes.
# Automatic suite status rollup from check run results

按 ref 和 suite 列出的 check run 支持 page 和 per_page(默认值为 30,最大值为 100),并通过 Link 标头进行分页。按 ref 列表会在分页前应用 check_name、status 和 filter;total_count 会报告筛选后的完整数量。

使用 installation token 创建的 checks 会在 API 响应和 Webhook 中包含其 GitHub App。Suites 按 App 和 commit 区分,filter=latest 会为每个 App 和 check name 保留最新的 run。用户 token 对应的 checks 会返回 app: null;请求正文中的 app_id 无法设置或更改归属信息。

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"
其他
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

常见模式

创建仓库、问题和 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 安装令牌流程
# 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_..."

# Inspect minted installation-token metadata without token values
curl $BASE/_emulate/installation-tokens

检查路由仅适用于模拟器。它会列出 App、安装、账户、权限、仓库访问权限、签发、到期时间和生命周期状态,但不会接受或返回令牌值或由令牌派生的标识符。到期时间仅供参考,不会改变授权行为。

OAuth 流程
  1. 将用户重定向到 $GITHUB_EMULATOR_URL/login/oauth/authorize?client_id=...&redirect_uri=...&scope=user+repo&state=...
  2. 用户在模拟器界面中选择一个预置用户
  3. 模拟器重定向回去,并附带 ?code=...&state=...
  4. 通过 POST /login/oauth/access_token 用 code 换取令牌
  5. 使用令牌调用 API 端点

与内置模拟器并用自定义模拟器

使用 npx emulate init --custom inventory 搭建第三方 API 模拟器并进行测试。在 emulate.config.ts 中使用 emulate 提供的 defineConfig 注册它,并与 { emulator: "github" } 等内置条目并列。运行 npx emulate start --watch 可重新加载导入项,并在打印出的 /_emulate URL 查看自定义状态。成功重新加载后,本次运行会重置为种子数据。现有的扁平种子配置仍然有效;--config 可指定一个显式文件。有关编写和测试第三方 API 模拟器的信息,请参阅 https://emulate.dev/docs/custom-emulators。

评论

支持 Markdown