Skip to content

Commit 18656ac

Browse files
authored
Merge branch 'main' into iunia/issues-granular-update-state
2 parents bcee6eb + 49abf15 commit 18656ac

10 files changed

Lines changed: 412 additions & 41 deletions

.github/workflows/goreleaser.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
run: go mod download
2929

3030
- name: Run GoReleaser
31-
uses: goreleaser/goreleaser-action@5daf1e915a5f0af01ddbcd89a43b8061ff4f1a89
31+
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94
3232
with:
3333
distribution: goreleaser
3434
# GoReleaser version

docs/feature-flags.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ runtime behavior (such as output formatting) won't appear here.
150150

151151
- **update_issue_assignees** - Update Issue Assignees
152152
- **Required OAuth Scopes**: `repo`
153-
- `assignees`: GitHub usernames to assign to this issue (string[], required)
153+
- `assignees`: GitHub usernames to assign to this issue. ([], required)
154154
- `issue_number`: The issue number to update (number, required)
155155
- `owner`: Repository owner (username or organization) (string, required)
156156
- `repo`: Repository name (string, required)

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,10 @@ require (
4040
github.com/stretchr/objx v0.5.2 // indirect
4141
github.com/subosito/gotenv v1.6.0 // indirect
4242
go.yaml.in/yaml/v3 v3.0.4 // indirect
43-
golang.org/x/net v0.38.0 // indirect
43+
golang.org/x/net v0.55.0 // indirect
4444
golang.org/x/sync v0.20.0 // indirect
45-
golang.org/x/sys v0.41.0 // indirect
46-
golang.org/x/text v0.28.0 // indirect
45+
golang.org/x/sys v0.45.0 // indirect
46+
golang.org/x/text v0.37.0 // indirect
4747
golang.org/x/time v0.15.0 // indirect
4848
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
4949
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
9292
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
9393
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
9494
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
95-
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
96-
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
95+
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
96+
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
9797
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
9898
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
9999
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -107,8 +107,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
107107
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
108108
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
109109
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
110-
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
111-
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
110+
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
111+
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
112112
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
113113
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
114114
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -117,16 +117,16 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
117117
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
118118
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
119119
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
120-
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
121-
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
120+
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
121+
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
122122
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
123123
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
124124
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
125125
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
126126
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
127127
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
128-
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
129-
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
128+
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
129+
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
130130
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
131131
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
132132
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

pkg/github/__toolsnaps__/update_issue_assignees.snap

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,48 @@
66
"readOnlyHint": false,
77
"title": "Update Issue Assignees"
88
},
9-
"description": "Update the assignees of an existing issue. This replaces the current assignees with the provided list.",
9+
"description": "Update the assignees of an existing issue. This replaces the current assignees with the provided list. When setting values, include a confidence level (LOW, MEDIUM, or HIGH) reflecting how certain you are about the choice.",
1010
"inputSchema": {
1111
"properties": {
1212
"assignees": {
13-
"description": "GitHub usernames to assign to this issue",
13+
"description": "GitHub usernames to assign to this issue.",
1414
"items": {
15-
"type": "string"
15+
"oneOf": [
16+
{
17+
"description": "GitHub username",
18+
"type": "string"
19+
},
20+
{
21+
"properties": {
22+
"confidence": {
23+
"description": "How confident you are in this choice. Use 'HIGH' for clear signal or explicit user request, 'MEDIUM' for reasonable inference with some ambiguity, 'LOW' for best guess with limited signal.",
24+
"enum": [
25+
"LOW",
26+
"MEDIUM",
27+
"HIGH"
28+
],
29+
"type": "string"
30+
},
31+
"is_suggestion": {
32+
"description": "If true, this assignee is sent to the API as a suggestion (suggest:true) rather than an applied assignee. Whether the assignee is applied or recorded as a proposal is determined by the API.",
33+
"type": "boolean"
34+
},
35+
"login": {
36+
"description": "GitHub username",
37+
"type": "string"
38+
},
39+
"rationale": {
40+
"description": "One concise sentence explaining what specifically about the issue led you to choose this assignee. State the concrete signal (e.g. 'Authored the file the crash originates in').",
41+
"maxLength": 280,
42+
"type": "string"
43+
}
44+
},
45+
"required": [
46+
"login"
47+
],
48+
"type": "object"
49+
}
50+
]
1651
},
1752
"type": "array"
1853
},

pkg/github/granular_tools_test.go

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,167 @@ func TestGranularUpdateIssueAssignees(t *testing.T) {
271271
assert.False(t, result.IsError)
272272
}
273273

274+
func TestGranularUpdateIssueAssigneesObjectForm(t *testing.T) {
275+
tests := []struct {
276+
name string
277+
requestArgs map[string]any
278+
expectedReq map[string]any
279+
}{
280+
{
281+
name: "assignee objects without intent serialize as strings",
282+
requestArgs: map[string]any{
283+
"owner": "owner",
284+
"repo": "repo",
285+
"issue_number": float64(1),
286+
"assignees": []any{
287+
map[string]any{"login": "octocat"},
288+
"monalisa",
289+
},
290+
},
291+
expectedReq: map[string]any{
292+
"assignees": []any{"octocat", "monalisa"},
293+
},
294+
},
295+
{
296+
name: "assignee suggested without rationale",
297+
requestArgs: map[string]any{
298+
"owner": "owner",
299+
"repo": "repo",
300+
"issue_number": float64(1),
301+
"assignees": []any{
302+
map[string]any{"login": "octocat", "is_suggestion": true},
303+
},
304+
},
305+
expectedReq: map[string]any{
306+
"assignees": []any{
307+
map[string]any{"login": "octocat", "suggest": true},
308+
},
309+
},
310+
},
311+
{
312+
name: "suggested assignee with rationale and confidence",
313+
requestArgs: map[string]any{
314+
"owner": "owner",
315+
"repo": "repo",
316+
"issue_number": float64(1),
317+
"assignees": []any{
318+
map[string]any{"login": "octocat", "rationale": " Authored the crashing file ", "confidence": "high", "is_suggestion": true},
319+
},
320+
},
321+
expectedReq: map[string]any{
322+
"assignees": []any{
323+
map[string]any{"login": "octocat", "rationale": "Authored the crashing file", "confidence": "HIGH", "suggest": true},
324+
},
325+
},
326+
},
327+
{
328+
name: "mix of plain and suggested assignees",
329+
requestArgs: map[string]any{
330+
"owner": "owner",
331+
"repo": "repo",
332+
"issue_number": float64(1),
333+
"assignees": []any{
334+
"monalisa",
335+
map[string]any{"login": "octocat", "is_suggestion": true},
336+
},
337+
},
338+
expectedReq: map[string]any{
339+
"assignees": []any{
340+
"monalisa",
341+
map[string]any{"login": "octocat", "suggest": true},
342+
},
343+
},
344+
},
345+
}
346+
347+
for _, tc := range tests {
348+
t.Run(tc.name, func(t *testing.T) {
349+
client := mustNewGHClient(t, MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
350+
PatchReposIssuesByOwnerByRepoByIssueNumber: expectRequestBody(t, tc.expectedReq).
351+
andThen(mockResponse(t, http.StatusOK, &gogithub.Issue{Number: gogithub.Ptr(1)})),
352+
}))
353+
deps := BaseDeps{Client: client}
354+
serverTool := GranularUpdateIssueAssignees(translations.NullTranslationHelper)
355+
handler := serverTool.Handler(deps)
356+
357+
request := createMCPRequest(tc.requestArgs)
358+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
359+
require.NoError(t, err)
360+
assert.False(t, result.IsError)
361+
})
362+
}
363+
}
364+
365+
func TestGranularUpdateIssueAssigneesInvalidInput(t *testing.T) {
366+
tests := []struct {
367+
name string
368+
requestArgs map[string]any
369+
expectedErrText string
370+
}{
371+
{
372+
name: "rationale too long",
373+
requestArgs: map[string]any{
374+
"owner": "owner",
375+
"repo": "repo",
376+
"issue_number": float64(1),
377+
"assignees": []any{
378+
map[string]any{"login": "octocat", "rationale": strings.Repeat("a", 281)},
379+
},
380+
},
381+
expectedErrText: "assignee rationale must be 280 characters or less",
382+
},
383+
{
384+
name: "assignee object missing login",
385+
requestArgs: map[string]any{
386+
"owner": "owner",
387+
"repo": "repo",
388+
"issue_number": float64(1),
389+
"assignees": []any{
390+
map[string]any{"rationale": "no login provided"},
391+
},
392+
},
393+
expectedErrText: "each assignee object must have a 'login' string",
394+
},
395+
{
396+
name: "invalid confidence value",
397+
requestArgs: map[string]any{
398+
"owner": "owner",
399+
"repo": "repo",
400+
"issue_number": float64(1),
401+
"assignees": []any{
402+
map[string]any{"login": "octocat", "confidence": "maybe"},
403+
},
404+
},
405+
expectedErrText: "confidence must be one of: LOW, MEDIUM, HIGH",
406+
},
407+
{
408+
name: "assignee entry is neither string nor object",
409+
requestArgs: map[string]any{
410+
"owner": "owner",
411+
"repo": "repo",
412+
"issue_number": float64(1),
413+
"assignees": []any{float64(123)},
414+
},
415+
expectedErrText: "each assignee must be a string or an object",
416+
},
417+
}
418+
419+
for _, tc := range tests {
420+
t.Run(tc.name, func(t *testing.T) {
421+
deps := BaseDeps{Client: mustNewGHClient(t, MockHTTPClientWithHandlers(nil))}
422+
serverTool := GranularUpdateIssueAssignees(translations.NullTranslationHelper)
423+
handler := serverTool.Handler(deps)
424+
425+
request := createMCPRequest(tc.requestArgs)
426+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
427+
require.NoError(t, err)
428+
429+
errorContent := getErrorResult(t, result)
430+
assert.Contains(t, errorContent.Text, tc.expectedErrText)
431+
})
432+
}
433+
}
434+
274435
func TestGranularUpdateIssueLabels(t *testing.T) {
275436
tests := []struct {
276437
name string

0 commit comments

Comments
 (0)