Skip to content

Commit 91b35e0

Browse files
Get check runs (#1953)
* Add support for get_check_runs * Run generate-docs * Address AI code review comment * make descriptions less ambiguous for model * lint and docs * fix lint --------- Co-authored-by: tommaso-moro <tommaso-moro@github.com> Co-authored-by: Tommaso Moro <37270480+tommaso-moro@users.noreply.github.com>
1 parent a32a757 commit 91b35e0

File tree

6 files changed

+271
-5
lines changed

6 files changed

+271
-5
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1097,11 +1097,12 @@ The following sets of tools are available:
10971097
Possible options:
10981098
1. get - Get details of a specific pull request.
10991099
2. get_diff - Get the diff of a pull request.
1100-
3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
1100+
3. get_status - Get combined commit status of a head commit in a pull request.
11011101
4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.
11021102
5. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.
11031103
6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.
11041104
7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.
1105+
8. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.
11051106
(string, required)
11061107
- `owner`: Repository owner (string, required)
11071108
- `page`: Page number for pagination (min 1) (number, optional)

pkg/github/__toolsnaps__/pull_request_read.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,16 @@
77
"inputSchema": {
88
"properties": {
99
"method": {
10-
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n",
10+
"description": "Action to specify what pull request data needs to be retrieved from GitHub. \nPossible options: \n 1. get - Get details of a specific pull request.\n 2. get_diff - Get the diff of a pull request.\n 3. get_status - Get combined commit status of a head commit in a pull request.\n 4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.\n 5. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.\n 6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.\n 7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.\n 8. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.\n",
1111
"enum": [
1212
"get",
1313
"get_diff",
1414
"get_status",
1515
"get_files",
1616
"get_review_comments",
1717
"get_reviews",
18-
"get_comments"
18+
"get_comments",
19+
"get_check_runs"
1920
],
2021
"type": "string"
2122
},

pkg/github/helper_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const (
5151
PostReposGitTreesByOwnerByRepo = "POST /repos/{owner}/{repo}/git/trees"
5252
GetReposCommitsStatusByOwnerByRepoByRef = "GET /repos/{owner}/{repo}/commits/{ref}/status"
5353
GetReposCommitsStatusesByOwnerByRepoByRef = "GET /repos/{owner}/{repo}/commits/{ref}/statuses"
54+
GetReposCommitsCheckRunsByOwnerByRepoByRef = "GET /repos/{owner}/{repo}/commits/{ref}/check-runs"
5455

5556
// Issues endpoints
5657
GetReposIssuesByOwnerByRepoByIssueNumber = "GET /repos/{owner}/{repo}/issues/{issue_number}"

pkg/github/minimal_types.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,45 @@ func convertToMinimalBranch(branch *github.Branch) MinimalBranch {
702702
}
703703
}
704704

705+
// MinimalCheckRun is the trimmed output type for check run objects.
706+
type MinimalCheckRun struct {
707+
ID int64 `json:"id"`
708+
Name string `json:"name"`
709+
Status string `json:"status"`
710+
Conclusion string `json:"conclusion,omitempty"`
711+
HTMLURL string `json:"html_url,omitempty"`
712+
DetailsURL string `json:"details_url,omitempty"`
713+
StartedAt string `json:"started_at,omitempty"`
714+
CompletedAt string `json:"completed_at,omitempty"`
715+
}
716+
717+
// MinimalCheckRunsResult is the trimmed output type for check runs list results.
718+
type MinimalCheckRunsResult struct {
719+
TotalCount int `json:"total_count"`
720+
CheckRuns []MinimalCheckRun `json:"check_runs"`
721+
}
722+
723+
// convertToMinimalCheckRun converts a GitHub API CheckRun to MinimalCheckRun
724+
func convertToMinimalCheckRun(checkRun *github.CheckRun) MinimalCheckRun {
725+
minimalCheckRun := MinimalCheckRun{
726+
ID: checkRun.GetID(),
727+
Name: checkRun.GetName(),
728+
Status: checkRun.GetStatus(),
729+
Conclusion: checkRun.GetConclusion(),
730+
HTMLURL: checkRun.GetHTMLURL(),
731+
DetailsURL: checkRun.GetDetailsURL(),
732+
}
733+
734+
if checkRun.StartedAt != nil {
735+
minimalCheckRun.StartedAt = checkRun.StartedAt.Format("2006-01-02T15:04:05Z")
736+
}
737+
if checkRun.CompletedAt != nil {
738+
minimalCheckRun.CompletedAt = checkRun.CompletedAt.Format("2006-01-02T15:04:05Z")
739+
}
740+
741+
return minimalCheckRun
742+
}
743+
705744
func convertToMinimalReviewThreadsResponse(query reviewThreadsQuery) MinimalReviewThreadsResponse {
706745
threads := query.Repository.PullRequest.ReviewThreads
707746

pkg/github/pullrequests.go

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,14 @@ func PullRequestRead(t translations.TranslationHelperFunc) inventory.ServerTool
3333
Possible options:
3434
1. get - Get details of a specific pull request.
3535
2. get_diff - Get the diff of a pull request.
36-
3. get_status - Get status of a head commit in a pull request. This reflects status of builds and checks.
36+
3. get_status - Get combined commit status of a head commit in a pull request.
3737
4. get_files - Get the list of files changed in a pull request. Use with pagination parameters to control the number of results returned.
3838
5. get_review_comments - Get review threads on a pull request. Each thread contains logically grouped review comments made on the same code location during pull request reviews. Returns threads with metadata (isResolved, isOutdated, isCollapsed) and their associated comments. Use cursor-based pagination (perPage, after) to control results.
3939
6. get_reviews - Get the reviews on a pull request. When asked for review comments, use get_review_comments method.
4040
7. get_comments - Get comments on a pull request. Use this if user doesn't specifically want review comments. Use with pagination parameters to control the number of results returned.
41+
8. get_check_runs - Get check runs for the head commit of a pull request. Check runs are the individual CI/CD jobs and checks that run on the PR.
4142
`,
42-
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments"},
43+
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"},
4344
},
4445
"owner": {
4546
Type: "string",
@@ -128,6 +129,9 @@ Possible options:
128129
case "get_comments":
129130
result, err := GetIssueComments(ctx, client, deps, owner, repo, pullNumber, pagination)
130131
return result, nil, err
132+
case "get_check_runs":
133+
result, err := GetPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination)
134+
return result, nil, err
131135
default:
132136
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
133137
}
@@ -267,6 +271,71 @@ func GetPullRequestStatus(ctx context.Context, client *github.Client, owner, rep
267271
return utils.NewToolResultText(string(r)), nil
268272
}
269273

274+
func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
275+
// First get the PR to get the head SHA
276+
pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber)
277+
if err != nil {
278+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
279+
"failed to get pull request",
280+
resp,
281+
err,
282+
), nil
283+
}
284+
defer resp.Body.Close()
285+
286+
if resp.StatusCode != http.StatusOK {
287+
body, err := io.ReadAll(resp.Body)
288+
if err != nil {
289+
return nil, fmt.Errorf("failed to read response body: %w", err)
290+
}
291+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil
292+
}
293+
294+
// Get check runs for the head SHA
295+
opts := &github.ListCheckRunsOptions{
296+
ListOptions: github.ListOptions{
297+
PerPage: pagination.PerPage,
298+
Page: pagination.Page,
299+
},
300+
}
301+
302+
checkRuns, resp, err := client.Checks.ListCheckRunsForRef(ctx, owner, repo, *pr.Head.SHA, opts)
303+
if err != nil {
304+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
305+
"failed to get check runs",
306+
resp,
307+
err,
308+
), nil
309+
}
310+
defer resp.Body.Close()
311+
312+
if resp.StatusCode != http.StatusOK {
313+
body, err := io.ReadAll(resp.Body)
314+
if err != nil {
315+
return nil, fmt.Errorf("failed to read response body: %w", err)
316+
}
317+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get check runs", resp, body), nil
318+
}
319+
320+
// Convert to minimal check runs to reduce context usage
321+
minimalCheckRuns := make([]MinimalCheckRun, 0, len(checkRuns.CheckRuns))
322+
for _, checkRun := range checkRuns.CheckRuns {
323+
minimalCheckRuns = append(minimalCheckRuns, convertToMinimalCheckRun(checkRun))
324+
}
325+
326+
minimalResult := MinimalCheckRunsResult{
327+
TotalCount: checkRuns.GetTotal(),
328+
CheckRuns: minimalCheckRuns,
329+
}
330+
331+
r, err := json.Marshal(minimalResult)
332+
if err != nil {
333+
return nil, fmt.Errorf("failed to marshal response: %w", err)
334+
}
335+
336+
return utils.NewToolResultText(string(r)), nil
337+
}
338+
270339
func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
271340
opts := &github.ListOptions{
272341
PerPage: pagination.PerPage,

pkg/github/pullrequests_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,6 +1404,161 @@ func Test_GetPullRequestStatus(t *testing.T) {
14041404
}
14051405
}
14061406

1407+
func Test_GetPullRequestCheckRuns(t *testing.T) {
1408+
// Verify tool definition once
1409+
serverTool := PullRequestRead(translations.NullTranslationHelper)
1410+
tool := serverTool.Tool
1411+
require.NoError(t, toolsnaps.Test(tool.Name, tool))
1412+
1413+
assert.Equal(t, "pull_request_read", tool.Name)
1414+
assert.NotEmpty(t, tool.Description)
1415+
schema := tool.InputSchema.(*jsonschema.Schema)
1416+
assert.Contains(t, schema.Properties, "method")
1417+
assert.Contains(t, schema.Properties, "owner")
1418+
assert.Contains(t, schema.Properties, "repo")
1419+
assert.Contains(t, schema.Properties, "pullNumber")
1420+
assert.ElementsMatch(t, schema.Required, []string{"method", "owner", "repo", "pullNumber"})
1421+
1422+
// Setup mock PR for successful PR fetch
1423+
mockPR := &github.PullRequest{
1424+
Number: github.Ptr(42),
1425+
Title: github.Ptr("Test PR"),
1426+
HTMLURL: github.Ptr("https://github.com/owner/repo/pull/42"),
1427+
Head: &github.PullRequestBranch{
1428+
SHA: github.Ptr("abcd1234"),
1429+
Ref: github.Ptr("feature-branch"),
1430+
},
1431+
}
1432+
1433+
// Setup mock check runs for success case
1434+
mockCheckRuns := &github.ListCheckRunsResults{
1435+
Total: github.Ptr(2),
1436+
CheckRuns: []*github.CheckRun{
1437+
{
1438+
ID: github.Ptr(int64(1)),
1439+
Name: github.Ptr("build"),
1440+
Status: github.Ptr("completed"),
1441+
Conclusion: github.Ptr("success"),
1442+
HTMLURL: github.Ptr("https://github.com/owner/repo/runs/1"),
1443+
},
1444+
{
1445+
ID: github.Ptr(int64(2)),
1446+
Name: github.Ptr("test"),
1447+
Status: github.Ptr("completed"),
1448+
Conclusion: github.Ptr("success"),
1449+
HTMLURL: github.Ptr("https://github.com/owner/repo/runs/2"),
1450+
},
1451+
},
1452+
}
1453+
1454+
tests := []struct {
1455+
name string
1456+
mockedClient *http.Client
1457+
requestArgs map[string]any
1458+
expectError bool
1459+
expectedCheckRuns *github.ListCheckRunsResults
1460+
expectedErrMsg string
1461+
}{
1462+
{
1463+
name: "successful check runs fetch",
1464+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1465+
GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPR),
1466+
GetReposCommitsCheckRunsByOwnerByRepoByRef: mockResponse(t, http.StatusOK, mockCheckRuns),
1467+
}),
1468+
requestArgs: map[string]any{
1469+
"method": "get_check_runs",
1470+
"owner": "owner",
1471+
"repo": "repo",
1472+
"pullNumber": float64(42),
1473+
},
1474+
expectError: false,
1475+
expectedCheckRuns: mockCheckRuns,
1476+
},
1477+
{
1478+
name: "PR fetch fails",
1479+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1480+
GetReposPullsByOwnerByRepoByPullNumber: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1481+
w.WriteHeader(http.StatusNotFound)
1482+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
1483+
}),
1484+
}),
1485+
requestArgs: map[string]any{
1486+
"method": "get_check_runs",
1487+
"owner": "owner",
1488+
"repo": "repo",
1489+
"pullNumber": float64(999),
1490+
},
1491+
expectError: true,
1492+
expectedErrMsg: "failed to get pull request",
1493+
},
1494+
{
1495+
name: "check runs fetch fails",
1496+
mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{
1497+
GetReposPullsByOwnerByRepoByPullNumber: mockResponse(t, http.StatusOK, mockPR),
1498+
GetReposCommitsCheckRunsByOwnerByRepoByRef: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
1499+
w.WriteHeader(http.StatusNotFound)
1500+
_, _ = w.Write([]byte(`{"message": "Not Found"}`))
1501+
}),
1502+
}),
1503+
requestArgs: map[string]any{
1504+
"method": "get_check_runs",
1505+
"owner": "owner",
1506+
"repo": "repo",
1507+
"pullNumber": float64(42),
1508+
},
1509+
expectError: true,
1510+
expectedErrMsg: "failed to get check runs",
1511+
},
1512+
}
1513+
1514+
for _, tc := range tests {
1515+
t.Run(tc.name, func(t *testing.T) {
1516+
// Setup client with mock
1517+
client := github.NewClient(tc.mockedClient)
1518+
serverTool := PullRequestRead(translations.NullTranslationHelper)
1519+
deps := BaseDeps{
1520+
Client: client,
1521+
RepoAccessCache: stubRepoAccessCache(githubv4.NewClient(nil), 5*time.Minute),
1522+
Flags: stubFeatureFlags(map[string]bool{"lockdown-mode": false}),
1523+
}
1524+
handler := serverTool.Handler(deps)
1525+
1526+
// Create call request
1527+
request := createMCPRequest(tc.requestArgs)
1528+
1529+
// Call handler
1530+
result, err := handler(ContextWithDeps(context.Background(), deps), &request)
1531+
1532+
// Verify results
1533+
if tc.expectError {
1534+
require.NoError(t, err)
1535+
require.True(t, result.IsError)
1536+
errorContent := getErrorResult(t, result)
1537+
assert.Contains(t, errorContent.Text, tc.expectedErrMsg)
1538+
return
1539+
}
1540+
1541+
require.NoError(t, err)
1542+
require.False(t, result.IsError)
1543+
1544+
// Parse the result and get the text content if no error
1545+
textContent := getTextResult(t, result)
1546+
1547+
// Unmarshal and verify the result (using minimal type)
1548+
var returnedCheckRuns MinimalCheckRunsResult
1549+
err = json.Unmarshal([]byte(textContent.Text), &returnedCheckRuns)
1550+
require.NoError(t, err)
1551+
assert.Equal(t, *tc.expectedCheckRuns.Total, returnedCheckRuns.TotalCount)
1552+
assert.Len(t, returnedCheckRuns.CheckRuns, len(tc.expectedCheckRuns.CheckRuns))
1553+
for i, checkRun := range returnedCheckRuns.CheckRuns {
1554+
assert.Equal(t, *tc.expectedCheckRuns.CheckRuns[i].Name, checkRun.Name)
1555+
assert.Equal(t, *tc.expectedCheckRuns.CheckRuns[i].Status, checkRun.Status)
1556+
assert.Equal(t, *tc.expectedCheckRuns.CheckRuns[i].Conclusion, checkRun.Conclusion)
1557+
}
1558+
})
1559+
}
1560+
}
1561+
14071562
func Test_UpdatePullRequestBranch(t *testing.T) {
14081563
// Verify tool definition once
14091564
serverTool := UpdatePullRequestBranch(translations.NullTranslationHelper)

0 commit comments

Comments
 (0)