CLOSED ≠ MERGED:GitHub 关闭语义里两处会静默失效的地方
GitHub 页面上的 PR 只有三种状态:Open、Closed、Merged。但 API 和关键词解析对「CLOSED」的定义并不一致,实际容易出错的地方集中在两处:REST 的 state=closed 会把已合并的 PR 一并算进来,另一处是 PR 描述里的 closing keywords 只在默认分支上生效。下面先说明 PR 和 Issue 各自的 CLOSED 状态,再对照 REST 与 GraphQL 的差异,最后给出可直接运行的查询。
PR 与 Issue 的 CLOSED 不是一回事
PullRequest:CLOSED 与 MERGED 互斥
GraphQL 的 PullRequestState 枚举有三个值:
OPEN:still openCLOSED:closed without being mergedMERGED:closed by being merged
CLOSED 单独一个值表示它表示「关了但没合并」。StackOverflow 上一条常见问答也印证了这点:页面上三种状态是 open(待处理)、closed(被拒)、merged(已接受),看到 Closed 就意味着没被接受。在 GraphQL 里,CLOSED 和 MERGED 是两个互斥的终态,不需要额外字段判断。
Issue:没有 merged,但有 reason
Issue 只有 open / closed / all 三态,从没有 merged。2022 年起 GitHub 给 Issue 的关闭加了 reason 概念,分 completed(已完成)和 not planned(不计划处理)。
对应到 API:
- REST 的 issue 对象带
state_reason字段,取值completed/not_planned/reopened - GraphQL 有
IssueStateReason枚举:COMPLETED/NOT_PLANNED/REOPENED
搜索语法是 is:issue is:closed reason:completed 和 reason:not_planned。这里用的是下划线小写,和 REST 的 not_planned 一致,但和 GraphQL 枚举的全大写写法不同——跨接口拼字符串时容易写错:REST 用 not_planned,GraphQL 用 NOT_PLANNED,搜索里用 reason:not_planned。
REST 的 state=closed 会混入 merged
REST 的 GET /repos/{owner}/{repo}/pulls 接口,state 参数只接受 open / closed / all,没有 merged。合并过的 PR 也归在 closed 状态里。要区分,得看返回对象里的 merged_at 是否非空,或者用 merged 布尔字段。
这和 GraphQL 的设定直接冲突:同一个 PR,GraphQL 报 MERGED,REST 的列表接口把它归到 closed。写统计时如果按 REST 的 state=closed 计数,会把已合并和未合并混在一起,数字偏大且不会报错。
另外,GET /repos/{owner}/{repo}/issues 会把 PR 也当成 issue 返回,过滤时要排掉带 pull_request 字段的条目,否则 Issue 的统计同样会混入 PR。
closing keywords:默认分支之外一律不解析
PR 描述或 commit message 里可以写关键词自动关联并关闭 issue。关键词是:
close、closes、closed、fix、fixes、fixed、resolve、resolves、resolved
语法形如 Closes #10、Fixes octo-org/octo-repo#100。多个 issue 必须每个都前置关键词,不能只写一次然后跟一串编号。大小写不敏感,也可以带冒号,如 CLOSES: #10。
关键边界是:只有 PR 的目标分支是仓库默认分支时,PR 描述里的关键词才会被解析。PR 打向任何其他分支时,关键词被忽略、不建立链接,合并后对 issue 也没有影响。这是最常见的静默失效方式——PR 看起来写对了,合并后 issue 仍然开着。
在 commit message 里用关键词的情况略有不同:issue 会在提交合入默认分支时关闭,但包含该提交的 PR 不会出现在 issue 的 linked pull requests 列表里。链接关系只来自 commit,不来自 PR。
2025-04-23 起,仓库管理员可以在 Repository settings → General → Issues 里选择合并 linked PR 时是否自动关闭 issue。默认仍然开启,保留旧行为。这个开关存在的原因是合并 PR 不等于工作完成,后面还有 QA 和验证步骤。
链接关系的解除也分两种:只有手动链接的 PR 才能手动取消链接;通过关键词建立的链接不能直接解除,必须改 PR 描述,把关键词删掉。
可复现的查询
REST,列出 closed 的 PR 并标出哪些其实已合并:
curl -s -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/OWNER/REPO/pulls?state=closed&per_page=100" \
| jq '.[] | {number, merged: (.merged_at != null), merged_at}'
REST,查看 Issue 的关闭 reason,先排掉 PR:
curl -s -H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/OWNER/REPO/issues?state=closed&per_page=100" \
| jq '.[] | select(.pull_request == null) | {number, state_reason}'
GraphQL,一次取出状态和 reason:
query {
repository(owner: "OWNER", name: "REPO") {
pullRequest(number: 123) {
state # OPEN / CLOSED / MERGED
merged
mergedAt
}
issue(number: 456) {
state # OPEN / CLOSED
stateReason # COMPLETED / NOT_PLANNED / REOPENED
}
}
}
参考文档: