Skip to content

Commit eb4c099

Browse files
zwickCopilotveralizeth
authored
Support singular Project Issue Field updates (#2941)
* Implement batch project write engine Resolve and validate shared field updates and item references before executing ordered, chunked GraphQL writes with explicit ambiguous outcomes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ae767ff-c1d0-46a9-b126-2e91403993a0 * Expose update_project_items Add the public projects_write contract, routing, handler coverage, and generated documentation for shared field updates across batches of up to 50 items. Co-authored-by: Lizeth Vera <47796851+veralizeth@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ae767ff-c1d0-46a9-b126-2e91403993a0 * Classify batch resolution failures Use a neutral code for non-structured lookup failures while preserving structured resolution details. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ae767ff-c1d0-46a9-b126-2e91403993a0 * Resolve issue references concurrently Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7ae767ff-c1d0-46a9-b126-2e91403993a0 Copilot-Session: 5709a470-df75-43ec-9a9c-98868e6065d2 * Add singular Issue Field project updates Support name-based attached Issue Field updates for singular Project items while preserving existing read and standard field behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c94f3ce-c04a-482f-830b-ab85abc3f6e4 * Preserve iteration project field updates Bypass Issue Field metadata resolution for standard field data types and recognize exact missing fragment-type schema errors. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4c94f3ce-c04a-482f-830b-ab85abc3f6e4 * Adding GraphQL-Features: update_issue_suggestions --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Lizeth Vera <47796851+veralizeth@users.noreply.github.com> Copilot-Session: 7ae767ff-c1d0-46a9-b126-2e91403993a0 Copilot-Session: 5709a470-df75-43ec-9a9c-98868e6065d2 Copilot-Session: 4c94f3ce-c04a-482f-830b-ab85abc3f6e4
1 parent 1b3f89a commit eb4c099

6 files changed

Lines changed: 798 additions & 217 deletions

File tree

pkg/github/granular_tools_test.go

Lines changed: 39 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,6 +1798,27 @@ func TestGranularUnresolveReviewThread(t *testing.T) {
17981798
}
17991799

18001800
func TestGranularSetIssueFields(t *testing.T) {
1801+
t.Run("mutation selects only issue identity", func(t *testing.T) {
1802+
transport := &sequencedGraphQLTransport{
1803+
t: t,
1804+
responses: []func(capturedGraphQLRequest) (int, string){
1805+
func(req capturedGraphQLRequest) (int, string) {
1806+
assert.Contains(t, req.Query, "issue{id,url}")
1807+
assert.NotContains(t, req.Query, "issueFieldValues")
1808+
assert.NotContains(t, req.Query, "number")
1809+
return http.StatusOK, `{"data":{"setIssueFieldValue":{"issue":{"id":"ISSUE_123","url":"https://github.com/owner/repo/issues/5"}}}}`
1810+
},
1811+
},
1812+
}
1813+
_, err := SetIssueFieldValues(context.Background(), githubv4.NewClient(&http.Client{Transport: transport}), SetIssueFieldValueInput{
1814+
IssueID: githubv4.ID("ISSUE_123"),
1815+
IssueFields: []IssueFieldCreateOrUpdateInput{{
1816+
FieldID: githubv4.ID("FIELD_1"), TextValue: githubv4.NewString("hello"),
1817+
}},
1818+
})
1819+
require.NoError(t, err)
1820+
})
1821+
18011822
t.Run("successful set with text value", func(t *testing.T) {
18021823
matchers := []githubv4mock.Matcher{
18031824
// Mock the issue ID query
@@ -1822,29 +1843,7 @@ func TestGranularSetIssueFields(t *testing.T) {
18221843
),
18231844
// Mock the setIssueFieldValue mutation
18241845
githubv4mock.NewMutationMatcher(
1825-
struct {
1826-
SetIssueFieldValue struct {
1827-
Issue struct {
1828-
ID githubv4.ID
1829-
Number githubv4.Int
1830-
URL githubv4.String
1831-
}
1832-
IssueFieldValues []struct {
1833-
TextValue struct {
1834-
Value string
1835-
} `graphql:"... on IssueFieldTextValue"`
1836-
SingleSelectValue struct {
1837-
Name string
1838-
} `graphql:"... on IssueFieldSingleSelectValue"`
1839-
DateValue struct {
1840-
Value string
1841-
} `graphql:"... on IssueFieldDateValue"`
1842-
NumberValue struct {
1843-
Value float64
1844-
} `graphql:"... on IssueFieldNumberValue"`
1845-
}
1846-
} `graphql:"setIssueFieldValue(input: $input)"`
1847-
}{},
1846+
setIssueFieldValueMutation{},
18481847
SetIssueFieldValueInput{
18491848
IssueID: githubv4.ID("ISSUE_123"),
18501849
IssueFields: []IssueFieldCreateOrUpdateInput{
@@ -1858,9 +1857,8 @@ func TestGranularSetIssueFields(t *testing.T) {
18581857
githubv4mock.DataResponse(map[string]any{
18591858
"setIssueFieldValue": map[string]any{
18601859
"issue": map[string]any{
1861-
"id": "ISSUE_123",
1862-
"number": 5,
1863-
"url": "https://github.com/owner/repo/issues/5",
1860+
"id": "ISSUE_123",
1861+
"url": "https://github.com/owner/repo/issues/5",
18641862
},
18651863
},
18661864
}),
@@ -1997,29 +1995,7 @@ func TestGranularSetIssueFields(t *testing.T) {
19971995
}),
19981996
),
19991997
githubv4mock.NewMutationMatcher(
2000-
struct {
2001-
SetIssueFieldValue struct {
2002-
Issue struct {
2003-
ID githubv4.ID
2004-
Number githubv4.Int
2005-
URL githubv4.String
2006-
}
2007-
IssueFieldValues []struct {
2008-
TextValue struct {
2009-
Value string
2010-
} `graphql:"... on IssueFieldTextValue"`
2011-
SingleSelectValue struct {
2012-
Name string
2013-
} `graphql:"... on IssueFieldSingleSelectValue"`
2014-
DateValue struct {
2015-
Value string
2016-
} `graphql:"... on IssueFieldDateValue"`
2017-
NumberValue struct {
2018-
Value float64
2019-
} `graphql:"... on IssueFieldNumberValue"`
2020-
}
2021-
} `graphql:"setIssueFieldValue(input: $input)"`
2022-
}{},
1998+
setIssueFieldValueMutation{},
20231999
SetIssueFieldValueInput{
20242000
IssueID: githubv4.ID("ISSUE_123"),
20252001
IssueFields: []IssueFieldCreateOrUpdateInput{
@@ -2034,9 +2010,8 @@ func TestGranularSetIssueFields(t *testing.T) {
20342010
githubv4mock.DataResponse(map[string]any{
20352011
"setIssueFieldValue": map[string]any{
20362012
"issue": map[string]any{
2037-
"id": "ISSUE_123",
2038-
"number": 5,
2039-
"url": "https://github.com/owner/repo/issues/5",
2013+
"id": "ISSUE_123",
2014+
"url": "https://github.com/owner/repo/issues/5",
20402015
},
20412016
},
20422017
}),
@@ -2111,29 +2086,7 @@ func TestGranularSetIssueFields(t *testing.T) {
21112086
}),
21122087
),
21132088
githubv4mock.NewMutationMatcher(
2114-
struct {
2115-
SetIssueFieldValue struct {
2116-
Issue struct {
2117-
ID githubv4.ID
2118-
Number githubv4.Int
2119-
URL githubv4.String
2120-
}
2121-
IssueFieldValues []struct {
2122-
TextValue struct {
2123-
Value string
2124-
} `graphql:"... on IssueFieldTextValue"`
2125-
SingleSelectValue struct {
2126-
Name string
2127-
} `graphql:"... on IssueFieldSingleSelectValue"`
2128-
DateValue struct {
2129-
Value string
2130-
} `graphql:"... on IssueFieldDateValue"`
2131-
NumberValue struct {
2132-
Value float64
2133-
} `graphql:"... on IssueFieldNumberValue"`
2134-
}
2135-
} `graphql:"setIssueFieldValue(input: $input)"`
2136-
}{},
2089+
setIssueFieldValueMutation{},
21372090
SetIssueFieldValueInput{
21382091
IssueID: githubv4.ID("ISSUE_123"),
21392092
IssueFields: []IssueFieldCreateOrUpdateInput{
@@ -2148,9 +2101,8 @@ func TestGranularSetIssueFields(t *testing.T) {
21482101
githubv4mock.DataResponse(map[string]any{
21492102
"setIssueFieldValue": map[string]any{
21502103
"issue": map[string]any{
2151-
"id": "ISSUE_123",
2152-
"number": 5,
2153-
"url": "https://github.com/owner/repo/issues/5",
2104+
"id": "ISSUE_123",
2105+
"url": "https://github.com/owner/repo/issues/5",
21542106
},
21552107
},
21562108
}),
@@ -2225,29 +2177,7 @@ func TestGranularSetIssueFields(t *testing.T) {
22252177
}),
22262178
),
22272179
githubv4mock.NewMutationMatcher(
2228-
struct {
2229-
SetIssueFieldValue struct {
2230-
Issue struct {
2231-
ID githubv4.ID
2232-
Number githubv4.Int
2233-
URL githubv4.String
2234-
}
2235-
IssueFieldValues []struct {
2236-
TextValue struct {
2237-
Value string
2238-
} `graphql:"... on IssueFieldTextValue"`
2239-
SingleSelectValue struct {
2240-
Name string
2241-
} `graphql:"... on IssueFieldSingleSelectValue"`
2242-
DateValue struct {
2243-
Value string
2244-
} `graphql:"... on IssueFieldDateValue"`
2245-
NumberValue struct {
2246-
Value float64
2247-
} `graphql:"... on IssueFieldNumberValue"`
2248-
}
2249-
} `graphql:"setIssueFieldValue(input: $input)"`
2250-
}{},
2180+
setIssueFieldValueMutation{},
22512181
SetIssueFieldValueInput{
22522182
IssueID: githubv4.ID("ISSUE_123"),
22532183
IssueFields: []IssueFieldCreateOrUpdateInput{
@@ -2262,9 +2192,8 @@ func TestGranularSetIssueFields(t *testing.T) {
22622192
githubv4mock.DataResponse(map[string]any{
22632193
"setIssueFieldValue": map[string]any{
22642194
"issue": map[string]any{
2265-
"id": "ISSUE_123",
2266-
"number": 5,
2267-
"url": "https://github.com/owner/repo/issues/5",
2195+
"id": "ISSUE_123",
2196+
"url": "https://github.com/owner/repo/issues/5",
22682197
},
22692198
},
22702199
}),
@@ -2316,29 +2245,7 @@ func TestGranularSetIssueFields(t *testing.T) {
23162245
}),
23172246
),
23182247
githubv4mock.NewMutationMatcher(
2319-
struct {
2320-
SetIssueFieldValue struct {
2321-
Issue struct {
2322-
ID githubv4.ID
2323-
Number githubv4.Int
2324-
URL githubv4.String
2325-
}
2326-
IssueFieldValues []struct {
2327-
TextValue struct {
2328-
Value string
2329-
} `graphql:"... on IssueFieldTextValue"`
2330-
SingleSelectValue struct {
2331-
Name string
2332-
} `graphql:"... on IssueFieldSingleSelectValue"`
2333-
DateValue struct {
2334-
Value string
2335-
} `graphql:"... on IssueFieldDateValue"`
2336-
NumberValue struct {
2337-
Value float64
2338-
} `graphql:"... on IssueFieldNumberValue"`
2339-
}
2340-
} `graphql:"setIssueFieldValue(input: $input)"`
2341-
}{},
2248+
setIssueFieldValueMutation{},
23422249
SetIssueFieldValueInput{
23432250
IssueID: githubv4.ID("ISSUE_123"),
23442251
IssueFields: []IssueFieldCreateOrUpdateInput{
@@ -2354,9 +2261,8 @@ func TestGranularSetIssueFields(t *testing.T) {
23542261
githubv4mock.DataResponse(map[string]any{
23552262
"setIssueFieldValue": map[string]any{
23562263
"issue": map[string]any{
2357-
"id": "ISSUE_123",
2358-
"number": 5,
2359-
"url": "https://github.com/owner/repo/issues/5",
2264+
"id": "ISSUE_123",
2265+
"url": "https://github.com/owner/repo/issues/5",
23602266
},
23612267
},
23622268
}),
@@ -2408,29 +2314,7 @@ func TestGranularSetIssueFields(t *testing.T) {
24082314
}),
24092315
),
24102316
githubv4mock.NewMutationMatcher(
2411-
struct {
2412-
SetIssueFieldValue struct {
2413-
Issue struct {
2414-
ID githubv4.ID
2415-
Number githubv4.Int
2416-
URL githubv4.String
2417-
}
2418-
IssueFieldValues []struct {
2419-
TextValue struct {
2420-
Value string
2421-
} `graphql:"... on IssueFieldTextValue"`
2422-
SingleSelectValue struct {
2423-
Name string
2424-
} `graphql:"... on IssueFieldSingleSelectValue"`
2425-
DateValue struct {
2426-
Value string
2427-
} `graphql:"... on IssueFieldDateValue"`
2428-
NumberValue struct {
2429-
Value float64
2430-
} `graphql:"... on IssueFieldNumberValue"`
2431-
}
2432-
} `graphql:"setIssueFieldValue(input: $input)"`
2433-
}{},
2317+
setIssueFieldValueMutation{},
24342318
SetIssueFieldValueInput{
24352319
IssueID: githubv4.ID("ISSUE_123"),
24362320
IssueFields: []IssueFieldCreateOrUpdateInput{
@@ -2444,9 +2328,8 @@ func TestGranularSetIssueFields(t *testing.T) {
24442328
githubv4mock.DataResponse(map[string]any{
24452329
"setIssueFieldValue": map[string]any{
24462330
"issue": map[string]any{
2447-
"id": "ISSUE_123",
2448-
"number": 5,
2449-
"url": "https://github.com/owner/repo/issues/5",
2331+
"id": "ISSUE_123",
2332+
"url": "https://github.com/owner/repo/issues/5",
24502333
},
24512334
},
24522335
}),

pkg/github/issues_granular.go

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1274,6 +1274,27 @@ type IssueFieldCreateOrUpdateInput struct {
12741274
Suggest *githubv4.Boolean `json:"suggest,omitempty"`
12751275
}
12761276

1277+
type setIssueFieldValueMutation struct {
1278+
SetIssueFieldValue struct {
1279+
Issue struct {
1280+
ID githubv4.ID
1281+
URL githubv4.String
1282+
}
1283+
} `graphql:"setIssueFieldValue(input: $input)"`
1284+
}
1285+
1286+
// SetIssueFieldValues updates Issue Field values and returns the updated issue.
1287+
func SetIssueFieldValues(ctx context.Context, gqlClient *githubv4.Client, input SetIssueFieldValueInput) (MinimalResponse, error) {
1288+
var mutation setIssueFieldValueMutation
1289+
if err := gqlClient.Mutate(ctx, &mutation, input, nil); err != nil {
1290+
return MinimalResponse{}, err
1291+
}
1292+
return MinimalResponse{
1293+
ID: fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID),
1294+
URL: string(mutation.SetIssueFieldValue.Issue.URL),
1295+
}, nil
1296+
}
1297+
12771298
// GranularSetIssueFields creates a tool to set issue field values on an issue using GraphQL.
12781299
func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.ServerTool {
12791300
st := NewTool(
@@ -1497,31 +1518,6 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
14971518
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to get issue", err), nil, nil
14981519
}
14991520

1500-
// Execute the setIssueFieldValue mutation
1501-
var mutation struct {
1502-
SetIssueFieldValue struct {
1503-
Issue struct {
1504-
ID githubv4.ID
1505-
Number githubv4.Int
1506-
URL githubv4.String
1507-
}
1508-
IssueFieldValues []struct {
1509-
TextValue struct {
1510-
Value string
1511-
} `graphql:"... on IssueFieldTextValue"`
1512-
SingleSelectValue struct {
1513-
Name string
1514-
} `graphql:"... on IssueFieldSingleSelectValue"`
1515-
DateValue struct {
1516-
Value string
1517-
} `graphql:"... on IssueFieldDateValue"`
1518-
NumberValue struct {
1519-
Value float64
1520-
} `graphql:"... on IssueFieldNumberValue"`
1521-
}
1522-
} `graphql:"setIssueFieldValue(input: $input)"`
1523-
}
1524-
15251521
mutationInput := SetIssueFieldValueInput{
15261522
IssueID: issueID,
15271523
IssueFields: issueFields,
@@ -1530,14 +1526,12 @@ func GranularSetIssueFields(t translations.TranslationHelperFunc) inventory.Serv
15301526
// The rationale and suggest input fields on IssueFieldCreateOrUpdateInput
15311527
// are gated behind the update_issue_suggestions GraphQL feature flag.
15321528
ctxWithFeatures := ghcontext.WithGraphQLFeatures(ctx, "update_issue_suggestions")
1533-
if err := gqlClient.Mutate(ctxWithFeatures, &mutation, mutationInput, nil); err != nil {
1529+
response, err := SetIssueFieldValues(ctxWithFeatures, gqlClient, mutationInput)
1530+
if err != nil {
15341531
return ghErrors.NewGitHubGraphQLErrorResponse(ctx, "failed to set issue field values", err), nil, nil
15351532
}
15361533

1537-
r, err := json.Marshal(MinimalResponse{
1538-
ID: fmt.Sprintf("%v", mutation.SetIssueFieldValue.Issue.ID),
1539-
URL: string(mutation.SetIssueFieldValue.Issue.URL),
1540-
})
1534+
r, err := json.Marshal(response)
15411535
if err != nil {
15421536
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
15431537
}

0 commit comments

Comments
 (0)