Skip to content

Commit 577a645

Browse files
zwickCopilot
andcommitted
Accept Issue Field option database IDs
Attached Issue Field options are surfaced to callers through the REST field metadata, which reports the numeric database ID (2068) rather than the GraphQL node ID (IFSSO_kgDNCBQ). An agent that discovered options via list_project_fields and wrote back the ID it was shown got option_not_found, because only the node ID and the option name were accepted. IssueFieldSingleSelectOption exposes databaseId alongside id, so select it and match either form, resolving to the node ID the mutation requires. Standard project fields are unaffected: their REST and GraphQL option IDs are already identical, and they carry no database ID, so an unmatched numeric value still surfaces option_not_found. Verified against the live API: an option ID taken from list_project_fields output now writes the matching option. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b95d817-7807-428a-95ce-8d8fe42b5d5b
1 parent 04e58f8 commit 577a645

2 files changed

Lines changed: 61 additions & 9 deletions

File tree

pkg/github/projects_resolver.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ const resolverFieldsPageSize = 100
1717

1818
// ResolvedFieldOption is one option on a SINGLE_SELECT project field.
1919
type ResolvedFieldOption struct {
20-
ID string
21-
Name string
20+
ID string
21+
// DatabaseID is the numeric option ID. Attached Issue Fields surface it
22+
// through the REST field metadata, so writes must accept it alongside ID.
23+
DatabaseID string
24+
Name string
2225
}
2326

2427
// ResolvedField contains a project's numeric database ID, GraphQL node ID, and
@@ -46,8 +49,9 @@ type projectIssueFieldMetadata struct {
4649
SingleSelect struct {
4750
ID githubv4.ID
4851
Options []struct {
49-
ID githubv4.ID
50-
Name githubv4.String
52+
ID githubv4.ID
53+
DatabaseID githubv4.Int `graphql:"databaseId"`
54+
Name githubv4.String
5155
}
5256
} `graphql:"... on IssueFieldSingleSelect"`
5357
}
@@ -269,7 +273,14 @@ func listAllProjectFieldsWithIssueFieldMetadata(ctx context.Context, gqlClient *
269273
field.IssueFieldNodeID = issueFieldNodeIDForType(field.DataType, node.ProjectV2SingleSelectField.IssueField)
270274
field.Options = field.Options[:0]
271275
for _, option := range node.ProjectV2SingleSelectField.IssueField.SingleSelect.Options {
272-
field.Options = append(field.Options, ResolvedFieldOption{ID: fmt.Sprintf("%v", option.ID), Name: string(option.Name)})
276+
resolvedOption := ResolvedFieldOption{
277+
ID: fmt.Sprintf("%v", option.ID),
278+
Name: string(option.Name),
279+
}
280+
if option.DatabaseID != 0 {
281+
resolvedOption.DatabaseID = fmt.Sprintf("%d", option.DatabaseID)
282+
}
283+
field.Options = append(field.Options, resolvedOption)
273284
}
274285
} else if bool(node.ProjectV2Field.IsIssueField) {
275286
field.IsIssueField = true
@@ -470,10 +481,12 @@ func resolveSingleSelectOptionByName(field *ResolvedField, optionName string) (s
470481
}
471482
}
472483

484+
// resolveSingleSelectOptionByNameOrID accepts an option's node ID, its numeric
485+
// database ID as surfaced by the REST field metadata, or its name.
473486
func resolveSingleSelectOptionByNameOrID(field *ResolvedField, value string) (string, error) {
474487
for _, option := range field.Options {
475-
if option.ID == value {
476-
return value, nil
488+
if option.ID == value || (option.DatabaseID != "" && option.DatabaseID == value) {
489+
return option.ID, nil
477490
}
478491
}
479492
return resolveSingleSelectOptionByName(field, value)

pkg/github/projects_resolver_test.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ func Test_ResolveProjectFieldByID_AttachedIssueFieldMetadata(t *testing.T) {
103103
fieldsQueryVars("octo-org", 7),
104104
githubv4mock.DataResponse(fieldsResponse([]map[string]any{
105105
attachedIssueFieldNode("PVTSSF_risk", 702, "IF_risk", "Risk", "SINGLE_SELECT", []map[string]any{
106-
{"id": "IFO_low", "name": "Low"}, {"id": "IFO_high", "name": "High"},
106+
{"id": "IFO_low", "databaseId": 2068, "name": "Low"},
107+
{"id": "IFO_high", "databaseId": 2069, "name": "High"},
107108
}),
108109
})),
109110
),
@@ -122,10 +123,48 @@ func Test_ResolveProjectFieldByID_AttachedIssueFieldMetadata(t *testing.T) {
122123
assert.Equal(t, "IF_risk", field.IssueFieldNodeID)
123124
assert.True(t, field.IsIssueField)
124125
assert.Equal(t, []ResolvedFieldOption{
125-
{ID: "IFO_low", Name: "Low"}, {ID: "IFO_high", Name: "High"},
126+
{ID: "IFO_low", DatabaseID: "2068", Name: "Low"},
127+
{ID: "IFO_high", DatabaseID: "2069", Name: "High"},
126128
}, field.Options)
127129
}
128130

131+
// Attached Issue Field options surface their numeric database ID through the
132+
// REST field metadata, so a write must accept that form as well as the node ID.
133+
func Test_ResolveSingleSelectOptionByNameOrID_AcceptsIssueFieldOptionForms(t *testing.T) {
134+
field := &ResolvedField{
135+
Name: "Risk",
136+
DataType: "SINGLE_SELECT",
137+
IsIssueField: true,
138+
Options: []ResolvedFieldOption{
139+
{ID: "IFSSO_low", DatabaseID: "2068", Name: "Low"},
140+
{ID: "IFSSO_high", DatabaseID: "2069", Name: "High"},
141+
},
142+
}
143+
144+
for _, tt := range []struct{ name, value, want string }{
145+
{"node ID", "IFSSO_high", "IFSSO_high"},
146+
{"database ID from REST metadata", "2069", "IFSSO_high"},
147+
{"option name", "high", "IFSSO_high"},
148+
} {
149+
t.Run(tt.name, func(t *testing.T) {
150+
got, err := resolveSingleSelectOptionByNameOrID(field, tt.value)
151+
require.NoError(t, err)
152+
assert.Equal(t, tt.want, got)
153+
})
154+
}
155+
156+
// Standard project fields carry no database ID, so an unmatched numeric
157+
// value must still surface as a normal option-not-found error.
158+
standard := &ResolvedField{
159+
Name: "Status",
160+
DataType: "SINGLE_SELECT",
161+
Options: []ResolvedFieldOption{{ID: "f75ad846", Name: "Todo"}},
162+
}
163+
_, err := resolveSingleSelectOptionByNameOrID(standard, "2069")
164+
require.Error(t, err)
165+
assert.Contains(t, err.Error(), `"error":"option_not_found"`)
166+
}
167+
129168
func Test_ResolveProjectFieldForUpdateByName_Success(t *testing.T) {
130169
mocked := githubv4mock.NewMockedHTTPClient(
131170
githubv4mock.NewQueryMatcher(

0 commit comments

Comments
 (0)