Skip to content

Commit 80fc69b

Browse files
fix(issues): fall back on unsupported field schemas
Retry list_issues without custom issue field dependencies only when the host schema lacks them. Preserve explicit field filters and propagate unrelated GraphQL errors. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 3085e59 commit 80fc69b

3 files changed

Lines changed: 404 additions & 12 deletions

File tree

pkg/github/issues.go

Lines changed: 150 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -507,12 +507,41 @@ type IssueFragment struct {
507507
} `graphql:"issueFieldValues(first: 25)"`
508508
}
509509

510+
type issueFragmentWithoutFieldValues struct {
511+
Number githubv4.Int
512+
Title githubv4.String
513+
Body githubv4.String
514+
State githubv4.String
515+
DatabaseID int64
516+
517+
Author struct {
518+
Login githubv4.String
519+
}
520+
CreatedAt githubv4.DateTime
521+
UpdatedAt githubv4.DateTime
522+
Labels struct {
523+
Nodes []struct {
524+
Name githubv4.String
525+
ID githubv4.String
526+
Description githubv4.String
527+
}
528+
} `graphql:"labels(first: 100)"`
529+
Comments struct {
530+
TotalCount githubv4.Int
531+
} `graphql:"comments"`
532+
}
533+
510534
// Common interface for all issue query types
511535
type IssueQueryResult interface {
512536
GetIssueFragment() IssueQueryFragment
513537
GetIsPrivate() bool
514538
}
515539

