From a78bedb772e6793d88a8b9a9360941afbb3b7858 Mon Sep 17 00:00:00 2001 From: Kynan Ware <47394200+BagToad@users.noreply.github.com> Date: Wed, 6 May 2026 16:05:07 -0600 Subject: [PATCH] Wrap relationship JSON output in {nodes, totalCount}, fetch full pages The subIssues, blockedBy, and blocking JSON output is currently shaped as a flat array, which silently truncates when there are more entries than the GraphQL fragment fetches. That's misleading once a user crosses the page size, so this switches each connection to a {nodes, totalCount} object so consumers can see when there's more. While confirming page sizes, the GitHub limits turn out to be: - sub-issues: up to 100 per parent https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/adding-sub-issues - blocked-by / blocking: up to 50 per relationship type https://github.blog/changelog/2025-08-21-dependencies-on-issues/ So subIssues moves to first:100 to fetch the full set; blockedBy and blocking stay at first:50, which already covers their cap. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- api/export_pr.go | 15 ++++++++++++--- api/queries_issue.go | 3 ++- api/query_builder.go | 6 +++--- pkg/cmd/issue/view/view_test.go | 30 ++++++++++++++++++++++-------- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/api/export_pr.go b/api/export_pr.go index 454a4e05c..53a921e43 100644 --- a/api/export_pr.go +++ b/api/export_pr.go @@ -71,7 +71,10 @@ func (issue *Issue) ExportData(fields []string) map[string]interface{} { "state": n.State, }) } - data[f] = items + data[f] = map[string]interface{}{ + "nodes": items, + "totalCount": issue.SubIssues.TotalCount, + } case "subIssuesSummary": data[f] = map[string]interface{}{ "total": issue.SubIssuesSummary.Total, @@ -89,7 +92,10 @@ func (issue *Issue) ExportData(fields []string) map[string]interface{} { "state": n.State, }) } - data[f] = items + data[f] = map[string]interface{}{ + "nodes": items, + "totalCount": issue.BlockedBy.TotalCount, + } case "blocking": items := make([]map[string]interface{}, 0, len(issue.Blocking.Nodes)) for _, n := range issue.Blocking.Nodes { @@ -101,7 +107,10 @@ func (issue *Issue) ExportData(fields []string) map[string]interface{} { "state": n.State, }) } - data[f] = items + data[f] = map[string]interface{}{ + "nodes": items, + "totalCount": issue.Blocking.TotalCount, + } default: sf := fieldByName(v, f) data[f] = sf.Interface() diff --git a/api/queries_issue.go b/api/queries_issue.go index 1cd5549a4..bcdd74058 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -91,7 +91,8 @@ type SubIssuesSummary struct { // LinkedIssueConnection is a connection of related issues (blocked-by or blocking). type LinkedIssueConnection struct { - Nodes []LinkedIssue `json:"nodes"` + Nodes []LinkedIssue `json:"nodes"` + TotalCount int `json:"totalCount"` } type ClosedByPullRequestsReferences struct { diff --git a/api/query_builder.go b/api/query_builder.go index 63c61cb45..d988cdc67 100644 --- a/api/query_builder.go +++ b/api/query_builder.go @@ -447,13 +447,13 @@ func IssueGraphQL(fields []string) string { case "parent": q = append(q, `parent{id,number,title,url,state,repository{nameWithOwner}}`) case "subIssues": - q = append(q, `subIssues(first:50){nodes{id,number,title,url,state,repository{nameWithOwner}},totalCount}`) + q = append(q, `subIssues(first:100){nodes{id,number,title,url,state,repository{nameWithOwner}},totalCount}`) case "subIssuesSummary": q = append(q, `subIssuesSummary{total,completed,percentCompleted}`) case "blockedBy": - q = append(q, `blockedBy(first:50){nodes{id,number,title,url,state,repository{nameWithOwner}}}`) + q = append(q, `blockedBy(first:50){nodes{id,number,title,url,state,repository{nameWithOwner}},totalCount}`) case "blocking": - q = append(q, `blocking(first:50){nodes{id,number,title,url,state,repository{nameWithOwner}}}`) + q = append(q, `blocking(first:50){nodes{id,number,title,url,state,repository{nameWithOwner}},totalCount}`) default: q = append(q, field) } diff --git a/pkg/cmd/issue/view/view_test.go b/pkg/cmd/issue/view/view_test.go index a233f3ebe..72b23fa18 100644 --- a/pkg/cmd/issue/view/view_test.go +++ b/pkg/cmd/issue/view/view_test.go @@ -674,10 +674,12 @@ func issueResponseAllIssues2Fields() string { }, "subIssuesSummary": {"total":2,"completed":1,"percentCompleted":50.0}, "blockedBy": { - "nodes": [{"number":200,"title":"API rate limiting","url":"https://github.com/OWNER/REPO/issues/200","state":"OPEN","repository":{"nameWithOwner":"OWNER/REPO"}}] + "nodes": [{"number":200,"title":"API rate limiting","url":"https://github.com/OWNER/REPO/issues/200","state":"OPEN","repository":{"nameWithOwner":"OWNER/REPO"}}], + "totalCount": 1 }, "blocking": { - "nodes": [{"number":300,"title":"Release v2.0","url":"https://github.com/OWNER/REPO/issues/300","state":"OPEN","repository":{"nameWithOwner":"OWNER/REPO"}}] + "nodes": [{"number":300,"title":"Release v2.0","url":"https://github.com/OWNER/REPO/issues/300","state":"OPEN","repository":{"nameWithOwner":"OWNER/REPO"}}], + "totalCount": 1 } } } } }` } @@ -878,8 +880,12 @@ func TestIssueView_json_ParentSubIssues(t *testing.T) { assert.Equal(t, "OPEN", parent["state"]) // Sub-issues - subIssues, ok := data["subIssues"].([]interface{}) - require.True(t, ok, "subIssues should be an array") + subIssuesObj, ok := data["subIssues"].(map[string]interface{}) + require.True(t, ok, "subIssues should be an object") + assert.Equal(t, float64(2), subIssuesObj["totalCount"]) + + subIssues, ok := subIssuesObj["nodes"].([]interface{}) + require.True(t, ok, "subIssues.nodes should be an array") require.Len(t, subIssues, 2) sub0 := subIssues[0].(map[string]interface{}) @@ -916,8 +922,12 @@ func TestIssueView_json_BlockedByBlocking(t *testing.T) { require.NoError(t, json.Unmarshal(output.OutBuf.Bytes(), &data)) // Blocked by - blockedBy, ok := data["blockedBy"].([]interface{}) - require.True(t, ok, "blockedBy should be an array") + blockedByObj, ok := data["blockedBy"].(map[string]interface{}) + require.True(t, ok, "blockedBy should be an object") + assert.Equal(t, float64(1), blockedByObj["totalCount"]) + + blockedBy, ok := blockedByObj["nodes"].([]interface{}) + require.True(t, ok, "blockedBy.nodes should be an array") require.Len(t, blockedBy, 1) blocked0 := blockedBy[0].(map[string]interface{}) @@ -927,8 +937,12 @@ func TestIssueView_json_BlockedByBlocking(t *testing.T) { assert.Equal(t, "OPEN", blocked0["state"]) // Blocking - blocking, ok := data["blocking"].([]interface{}) - require.True(t, ok, "blocking should be an array") + blockingObj, ok := data["blocking"].(map[string]interface{}) + require.True(t, ok, "blocking should be an object") + assert.Equal(t, float64(1), blockingObj["totalCount"]) + + blocking, ok := blockingObj["nodes"].([]interface{}) + require.True(t, ok, "blocking.nodes should be an array") require.Len(t, blocking, 1) blocking0 := blocking[0].(map[string]interface{})