编程 CLOSED ≠ MERGED:GitHub 关闭语义里两处会静默失效的地方

2026-10-30 00:03:34

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 open
  • CLOSED:closed without being merged
  • MERGED: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
    }
  }
}

参考文档:

推荐文章

程序员茄子在线接单