Skip to content

Commit 51cb3a0

Browse files
committed
docs(issue-1895): answer linking-API follow-up (R-15) + report platform gap upstream
The maintainer asked on PR #1896 whether GitHub exposes an API to link a PR to an issue (as the web-UI Development sidebar does): "maybe we just missed that API? Or if it does not exist, we need to report all the issues." Confirmed via live GraphQL schema introspection that NO such public API exists (REST or GraphQL) — we did not miss it. The only API-reachable mechanism is the `Fixes #N` closing keyword, which GitHub honors only for default-branch PRs (the exact limitation behind this issue). Even a hypothetical link API would still auto-close only on a default-branch merge, so the shipped post-merge close fallback remains necessary. Because the gap is real, reported it upstream (as konard): upvoted the three canonical community feature requests (#112224, #155339, #179613) and posted a reproducible evidence comment on #155339. - NEW docs/case-studies/issue-1895/github-api-linking-research.md: definitive answer + live introspection proof + willCloseTarget evidence + upstream-reports table + reproduction commands. - NEW data/: github-api-introspection.txt, github-upstream-discussions.txt, meta-language-evidence-refreshed.json (willCloseTarget:false proof; issues #49/#50 closed by #48/commit, never by their own PRs). - NEW experiments/issue-1895-api-research/discussion-155339-comment.md: exact text posted upstream. - Corrected the earlier "nothing to report" conclusion across external-report.md (Part 1/Part 2), requirements.md (R-11 + new R-15), analysis.md (§6), README.md (R-15 section). - Extended the changeset note. No code change: calling a non-existent API is impossible; the shipped head-branch search + post-merge close are the only viable response.
1 parent 7bdde69 commit 51cb3a0

10 files changed

Lines changed: 540 additions & 68 deletions

