Skip to content

Commit 13e7f10

Browse files
Add custom field filtering to list_issues (#2480)
* Add custom field filtering to list_issues * Flatten schema * add repo fields flag * test fix --------- Co-authored-by: Sam Morrow <sammorrowdrums@github.com>
1 parent 8f6050a commit 13e7f10

5 files changed

Lines changed: 755 additions & 133 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -885,6 +885,7 @@ The following sets of tools are available:
885885
- **Required OAuth Scopes**: `repo`
886886
- `after`: Cursor for pagination. Use the endCursor from the previous page's PageInfo for GraphQL APIs. (string, optional)
887887
- `direction`: Order direction. If provided, the 'orderBy' also needs to be provided. (string, optional)
888+
- `field_filters`: Filter by custom issue field values. Each entry takes a field_name and a value; the server looks up the field and coerces the value to its type (single-select option name, text, number, or YYYY-MM-DD date). (object[], optional)
888889
- `labels`: Filter by labels (string[], optional)
889890
- `orderBy`: Order issues by field. If provided, the 'direction' also needs to be provided. (string, optional)
890891
- `owner`: Repository owner (string, required)

pkg/github/__toolsnaps__/list_issues.snap

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,27 @@
1818
],
1919
"type": "string"
2020
},
21+
"field_filters": {
22+
"description": "Filter by custom issue field values. Each entry takes a field_name and a value; the server looks up the field and coerces the value to its type (single-select option name, text, number, or YYYY-MM-DD date).",
23+
"items": {
24+
"properties": {
25+
"field_name": {
26+
"description": "Name of the custom field (e.g. \"Priority\"). Case-insensitive.",
27+
"type": "string"
28+
},
29+
"value": {
30+
"description": "Value to filter on. For single-select fields, the option name (e.g. \"P1\"). For dates, YYYY-MM-DD. For numbers, the numeric value as a string. For text, the text value.",
31+
"type": "string"
32+
}
33+
},
34+
"required": [
35+
"field_name",
36+
"value"
37+
],
38+
"type": "object"
39+
},
40+
"type": "array"
41+
},
2142
"labels": {
2243
"description": "Filter by labels",
2344
"items": {

pkg/github/issue_fields.go

Lines changed: 88 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -136,81 +136,9 @@ func ListIssueFields(t translations.TranslationHelperFunc) inventory.ServerTool
136136
return utils.NewToolResultErrorFromErr("failed to get GitHub GraphQL client", err), nil, nil
137137
}
138138

139-
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
140-
var nodes []issueFieldNode
141-
if repo != "" {
142-
var query issueFieldsRepoQuery
143-
vars := map[string]any{
144-
"owner": githubv4.String(owner),
145-
"name": githubv4.String(repo),
146-
}
147-
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
148-
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to list issue fields", err), nil, nil
149-
}
150-
nodes = query.Repository.IssueFields.Nodes
151-
} else {
152-
var query issueFieldsOrgQuery
153-
vars := map[string]any{
154-
"login": githubv4.String(owner),
155-
}
156-
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
157-
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to list issue fields", err), nil, nil
158-
}
159-
nodes = query.Organization.IssueFields.Nodes
160-
}
161-
162-
fields := make([]IssueField, 0, len(nodes))
163-
for _, node := range nodes {
164-
var f IssueField
165-
// Read from the fragment matching __typename; the other fragments are zero-valued.
166-
switch string(node.TypeName) {
167-
case "IssueFieldSingleSelect":
168-
opts := make([]IssueSingleSelectFieldOption, 0, len(node.IssueFieldSingleSelect.Options))
169-
for _, o := range node.IssueFieldSingleSelect.Options {
170-
opts = append(opts, IssueSingleSelectFieldOption{
171-
ID: fmt.Sprintf("%v", o.ID),
172-
Name: string(o.Name),
173-
Description: string(o.Description),
174-
Color: string(o.Color),
175-
Priority: o.Priority,
176-
})
177-
}
178-
f = IssueField{
179-
ID: fmt.Sprintf("%v", node.IssueFieldSingleSelect.ID),
180-
Name: string(node.IssueFieldSingleSelect.Name),
181-
Description: string(node.IssueFieldSingleSelect.Description),
182-
DataType: string(node.IssueFieldSingleSelect.DataType),
183-
Visibility: string(node.IssueFieldSingleSelect.Visibility),
184-
Options: opts,
185-
}
186-
case "IssueFieldText":
187-
f = IssueField{
188-
ID: fmt.Sprintf("%v", node.IssueFieldText.ID),
189-
Name: string(node.IssueFieldText.Name),
190-
Description: string(node.IssueFieldText.Description),
191-
DataType: string(node.IssueFieldText.DataType),
192-
Visibility: string(node.IssueFieldText.Visibility),
193-
}
194-
case "IssueFieldNumber":
195-
f = IssueField{
196-
ID: fmt.Sprintf("%v", node.IssueFieldNumber.ID),
197-
Name: string(node.IssueFieldNumber.Name),
198-
Description: string(node.IssueFieldNumber.Description),
199-
DataType: string(node.IssueFieldNumber.DataType),
200-
Visibility: string(node.IssueFieldNumber.Visibility),
201-
}
202-
case "IssueFieldDate":
203-
f = IssueField{
204-
ID: fmt.Sprintf("%v", node.IssueFieldDate.ID),
205-
Name: string(node.IssueFieldDate.Name),
206-
Description: string(node.IssueFieldDate.Description),
207-
DataType: string(node.IssueFieldDate.DataType),
208-
Visibility: string(node.IssueFieldDate.Visibility),
209-
}
210-
default:
211-
continue
212-
}
213-
fields = append(fields, f)
139+
fields, err := fetchIssueFields(ctx, gqlClient, owner, repo)
140+
if err != nil {
141+
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to list issue fields", err), nil, nil
214142
}
215143

216144
r, err := json.Marshal(fields)
@@ -221,3 +149,88 @@ func ListIssueFields(t translations.TranslationHelperFunc) inventory.ServerTool
221149
return utils.NewToolResultText(string(r)), nil, nil
222150
})
223151
}
152+
153+
// fetchIssueFields returns the issue field definitions for the given owner.
154+
// If repo is provided, fields are scoped to that repository (inherited from its
155+
// organization); otherwise fields are returned directly from the organization.
156+
func fetchIssueFields(ctx context.Context, gqlClient *githubv4.Client, owner, repo string) ([]IssueField, error) {
157+
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields")
158+
if repo != "" {
159+
var query issueFieldsRepoQuery
160+
vars := map[string]any{
161+
"owner": githubv4.String(owner),
162+
"name": githubv4.String(repo),
163+
}
164+
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
165+
return nil, err
166+
}
167+
return issueFieldsFromNodes(query.Repository.IssueFields.Nodes), nil
168+
}
169+
170+
var query issueFieldsOrgQuery
171+
vars := map[string]any{
172+
"login": githubv4.String(owner),
173+
}
174+
if err := gqlClient.Query(ctxWithFeatures, &query, vars); err != nil {
175+
return nil, err
176+
}
177+
return issueFieldsFromNodes(query.Organization.IssueFields.Nodes), nil
178+
}
179+
180+
// issueFieldsFromNodes converts GraphQL issue field union nodes into IssueField values.
181+
// Read from the fragment matching __typename; the other fragments are zero-valued.
182+
func issueFieldsFromNodes(nodes []issueFieldNode) []IssueField {
183+
fields := make([]IssueField, 0, len(nodes))
184+
for _, node := range nodes {
185+
var f IssueField
186+
switch string(node.TypeName) {
187+
case "IssueFieldSingleSelect":
188+
opts := make([]IssueSingleSelectFieldOption, 0, len(node.IssueFieldSingleSelect.Options))
189+
for _, o := range node.IssueFieldSingleSelect.Options {
190+
opts = append(opts, IssueSingleSelectFieldOption{
191+
ID: fmt.Sprintf("%v", o.ID),
192+
Name: string(o.Name),
193+
Description: string(o.Description),
194+
Color: string(o.Color),
195+
Priority: o.Priority,
196+
})
197+
}
198+
f = IssueField{
199+
ID: fmt.Sprintf("%v", node.IssueFieldSingleSelect.ID),
200+
Name: string(node.IssueFieldSingleSelect.Name),
201+
Description: string(node.IssueFieldSingleSelect.Description),
202+
DataType: string(node.IssueFieldSingleSelect.DataType),
203+
Visibility: string(node.IssueFieldSingleSelect.Visibility),
204+
Options: opts,
205+
}
206+
case "IssueFieldText":
207+
f = IssueField{
208+
ID: fmt.Sprintf("%v", node.IssueFieldText.ID),
209+
Name: string(node.IssueFieldText.Name),
210+
Description: string(node.IssueFieldText.Description),
211+
DataType: string(node.IssueFieldText.DataType),
212+
Visibility: string(node.IssueFieldText.Visibility),
213+
}
214+
case "IssueFieldNumber":
215+
f = IssueField{
216+
ID: fmt.Sprintf("%v", node.IssueFieldNumber.ID),
217+
Name: string(node.IssueFieldNumber.Name),
218+
Description: string(node.IssueFieldNumber.Description),
219+
DataType: string(node.IssueFieldNumber.DataType),
220+
Visibility: string(node.IssueFieldNumber.Visibility),
221+
}
222+
case "IssueFieldDate":
223+
f = IssueField{
224+
ID: fmt.Sprintf("%v", node.IssueFieldDate.ID),
225+
Name: string(node.IssueFieldDate.Name),
226+
Description: string(node.IssueFieldDate.Description),
227+
DataType: string(node.IssueFieldDate.DataType),
228+
Visibility: string(node.IssueFieldDate.Visibility),
229+
}
230+
default:
231+
continue
232+
}
233+
fields = append(fields, f)
234+
}
235+
return fields
236+
}

0 commit comments

Comments
 (0)