540+
type issueQueryResultWithoutFieldValues interface {
541+
getIssueFragmentWithoutFieldValues() issueQueryFragmentWithoutFieldValues
542+
GetIsPrivate() bool
543+
}
544+
516545
type IssueQueryFragment struct {
517546
Nodes []IssueFragment `graphql:"nodes"`
518547
PageInfo struct {
@@ -524,6 +553,17 @@ type IssueQueryFragment struct {
524553
TotalCount int
525554
}
526555

556+
type issueQueryFragmentWithoutFieldValues struct {
557+
Nodes []issueFragmentWithoutFieldValues `graphql:"nodes"`
558+
PageInfo struct {
559+
HasNextPage githubv4.Boolean
560+
HasPreviousPage githubv4.Boolean
561+
StartCursor githubv4.String
562+
EndCursor githubv4.String
563+
}
564+
TotalCount int
565+
}
566+
527567
// ListIssuesQuery is the root query structure for fetching issues with optional label filtering.
528568
type ListIssuesQuery struct {
529569
Repository struct {
@@ -556,6 +596,34 @@ type ListIssuesQueryTypeWithLabelsWithSince struct {
556596
} `graphql:"repository(owner: $owner, name: $repo)"`
557597
}
558598

599+
type listIssuesQueryWithoutFieldValues struct {
600+
Repository struct {
601+
Issues issueQueryFragmentWithoutFieldValues `graphql:"issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction})"`
602+
IsPrivate githubv4.Boolean
603+
} `graphql:"repository(owner: $owner, name: $repo)"`
604+
}
605+
606+
type listIssuesQueryWithLabelsWithoutFieldValues struct {
607+
Repository struct {
608+
Issues issueQueryFragmentWithoutFieldValues `graphql:"issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction})"`
609+
IsPrivate githubv4.Boolean
610+
} `graphql:"repository(owner: $owner, name: $repo)"`
611+
}
612+
613+
type listIssuesQueryWithSinceWithoutFieldValues struct {
614+
Repository struct {
615+
Issues issueQueryFragmentWithoutFieldValues `graphql:"issues(first: $first, after: $after, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {since: $since})"`
616+
IsPrivate githubv4.Boolean
617+
} `graphql:"repository(owner: $owner, name: $repo)"`
618+
}
619+
620+
type listIssuesQueryWithLabelsAndSinceWithoutFieldValues struct {
621+
Repository struct {
622+
Issues issueQueryFragmentWithoutFieldValues `graphql:"issues(first: $first, after: $after, labels: $labels, states: $states, orderBy: {field: $orderBy, direction: $direction}, filterBy: {since: $since})"`
623+
IsPrivate githubv4.Boolean
624+
} `graphql:"repository(owner: $owner, name: $repo)"`
625+
}
626+
559627
// IssueFieldValueFilter mirrors the GraphQL IssueFieldValueFilter input. Exactly one typed value
560628
// field should be set per filter (the monolith resolver rejects multiple).
561629
type IssueFieldValueFilter struct {
@@ -593,6 +661,38 @@ func (q *ListIssuesQueryTypeWithLabelsWithSince) GetIsPrivate() bool {
593661
return bool(q.Repository.IsPrivate)
594662
}
595663

664+
func (q *listIssuesQueryWithoutFieldValues) getIssueFragmentWithoutFieldValues() issueQueryFragmentWithoutFieldValues {
665+
return q.Repository.Issues
666+
}
667+
668+
func (q *listIssuesQueryWithoutFieldValues) GetIsPrivate() bool {
669+
return bool(q.Repository.IsPrivate)
670+
}
671+
672+
func (q *listIssuesQueryWithLabelsWithoutFieldValues) getIssueFragmentWithoutFieldValues() issueQueryFragmentWithoutFieldValues {
673+
return q.Repository.Issues
674+
}
675+
676+
func (q *listIssuesQueryWithLabelsWithoutFieldValues) GetIsPrivate() bool {
677+
return bool(q.Repository.IsPrivate)
678+
}
679+
680+
func (q *listIssuesQueryWithSinceWithoutFieldValues) getIssueFragmentWithoutFieldValues() issueQueryFragmentWithoutFieldValues {
681+
return q.Repository.Issues
682+
}
683+
684+
func (q *listIssuesQueryWithSinceWithoutFieldValues) GetIsPrivate() bool {
685+
return bool(q.Repository.IsPrivate)
686+
}
687+
688+
func (q *listIssuesQueryWithLabelsAndSinceWithoutFieldValues) getIssueFragmentWithoutFieldValues() issueQueryFragmentWithoutFieldValues {
689+
return q.Repository.Issues
690+
}
691+
692+
func (q *listIssuesQueryWithLabelsAndSinceWithoutFieldValues) GetIsPrivate() bool {
693+
return bool(q.Repository.IsPrivate)
694+
}
695+
596696
func getIssueQueryType(hasLabels bool, hasSince bool) any {
597697
switch {
598698
case hasLabels && hasSince:
@@ -606,6 +706,29 @@ func getIssueQueryType(hasLabels bool, hasSince bool) any {
606706
}
607707
}
608708

709+
func getIssueQueryTypeWithoutFieldValues(hasLabels bool, hasSince bool) issueQueryResultWithoutFieldValues {
710+
switch {
711+
case hasLabels && hasSince:
712+
return &listIssuesQueryWithLabelsAndSinceWithoutFieldValues{}
713+
case hasLabels:
714+
return &listIssuesQueryWithLabelsWithoutFieldValues{}
715+
case hasSince:
716+
return &listIssuesQueryWithSinceWithoutFieldValues{}
717+
default:
718+
return &listIssuesQueryWithoutFieldValues{}
719+
}
720+
}
721+
722+
func isUnsupportedListIssuesIssueFieldsError(err error) bool {
723+
switch err.Error() {
724+
case "IssueFieldValueFilter isn't a defined input type (on $issueFieldValues)",
725+
"Field 'issueFieldValues' doesn't exist on type 'Issue'":
726+
return true
727+
default:
728+
return false
729+
}
730+
}
731+
609732
// IssueRead creates a tool to get details of a specific issue in a GitHub repository.
610733
func IssueRead(t translations.TranslationHelperFunc) inventory.ServerTool {
611734
schema := &jsonschema.Schema{
@@ -3003,16 +3126,37 @@ func ListIssues(t translations.TranslationHelperFunc) inventory.ServerTool {
30033126
// is a no-op once the flags are globally rolled out.
30043127
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
30053128
if err := client.Query(ctxWithFeatures, issueQuery, vars); err != nil {
3006-
return ghErrors.NewGitHubGraphQLErrorResponse(
3007-
ctx,
3008-
"failed to list issues",
3009-
err,
3010-
), nil, nil
3129+
if len(fieldFilters) > 0 || !isUnsupportedListIssuesIssueFieldsError(err) {
3130+
return ghErrors.NewGitHubGraphQLErrorResponse(
3131+
ctx,
3132+
"failed to list issues",
3133+
err,
3134+
), nil, nil
3135+
}
3136+
3137+
issueQueryWithoutFieldValues := getIssueQueryTypeWithoutFieldValues(hasLabels, hasSince)
3138+
varsWithoutFieldValues := make(map[string]any, len(vars)-1)
3139+
for name, value := range vars {
3140+
if name != "issueFieldValues" {
3141+
varsWithoutFieldValues[name] = value
3142+
}
3143+
}
3144+
if err := client.Query(ctx, issueQueryWithoutFieldValues, varsWithoutFieldValues); err != nil {
3145+
return ghErrors.NewGitHubGraphQLErrorResponse(
3146+
ctx,
3147+
"failed to list issues",
3148+
err,
3149+
), nil, nil
3150+
}
3151+
issueQuery = issueQueryWithoutFieldValues
30113152
}
30123153

30133154
var resp MinimalIssuesResponse
30143155
var isPrivate bool
3015-
if queryResult, ok := issueQuery.(IssueQueryResult); ok {
3156+
if queryResult, ok := issueQuery.(issueQueryResultWithoutFieldValues); ok {
3157+
resp = convertToMinimalIssuesResponseWithoutFieldValues(queryResult.getIssueFragmentWithoutFieldValues())
3158+
isPrivate = queryResult.GetIsPrivate()
3159+
} else if queryResult, ok := issueQuery.(IssueQueryResult); ok {
30163160
resp = convertToMinimalIssuesResponse(queryResult.GetIssueFragment())
30173161
isPrivate = queryResult.GetIsPrivate()
30183162
}

0 commit comments

Comments
 (0)