.changeset/issue-1895-non-default-base-auto-close.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,7 @@ exact failure reported for meta-language PRs #65/#66 / issues #49/#50.
3636
preserved, results are deduped/merged, and search failures degrade gracefully.
3737
- docs/case-studies/issue-1895: deep case study with downloaded GraphQL/PR/issue
3838
evidence, reconstructed timeline, root-cause analysis, requirement mapping, and the
39-
external-reporting decision.
39+
external-reporting decision. Includes `github-api-linking-research.md` — a
40+
definitive, introspection-backed answer to "is there an API to link a PR to an
41+
issue?" (no: confirmed via live GraphQL schema introspection), with the gap
42+
reported upstream (GitHub Community discussions #112224 / #155339 / #179613).

docs/case-studies/issue-1895/README.md

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ This folder is the deep case study for issue #1895, compiled as required by the
1010
issue itself ("make sure we compile that data to `./docs/case-studies/issue-{id}`
1111
folder, and use it to do deep case study analysis"). It contains:
1212

13-
| File | Purpose |
14-
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
15-
| [`README.md`](./README.md) | Overview, the verbatim problem, the reconstructed timeline, and the shipped solution at a glance |
16-
| [`requirements.md`](./requirements.md) | The exhaustive, numbered list of every requirement extracted from the issue, each mapped to where it is satisfied |
17-
| [`analysis.md`](./analysis.md) | Root-cause analysis, the evidence, design decisions and trade-offs |
18-
| [`existing-components.md`](./existing-components.md) | Survey of in-repo components reused, plus external prior art / GitHub behavior references |
19-
| [`external-report.md`](./external-report.md) | Decision on the "report to other repositories" requirement (meta-language) |
20-
| [`data/`](./data/) | Downloaded raw evidence (GraphQL dump, PR/issue JSON) for the meta-language PRs #65/#66 and issues #49/#50 |
13+
| File | Purpose |
14+
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
15+
| [`README.md`](./README.md) | Overview, the verbatim problem, the reconstructed timeline, and the shipped solution at a glance |
16+
| [`requirements.md`](./requirements.md) | The exhaustive, numbered list of every requirement extracted from the issue, each mapped to where it is satisfied |
17+
| [`analysis.md`](./analysis.md) | Root-cause analysis, the evidence, design decisions and trade-offs |
18+
| [`existing-components.md`](./existing-components.md) | Survey of in-repo components reused, plus external prior art / GitHub behavior references |
19+
| [`external-report.md`](./external-report.md) | Decision on the "report to other repositories" requirement (meta-language **and** the GitHub platform gap) |
20+
| [`github-api-linking-research.md`](./github-api-linking-research.md) | **R-15** — definitive answer to "is there an API to link a PR to an issue?" (no), with live proof + upstream reports |
21+
| [`data/`](./data/) | Downloaded raw evidence (GraphQL dumps, schema introspection, upstream-discussion snapshots) for #65/#66/#49/#50 |
2122

2223
---
2324

@@ -69,6 +70,34 @@ So a PR stacked onto another feature branch (a "sub-issue" branch such as
6970
"ISSUE LINK MISSING" warning fired even though `Fixes #N` was present), and
7071
2. **not** close its linked issue on merge.
7172

73+
## Maintainer follow-up: "is there an API to link a PR to an issue?" (R-15)
74+
75+
> _"We need to be able to do linking of pull requests to issues via API, as we can
76+
> do it manually in web UI. Maybe we just missed that API? Or if it does not exist,
77+
> we need to report all the issues."_
78+
79+
**We did not miss an API — it does not exist.** Live GraphQL schema introspection
80+
confirms there is **no** public mutation (and no REST endpoint) to link an existing
81+
PR to an issue the way the web-UI **Development** sidebar does. The only
82+
API-reachable mechanism is the `Fixes #N` closing keyword, which GitHub honors
83+
**only for default-branch PRs** — the exact limitation behind this issue.
84+
85+
Because the gap is real, we **reported it upstream** (as `konard`): upvoted the
86+
three canonical GitHub feature requests
87+
([#155339](https://github.com/orgs/community/discussions/155339) — link API,
88+
[#112224](https://github.com/orgs/community/discussions/112224) — non-default-branch
89+
auto-close,
90+
[#179613](https://github.com/orgs/community/discussions/179613) — read linked
91+
issues) and [posted a reproducible evidence comment](https://github.com/community/community/discussions/155339#discussioncomment-17288911)
92+
on #155339.
93+
94+
Full method, proof and reproduction:
95+
[`github-api-linking-research.md`](./github-api-linking-research.md). No new code
96+
was added — calling a non-existent API is impossible, and hive-mind's
97+
head-branch search + post-merge close are the only viable response (and remain
98+
necessary even if GitHub ships the API, since auto-close is still default-branch
99+
only).
100+
72101
## The evidence (see [`data/meta-language-graphql-evidence.json`](./data/meta-language-graphql-evidence.json))
73102

74103
| PR | head branch | base branch | merged | `closingIssuesReferences` | linked issue | issue state after merge |
@@ -80,6 +109,14 @@ So a PR stacked onto another feature branch (a "sub-issue" branch such as
80109
`issue-47-76af108c0f24`, a **non-default** branch → empty closing refs → issues
81110
left open. This is the textbook reproduction of the root cause.
82111

112+
**Two refresher findings** (see [`data/meta-language-evidence-refreshed.json`](./data/meta-language-evidence-refreshed.json)):
113+
114+
- GitHub itself records the broken link as **`willCloseTarget: false`** on the
115+
`Fixes #N` cross-reference — machine-readable proof the keyword was not honored.
116+
- Issues #49/#50 are **now closed**, but by unrelated default-branch activity
117+
(parent PR #48 / a later commit), **never by their own PRs #65/#66** (still empty
118+
closing refs). This reinforces — does not contradict — the root cause.
119+
83120
## Reconstructed timeline
84121

85122
1. hive-mind solves sub-issues #49 and #50, each on its own branch, **stacked**

docs/case-studies/issue-1895/analysis.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,22 @@ wording of the issue title.
7474
}
7575
```
7676

77-
Both PRs: non-default base, merged, **empty** closing refs. Issues #49 and #50:
78-
**OPEN**. Root cause confirmed beyond doubt.
77+
Both PRs: non-default base, merged, **empty** closing refs. Issues #49 and #50
78+
were **OPEN** at capture time. Root cause confirmed beyond doubt.
79+
80+
**Refreshed capture** ([`data/meta-language-evidence-refreshed.json`](./data/meta-language-evidence-refreshed.json))
81+
adds two findings:
82+
83+
1. **`willCloseTarget: false`.** The `Fixes #49` keyword in PR #65 _does_ create a
84+
`CrossReferencedEvent` in issue #49's timeline (so the relationship is visible),
85+
but GitHub stamps that cross-reference with **`willCloseTarget: false`** — a
86+
machine-readable admission that the keyword was **not** honored as a closing
87+
link, precisely because of the non-default base. The same holds for #66#50.
88+
2. **Issues #49/#50 are now CLOSED — but not by their own PRs.** Their
89+
`closingIssuesReferences` are still empty; they were closed later by unrelated
90+
default-branch activity (parent PR #48 / a subsequent commit), never by #65/#66.
91+
This _reinforces_ the root cause rather than contradicting it: the PRs that were
92+
supposed to close them never did.
7993

8094
## 3. Solution options and the chosen plan (R-7)
8195

@@ -153,3 +167,38 @@ observability so any recurrence is self-diagnosing:
153167
- `ensureLinkedIssueClosedAfterMerge` against a fake `$` exec: closes on
154168
non-default base, skips on default base / already-closed / no-keyword, and
155169
derives the issue number from the PR body when not supplied.
170+
171+
## 6. Is there an API to link a PR to an issue? (R-15 — maintainer follow-up)
172+
173+
On PR #1896 the maintainer asked whether GitHub exposes an API to link a PR to an
174+
issue the way the web-UI **Development** sidebar does — _"maybe we just missed that
175+
API? Or if it does not exist, we need to report all the issues."_
176+
177+
**Answer: there is no such public API; we did not miss it.** Verified by **live
178+
GraphQL schema introspection** (full method, evidence and reproduction in
179+
[`github-api-linking-research.md`](./github-api-linking-research.md)):
180+
181+
- The complete public mutation list contains **no** `linkPullRequestToIssue` /
182+
`addClosingIssueReference` / `connectIssue`. The only link-ish mutations are
183+
`createLinkedBranch`/`deleteLinkedBranch` (branches, not PRs; cannot link an
184+
_existing_ branch). Raw dump: [`data/github-api-introspection.txt`](./data/github-api-introspection.txt).
185+
- `CreatePullRequestInput` and `UpdatePullRequestInput` expose **no** linked-issue
186+
field.
187+
- REST has no such endpoint either (and no endpoint to even _read_ a PR's linked
188+
issues — upstream #179613).
189+
190+
The **only** API-reachable link is the closing keyword (`Fixes #N`), which GitHub
191+
honors **only for default-branch PRs** — the very limitation behind this issue. And
192+
even a hypothetical link API would still auto-close only on a default-branch merge
193+
(GitHub Docs), so it would restore _discoverability_, not auto-close — meaning the
194+
post-merge close fallback in §3 remains necessary regardless.
195+
196+
**Why no new code.** Because the link API does not exist, the engineering response
197+
is already complete and correct: deterministic `head:issue-N-` branch search for
198+
discovery (auto-continue) + explicit post-merge close for closure. Adding code that
199+
calls a non-existent API is impossible; re-targeting PRs to `main` was rejected in
200+
§3 (option D). So R-15's code path is "report upstream," which we did.
201+
202+
**What we reported (R-11).** We upvoted the three canonical feature requests and
203+
added a reproducible evidence comment — see [`external-report.md`](./external-report.md)
204+
and [`github-api-linking-research.md`](./github-api-linking-research.md) §5.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# GitHub GraphQL API introspection — PR<->issue linking mutations
2+
# Captured 2026-06-13T11:25:34Z against api.github.com (live schema)
3+
4+
## All mutations whose name matches link|connect|clos|refer:
5+
- closeDiscussion
6+
- closeIssue
7+
- closePullRequest
8+
- createLinkedBranch
9+
- deleteLinkedBranch
10+
- linkProjectV2ToRepository
11+
- linkProjectV2ToTeam
12+
- unlinkProjectV2FromRepository
13+
- unlinkProjectV2FromTeam
14+
- updateCheckSuitePreferences
15+
- updateSponsorshipPreferences
16+
17+
## createPullRequest input fields (no linked-issue / closing-reference field):
18+
- clientMutationId
19+
- repositoryId
20+
- baseRefName
21+
- headRefName
22+
- headRepositoryId
23+
- title
24+
- body
25+
- maintainerCanModify
26+
- draft
27+
28+
## updatePullRequest input fields (no linked-issue / closing-reference field):
29+
- clientMutationId
30+
- pullRequestId
31+
- baseRefName
32+
- title
33+
- body
34+
- state
35+
- maintainerCanModify
36+
- assigneeIds
37+
- milestoneId
38+
- labelIds
39+
- projectIds
40+
41+
## createLinkedBranch input (links a NEW branch to an issue — NOT a PR, cannot link existing branch):
42+
- clientMutationId
43+
- issueId
44+
- oid
45+
- name
46+
- repositoryId
47+
48+
## CONCLUSION: No mutation exists to link an existing PR (or existing branch) to an issue.
49+
## The web UI 'Development' sidebar link is backed by an internal, unexposed mutation.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Upstream GitHub Community discussions tracking the API/linking gaps
2+
# Captured 2026-06-13T11:25:46Z
3+
4+
## #112224 — Github should support closing issues by merging PRs to non-main branches
5+
url: https://github.com/orgs/community/discussions/112224
6+
category: Pull Requests
7+
upvotes: 6
8+
comments: 2
9+
answered: false
10+
locked: false
11+
12+
## #155339 — GraphQL: Missing mutations for linking existing branch and pull request
13+
url: https://github.com/orgs/community/discussions/155339
14+
category: Apps, API and Webhooks
15+
upvotes: 7
16+
comments: 3
17+
answered: false
18+
locked: false
19+
20+
## #179613 — How to Retrieve Linked Issues for a Pull Request via REST API
21+
url: https://github.com/orgs/community/discussions/179613
22+
category: Apps, API and Webhooks
23+
upvotes: 1
24+
comments: 4
25+
answered: false
26+
locked: false
27+
28+
## #177657 — How can I automatically close an issue when a pull request is merged?
29+
url: https://github.com/orgs/community/discussions/177657
30+
category: Other Feature Feedback, Questions, & Ideas
31+
upvotes: 1
32+
comments: 3
33+
answered: true
34+
locked: false
35+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"data": {
3+
"repository": {
4+
"defaultBranchRef": { "name": "main" },
5+
"pr65": { "number": 65, "baseRefName": "issue-47-76af108c0f24", "headRefName": "issue-49-3a3011bb1089", "merged": true, "mergedAt": "2026-06-11T00:11:58Z", "closingIssuesReferences": { "nodes": [] } },
6+
"pr66": { "number": 66, "baseRefName": "issue-47-76af108c0f24", "headRefName": "issue-50-2b26543616e5", "merged": true, "mergedAt": "2026-06-10T23:53:40Z", "closingIssuesReferences": { "nodes": [] } },
7+
"issue49": {
8+
"number": 49,
9+
"state": "CLOSED",
10+
"stateReason": "COMPLETED",
11+
"timelineItems": {
12+
"nodes": [
13+
{ "__typename": "CrossReferencedEvent", "willCloseTarget": false, "source": { "number": 48 } },
14+
{ "__typename": "CrossReferencedEvent", "willCloseTarget": false, "source": { "number": 65 } },
15+
{ "__typename": "CrossReferencedEvent", "willCloseTarget": false, "source": {} },
16+
{ "__typename": "ClosedEvent", "closer": { "__typename": "PullRequest", "number": 48 } }
17+
]
18+
}
19+
},
20+
"issue50": {
21+
"number": 50,
22+
"state": "CLOSED",
23+
"stateReason": "COMPLETED",
24+
"timelineItems": {
25+
"nodes": [
26+
{ "__typename": "CrossReferencedEvent", "willCloseTarget": false, "source": { "number": 48 } },
27+
{ "__typename": "CrossReferencedEvent", "willCloseTarget": false, "source": { "number": 66 } },
28+
{ "__typename": "CrossReferencedEvent", "willCloseTarget": false, "source": {} },
29+
{ "__typename": "CrossReferencedEvent", "willCloseTarget": false, "source": { "number": 80 } },
30+
{ "__typename": "ClosedEvent", "closer": { "__typename": "Commit" } }
31+
]
32+
}
33+
}
34+
}
35+
}
36+
}

0 commit comments

Comments
 (0)