Add multi_select issue field support (gated behind remote_mcp_issue_fields_multiselect)#2659
Conversation
There was a problem hiding this comment.
Pull request overview
Adds end-to-end support for multi_select issue fields across the MCP server’s issue-field surfaces (list field definitions, set/clear values via issue_write, and read/filter via list_issues), aligning behavior and schema with the existing single-select plumbing and ensuring GraphQL feature-gated fragments are actually exercised.
Changes:
- Threaded
multi_selectthrough GraphQL fragments + minimal output mapping (includingMinimalFieldValue.Values). - Extended
issue_writeto acceptfield_option_names: []stringfor multi-select, with option validation + improved delete semantics. - Extended
list_issuesfield filtering to acceptvalues: []stringfor multi-select (AND semantics) and updated toolsnaps/docs/tests accordingly.
Show a summary per file
| File | Description |
|---|---|
| pkg/github/minimal_types.go | Adds multi-select support in GraphQL→minimal field-value conversion (populates Values). |
| pkg/github/issues.go | Adds multi-select fragments, issue_write parsing/validation, list_issues multi-select filtering, and GraphQL-backed delete-on-clear behavior. |
| pkg/github/issues_test.go | Updates hardcoded GraphQL query string expectations and error-message assertions for new fragments/validation. |
| pkg/github/issues_multiselect_test.go | New targeted tests for multi-select parsing, filter resolution, fragment conversion, and delete mutation behavior. |
| pkg/github/issues_granular.go | Introduces DeleteIssueFieldValueInput type used by the new delete mutation path. |
| pkg/github/issue_fields.go | Extends list_issue_fields to surface multi_select field definitions and options. |
| pkg/github/toolsnaps/list_issues_ff_remote_mcp_issue_fields.snap | Updates schema snapshot for new field_filters[].values and revised description/required fields. |
| pkg/github/toolsnaps/list_issue_fields.snap | Updates schema snapshot to include multi_select in description/output expectations. |
| pkg/github/toolsnaps/issue_write_ff_remote_mcp_issue_fields.snap | Updates schema snapshot to include field_option_names and updated mutual-exclusion wording. |
| docs/insiders-features.md | Regenerates tool docs reflecting new issue_write.issue_fields[].field_option_names and list_issues.field_filters[].values. |
| docs/feature-flags.md | Regenerates feature-flag inventory/docs to reflect the updated tool schemas/descriptions. |
Copilot's findings
- Files reviewed: 11/11 changed files
- Comments generated: 3
| func matchOption(field IssueField, value string) (string, error) { | ||
| for _, o := range field.Options { | ||
| if strings.EqualFold(o.Name, value) { | ||
| return o.Name, nil | ||
| } | ||
| } | ||
| optionNames := make([]string, 0, len(field.Options)) | ||
| for _, o := range field.Options { | ||
| optionNames = append(optionNames, o.Name) | ||
| } | ||
| return "", fmt.Errorf("field_filters: %q is not a valid option for %q. Valid options: %s", value, field.Name, strings.Join(optionNames, ", ")) | ||
| } |
There was a problem hiding this comment.
Done — trimming both sides now, with a regression test.
akenneth
left a comment
There was a problem hiding this comment.
nice work! left a few questions I am unsure about.
Also tagging @iulia-b and @kelsey-myers for extra 👀 since you are familiar with this part
| type issueFieldWriteMetadataNode struct { | ||
| TypeName githubv4.String `graphql:"__typename"` | ||
| IssueFieldText struct { | ||
| ID githubv4.ID |
There was a problem hiding this comment.
Why is this needed? this wasn't here before for other types right?
There was a problem hiding this comment.
Yeah, it's new. The delete: true path now calls the deleteIssueFieldValue GraphQL mutation, which takes the field's node id as input — and delete can target any field type, so we grab id on every fragment. Before this PR there was no GraphQL delete, so we never needed the node id.
| } | ||
|
|
||
| func resolveIssueRequestFieldValues(ctx context.Context, gqlClient *githubv4.Client, owner, repo string, issueFields []issueWriteFieldInput) ([]*github.IssueRequestFieldValue, []int64, error) { | ||
| // fieldDeletion carries both identifiers needed to clear an issue field value: |
There was a problem hiding this comment.
Are you sure this change is needed?
There was a problem hiding this comment.
I think so — we added field_option_names as a third value setter, so the old 2-way value vs field_option_name mutual-exclusion check had to become 3-way. Happy to simplify if you can see a cleaner way though.
| // REST IssueField#build_value_attributes for multi_select expects an array of option names. | ||
| resolvedValue = resolvedNames | ||
| default: | ||
| // Raw value path. Reject it for multi_select so callers get a clear error |
There was a problem hiding this comment.
nit: i think we can reduce the comments here, the if is clear enough
| value = string(node.SingleSelectValue.Value) | ||
| case "IssueFieldMultiSelectValue": | ||
| fieldIDStr = node.MultiSelectValue.Field.FullDatabaseIDStr() | ||
| // REST IssueField#build_value_attributes for multi_select expects an array |
|
|
||
| var q searchIssuesNodesQuery | ||
| if err := gqlClient.Query(ctx, &q, map[string]any{"ids": ids}); err != nil { | ||
| ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "issue_fields", "repo_issue_fields") |
There was a problem hiding this comment.
Latent bug fix — fetchIssueFieldValuesByNodeID was missing the ghcontext.WithGraphQLFeatures("issue_fields", "repo_issue_fields") wrap, so without it the new fragment would have silently no-op'd on dotcom even with the right feature flags enabled.
Interesting! What was broken here? is it searching with fields?
There was a problem hiding this comment.
Not search/filtering — it's the read enrichment. fetchIssueFieldValuesByNodeID backs the field_values you get on get_issue and search_issues output. It was missing the WithGraphQLFeatures("issue_fields", "repo_issue_fields") wrap, so on dotcom the new IssueFieldValues fragment would just come back empty even with the flags on — field values silently wouldn't show up. Adding the wrap makes it actually resolve.
| // Clear any fields marked with delete:true via the GraphQL deleteIssueFieldValue | ||
| // mutation. The REST PATCH above can't do this reliably — Go's omitempty drops an | ||
| // empty issue_field_values array, leaving the old values intact. | ||
| if len(fieldsToDelete) > 0 { |
There was a problem hiding this comment.
i am confused, wasn't this code already in place?
what were we doing with the previous fieldIDsToDelete ?
There was a problem hiding this comment.
Good question — it looked in place but it didn't actually work. Previously resolveIssueRequestFieldValues returned []int64 fieldIDsToDelete and we leaned on the REST PATCH to clear them by sending an empty value — but Go's omitempty drops the empty issue_field_values array, so the clear silently no-op'd and the old values stuck around. This PR swaps that for an explicit deleteIssueFieldValue GraphQL mutation per field, so fieldDeletion now carries both the db id (to keep it out of the REST payload) and the node id (for the mutation). So delete: true actually clears now.
Multi-select issue fields ride on the existing custom-fields surface: - issue_write (consolidated) and set_issue_fields (granular) gain multi-select inputs (field_option_names / multi_select_option_ids) - list_issues field_filters gain a 'values' slot for multi-select filtering with AND semantics - list_issue_fields advertises multi_select in its description and surfaces multi-select definitions - Read paths (IssueFieldValueFragment, list_issues enrichment, etc.) decode multi-select values when an org has them The whole write surface is gated behind a new FF remote_mcp_issue_fields_multiselect. When the flag is off, the legacy variants of issue_write, list_issues, and list_issue_fields are served — same handler bodies, but their schemas and descriptions omit multi_select. Read paths stay unchanged: orgs that have dotcom-side multi-select enabled continue to see multi-select VALUES surfaced, matching what the dotcom UI shows. The granular tools (set_issue_fields) are not separately gated — they are already behind FeatureFlagIssuesGranular, which is itself a user-opt-in rollout flag. Double-gating adds complexity without proportionate benefit for users who have already accepted experimental territory. Per the user's preference for code duplication over interleaved branching: - IssueWrite and IssueWriteLegacy are full siblings (issues.go + issues_legacy_multiselect.go). The shared parser and resolver (optionalIssueWriteFields, resolveIssueRequestFieldValues) take a single multiSelectEnabled bool and reject multi-select inputs/fields when false — one contained branch point per helper, no FF checks threaded through the handler bodies. - ListIssues and ListIssuesLegacy share a buildListIssues helper that swaps in the right descriptions and adds/removes the field_filters[] values slot. - ListIssueFields and ListIssueFieldsLegacy share a buildListIssueFields helper that swaps the description (handler is identical). Snapshot naming follows the established convention: legacy variants own the canonical <name>.snap; MS-aware variants own <name>_ff_remote_mcp_issue_fields_multiselect.snap. Stacked on #2755 (the universal delete-fix half of the original PR). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
786a2d1 to
c726b68
Compare
Summary
Adds multi-select issue field support across the consolidated and granular issue tools, gated behind a new feature flag
remote_mcp_issue_fields_multiselect. When the flag is off, the existing single-select behaviour is preserved.What's gated behind the FF
field_option_namesslot onissue_fields[]items + multi_select resolverfield_filters[].valuesslot for multi-select option filtering (AND semantics)multi_selectWhat's not gated
FeatureFlagIssuesGranular, no double-gatingIssueFieldValueFragment,list_issuesenrichment, etc.) — orgs with dotcom-side multi-select enabled continue to see multi-select VALUES surfaced, matching the dotcom UIStructure
Per the user's preference, code duplication is preferred over interleaved FF branches:
IssueWrite/IssueWriteLegacyare full siblings (issues.go+issues_legacy_multiselect.go)ListIssues/ListIssuesLegacyshare abuildListIssueshelperListIssueFields/ListIssueFieldsLegacyshare abuildListIssueFieldshelperoptionalIssueWriteFields,resolveIssueRequestFieldValues,parseRawFieldFilters) take a singlemultiSelectEnabled bool— one contained branch point per helper, no FF checks threaded through handler bodiesSnapshot convention: legacy variants own the canonical
<name>.snap; MS-aware own<name>_ff_remote_mcp_issue_fields_multiselect.snap.Stacked on #2755
This PR is stacked on #2755 — Fix delete:true on issue fields by calling deleteIssueFieldValue mutation. Please review and merge that one first.
The original PR contained both the universal delete-bug fix and the multi-select feature; they were split for cleaner review. The base of this PR is set to the delete-fix branch so the diff shown reflects only the multi-select work.