2026 年 GitHub Enterprise API 实操:PAT vs GitHub Apps、15K 速率上限、audit log、EMU 和 GHES 版本错位
GitHub Enterprise Cloud 和 Server 共用品牌却是两套 API 表面。鉴权方式决定你能拿到 5,000 还是 15,000 次/小时;audit log 只在每席 $21 的档位开放;EMU 账号一半文档里写得能干的事其实不能干;GHES 版本错位会让昨天能跑的代码今天 410。这篇把这些事的实际映射讲清楚。
上面是文章摘要,下面进入正文深读。可以配合目录逐段阅读,不会丢掉上下文。
上个月会议上一个 300 工程师公司的 platform engineer 坐我旁边问我,他们新搭的 GitHub Apps 集成为什么每周一早上被限流。他们从一堆 PAT 迁过来(理由是「Apps 速率上限高,文档写 15,000 一小时」),现在每周一次的依赖扫描扫所有仓库时吃 429。烧掉的量大概 9,000 calls/hour,他们以为的上限是 15,000。文档他们没读错,只是没看到限定条件那句。
GitHub 的 Enterprise API 表面属于那种「文档技术上都写了、但顺序完全不是集成工程师真正需要的顺序」的产品。头条数字(PAT 5,000、Apps 15,000)是真的,但限定条件(按用户和仓库扩展、次要限制单独触发、GHEC 和 GHES 行为不同)才是把生产搞挂的部分。这篇走一遍 GitHub Enterprise API 文档技术上覆盖了但埋得很深的那一层:什么鉴权路径真正抗规模、速率上限的悬崖在哪、audit log 给你什么值这每席 $21、EMU 静默打断了什么、怎么写代码挺过 GHES 版本错位。
五种鉴权方式,按企业规模下的耐用度排
GitHub 几年下来攒了五条鉴权路径,企业规模下并不等价:
| 方式 | 速率上限 | 作用域 | 生命周期 | 用在什么场景 |
|---|---|---|---|---|
| 不鉴权 | 60/hour/IP | 仅公开读 | 无 | 生产环境永远不要 |
| Personal access token (PAT) | 5,000/hour | 用户级 scope | 用户离职就失效 | 一次性脚本 |
| OAuth App | 5,000/hour/app | OAuth 流的用户 scope | App 级 | 面向用户的第三方工具 |
| GitHub App (installation token) | 5,000~15,000/hour | installation 级细粒度 | App 级,不随用户 | 企业集成 |
| Actions 内的 GITHUB_TOKEN | 1,000~15,000/hour/repo | workflow scope | 每次 run | Actions 内的 CI/CD |
表里反直觉的是 GitHub Apps 这行。5,000 到 15,000 不是单一数字,是两件事的函数。GHES 装的 App 基线 5,000/hour。GHEC 装的 App 基线 15,000/hour。在基线之上,两边都按超过 20 个之后的仓库和用户线性扩展:每多一个仓库 +50/hour,每多一个用户 +50/hour,GHES 封顶 12,500,GHEC 封顶 15,000。GHEC org 上装的 App,400 仓 200 用户,吃到封顶。GHES 实例上 30 仓 25 用户,得到 5,000 +(10 × 50)+(5 × 50)= 5,750。
那个会议上的 platform engineer 公司 300 工程师几百仓但跑的是 GHES,所以实际上限是 GHES 12,500 顶减去那个早上次要 CPU-time 限制吃掉的部分。他们记住的 15,000 是 GHEC 上限,他们没有。
文档没放在一页上的速率上限算术
主要速率上限在官方速率限制页,次要限制零散各处。第一天就该知道的完整清单:
| 限制 | 触发条件 | 看到什么 |
|---|---|---|
| 主要,按身份 | 5K/15K calls/hour | 403 with x-ratelimit-remaining: 0 |
| 并发请求 | 在飞超过 100 | 429 或 403 |
| 端点点数 | 单端点超过 900 points/min | 403 with Retry-After |
| CPU 时间 | 60 秒墙时窗口超过 90 秒 CPU | 403 |
| 内容创建写入 | 80/min 或 500/hour | 403 |
| OAuth token 申请 | 2,000/hour | 403 |
| Search API | 不鉴权 10/min、鉴权 30/min | 403 |
最常打人措手不及的两个是端点点数限制和内容创建限制。点数限制把每个 REST 端点当作独立的每分钟桶,所以对单端点(比如 /repos/{owner}/{repo}/issues/comments)每分钟打几千次读调用会触发点数限制,哪怕全局小时计数器还宽裕。内容创建限制打的是自动开 issue、发 status check、批量开 PR 的 bot;从外部追踪器同步状态的话,500/hour 很紧。
X-RateLimit-* 响应头报告所有这些。主要的四个是 X-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Used、X-RateLimit-Reset(Unix 时间戳)。要埋的隐藏 header 是 X-RateLimit-Resource,告诉你该次调用被哪个桶记账(core、search、code_search、graphql、integration_manifest、code_scanning_upload、actions_runner_registration、dependency_snapshots)。看到限流时第一个问题是「哪个桶」,答案就在这个 header。
audit log API 就是每席 $21 的全部理由
操作上最有价值的 Enterprise-only 端点就一个:audit log。GHEC 上路径是 /enterprises/{ENTERPRISE}/audit-log,接 phrase 查询参数做过滤,返回分页事件列表。值得喂进 SIEM 的事件类型:
org.sso_response_succeeded和org.sso_response_failed(SSO 登录尝试)repo.add_member/repo.remove_member/repo.change_member_permissionorg.update_member_repository_creation_permissionpersonal_access_token.access_granted和personal_access_token.access_revokedoauth_authorization.create和oauth_authorization.destroyprotected_branch.update和protected_branch.destroyrepo.transfer和repo.destroy
每个事件带 ISO 8601 时间戳、actor login、actor IP 地址、仓库或 org 上下文、事件特定的 metadata。数据密度足以让一个全新的 GHEC 租户每周每活跃工程师产出几百条事件。存储要按这个量规划:500 席 enterprise 每月通常产 5 万~20 万条审计事件,对象存储这个量很小,但如果想塞进关系数据库做查询不分区的话就不轻。
不显眼的坑是 API 的实时性延迟。多数事件几分钟内出现在 audit log API,但高频桶(Actions workflow run、依赖图更新)可能延迟 30~60 分钟。预期亚分钟级到达的 SIEM 规则要按你租户实际的延迟分布调一下。
定价上:首年每席每月 $21,之后按常规价。500 席 enterprise 第一年付 $126,000。audit log 端点是合规和安全团队最常拿来支撑这条预算项的功能。
代码层怎么处理速率限制
生产环境的 GitHub 集成最终都得写两块可复用胶水:一个监控 X-RateLimit-* 和 Retry-After 的 header parser,一个在剩余配额低时节流出站调用的队列。最小可用 Python 形态用 requests:
import time
import requests
class GitHubRateLimiter:
def __init__(self):
self.remaining = {}
self.reset = {}
def update(self, response: requests.Response) -> None:
resource = response.headers.get("X-RateLimit-Resource", "core")
remaining = int(response.headers.get("X-RateLimit-Remaining", "5000"))
reset = int(response.headers.get("X-RateLimit-Reset", "0"))
self.remaining[resource] = remaining
self.reset[resource] = reset
def should_wait(self, resource: str = "core", floor: int = 100) -> int:
if self.remaining.get(resource, 5000) > floor:
return 0
return max(0, self.reset.get(resource, 0) - int(time.time()))
def call_api(url: str, token: str, limiter: GitHubRateLimiter) -> dict:
resource = "search" if "/search/" in url else "core"
wait = limiter.should_wait(resource)
if wait > 0:
time.sleep(min(wait, 300))
headers = {
"Authorization": f"Bearer {token}",
"X-GitHub-Api-Version": "2026-03-10",
"Accept": "application/vnd.github+json",
}
r = requests.get(url, headers=headers, timeout=30)
limiter.update(r)
if r.status_code == 403 and "rate limit" in r.text.lower():
retry_after = int(r.headers.get("Retry-After", "60"))
time.sleep(retry_after)
return call_api(url, token, limiter)
r.raise_for_status()
return r.json()
这段代码里编码的三件事:
- 按 resource 跟踪而不是全局。 search 重的工作负载和 core REST 工作负载独立限流,单一全局计数器看不出来谁是瓶颈。
Retry-After是秒。 和 Meta 的 BUC header 不同,GitHub 的Retry-After按 HTTP 标准走。这里的坑反过来:被 Meta 分钟编码烫过的开发者有时会过度纠正。- 403 + body 含 "rate limit" 是次要限制信号。 主要限制返回 403 +
X-RateLimit-Remaining: 0。次要限制返回 403 + 另一种 body。两种都可重试,但次要限制往往需要比Retry-After建议的更长退避,content-creating 桶尤其如此。
EMU 边界,销售工程师跳过的部分
Enterprise Managed Users(EMU)是 GHEC 把用户身份完全交给你 IdP 管的方案。卖点很干净:每个用户账号通过 SCIM 从 Okta 或 Entra ID 同步过来,用户走 SSO 登录,IdP 删人就账号消失,命名空间公司控制。卖点和集成现实之间的差就是团队没预留的那两周。
影响集成设计的 EMU 硬性约束:
- 不能有 personal repository。 所有 repo 必须挂在 enterprise 内的 org 下。
/user/repos创建个人 repo 端点返回 403。想要 sandbox 的开发者要么用 org 级 "sandboxes" repo,要么另开一个 GitHub.com 个人账号。 - 不能 fork 到 enterprise 外。 fork API 在目标不在 enterprise 内时返回 403。依赖 fork 上游开源项目回馈贡献的工作流不行;支持的模式是另开一个非 EMU 账号做 OSS 贡献。
- 不能跨 org 评论或 star。 EMU 用户没法给 github.com 公开 repo 的 issue 评论或 star。这会打断那种「开发者对外部项目评论时触发 Slack 通知」的集成。
- 用户名变形。 EMU 用户名带
_short_code后缀(基于 enterprise short code 衍生)。任何模式匹配 GitHub 用户名的集成(Slack handle、IdP 属性)要知道有这个后缀。 - 不和 GitHub.com 个人账号合并。 如果开发者已有 GitHub.com 账号,EMU 不合并也不关联,是两个独立实体。
集成设计原则:任何产品特性如果依赖跨 org GitHub 活动(在 OSS 里 @ 人、profile 浏览、公开贡献),就不能只靠 EMU 账号。要么这些特性走非 EMU 服务账号,要么绕开边界设计。
GHES 版本错位与 X-GitHub-Api-Version 头
GitHub Enterprise Server 大约 3 个月一个新版本,每个版本支持 12 个月左右。2026 年生产部署里客户的版本分布大致:
| GHES 版本 | 发布时间 | 生产里常见? |
|---|---|---|
| 3.8 | 2025 下半年 | 是(最新) |
| 3.7 | 2025 上半年 | 是 |
| 3.6 | 2024 下半年 | 是 |
| 3.5 | 2024 上半年 | 部分 |
| 3.4 及以前 | 2023 及以前 | 少见但仍存在 |
GHEC 加的端点带着滞后进入 GHES。audit log 的 Streaming API 是 GHEC 独占。SCIM 增强常常先在 GHEC 落地两个 GHES 版本之后才打入 GHES tag build。要覆盖这个版本跨度的客户群,代码得做两件事。
一,每个 REST 调用带 X-GitHub-Api-Version 头。不带的话 GitHub 默认 2022-11-28(支持到 2028-03-10)。当前的 2026-03-10 是更好的目标,如果客户实例较新。这个选择重要,因为新版本有破坏性变更(重命名字段、删除废弃端点),默认值不会暴露这些。
二,对每个 GHES 实例的集成会话开始时调 GET /meta 拿到当前安装的 GHES 版本。代码按这个版本分支,不假设最新。response 上的 installed_version 字段是 key。
GraphQL 有自己的版本模型。GitHub 废弃的 mutation 和类型在废弃后至少 12 个月仍可用,客户端在 introspection schema 里看到 @deprecated 注解。如果代码用 GraphQL Code Generator 或类似工具钉到一个 schema 快照,每季度针对客户群支持的最新 GHES 版本刷新一次快照。
企业规模 org 上的 REST vs GraphQL
REST 还是 GraphQL 的判断规则,企业规模下比文档暗示的更微妙。关键是点数系统。GraphQL 每次查询读 1 点、mutation 5 点,每小时上限 5,000 点。REST 等价物每次请求算 1 调用,不管复杂度。
大型 org 的算术常在读重工作流上翻向 GraphQL,因为一次 GraphQL 查询可以拉走否则要几十次 REST 调用的数据。例子:取一个仓库最近的 issue + 其评论 + label + assignee,REST 要 1+N 次(1 次列表,N 次详情)调用,GraphQL 一次查询。5,000 个 issue 时 REST 烧 5,001 调用,GraphQL 烧 1 点。GraphQL 赢。
写重工作流算术反过来。GraphQL mutation 每次 5 点对 REST 1 调用。在 REST 里批量建 1,000 个 issue 是 1,000 调用(小时限制内宽裕但撞内容创建的次要限制 80/min、500/hour)。在 GraphQL 里 1,000 mutation 是 5,000 点,正好是小时上限,内容创建次要限制依然适用。写量大时通常 REST 更好,每请求开销小,点数封顶不是瓶颈。
另一个实际差别是错误处理。REST 错误是 HTTP 状态码 + JSON body。GraphQL 永远返回 200,错误嵌在 response body 的 errors 数组里,单次查询能部分成功——某些字段填了,另一些字段带错误。集成代码要为「REST 4xx 响应」和「GraphQL 200 响应带 errors 数组」分别走两条路径。
上 GHEC 或 GHES 之前的清单
把 enterprise GitHub 集成从原型推到生产前的具体清单:
第一天就选 GitHub Apps 不选 PAT。 后期从 PAT 迁过去成本高(重发 token、重写鉴权流、重新 onboard 用户)。原型期立 App 多花约两小时,省掉整段迁移。
每个 REST 调用都钉
X-GitHub-Api-Version。 显式指定日期字符串,不靠默认。等 GitHub 最终退役 2022-11-28 默认值时,不带 header 的代码会静默切到次老版本。第一个请求就埋全部六个
X-RateLimit-*header。 包括X-RateLimit-Resource,这是「我们小时配额用完了」和「我们在某个 resource 桶上被限流」两件事的分辨依据。如果客户群跨 GHEC 和 GHES,按两种都规划。 特性探测优先于版本判断。集成测试跑客户群用的 GHES 版本,不光跑最新。
卖到受监管行业的话,按 audit log API ingestion 留预算。 500 席客户每月 5 万~20 万事件,这个量决定 ingestion buffer 设计、分区策略、保留策略的规模。
销售对话前把数据和用户模型对到 EMU 边界上。 卖完集成等客户铺开 EMU 才发现集成挂掉,是丢六位数年合同的那种错误。
读重批量用 GraphQL,写重批量用 REST。 点数封顶和每请求开销两个方向相反,匹配协议到工作负载形态能让有效吞吐差一个数量级。
速率上限、鉴权方法、Enterprise 独占端点的合并参考在 GitHub Enterprise API 工具页上。更大的开发者工具 API 目录在开发者工具分类。
快速跳到对应段落
下一步
读完后可以继续回到工具目录,对比具体产品。
去看工具