Skip to content

Commit 1a62381

Browse files
mikelaneclaude
andcommitted
feat(pull_requests): add get_ci_summary method to pull_request_read tool
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2a1eaac commit 1a62381

File tree

3 files changed

+685
-3
lines changed

3 files changed

+685
-3
lines changed

pkg/github/__toolsnaps__/pull_request_read.snap

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
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 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",
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 9. get_ci_summary - Get a summary of the CI status for a pull request. Combines the commit status and check runs into a single aggregated result with an overall verdict (passing, failing, pending, or no_checks).\n",
1111
"enum": [
1212
"get",
1313
"get_diff",
@@ -16,7 +16,8 @@
1616
"get_review_comments",
1717
"get_reviews",
1818
"get_comments",
19-
"get_check_runs"
19+
"get_check_runs",
20+
"get_ci_summary"
2021
],
2122
"type": "string"
2223
},

pkg/github/pullrequests.go

Lines changed: 195 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ Possible options:
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.
4141
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.
42+
9. get_ci_summary - Get a summary of the CI status for a pull request. Combines the commit status and check runs into a single aggregated result with an overall verdict (passing, failing, pending, or no_checks).
4243
`,
43-
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments", "get_check_runs"},
44+
Enum: []any{"get", "get_diff", "get_status", "get_files", "get_review_comments", "get_reviews", "get_comments", "get_check_runs", "get_ci_summary"},
4445
},
4546
"owner": {
4647
Type: "string",
@@ -132,6 +133,9 @@ Possible options:
132133
case "get_check_runs":
133134
result, err := GetPullRequestCheckRuns(ctx, client, owner, repo, pullNumber, pagination)
134135
return result, nil, err
136+
case "get_ci_summary":
137+
result, err := GetPullRequestCISummary(ctx, client, owner, repo, pullNumber)
138+
return result, nil, err
135139
default:
136140
return utils.NewToolResultError(fmt.Sprintf("unknown method: %s", method)), nil, nil
137141
}
@@ -336,6 +340,196 @@ func GetPullRequestCheckRuns(ctx context.Context, client *github.Client, owner,
336340
return utils.NewToolResultText(string(r)), nil
337341
}
338342

343+
// CISummaryVerdict represents the possible verdicts for a CI summary.
344+
type CISummaryVerdict string
345+
346+
const (
347+
CISummaryVerdictPassing CISummaryVerdict = "passing"
348+
CISummaryVerdictFailing CISummaryVerdict = "failing"
349+
CISummaryVerdictPending CISummaryVerdict = "pending"
350+
CISummaryVerdictNoChecks CISummaryVerdict = "no_checks"
351+
)
352+
353+
// CISummaryCheck represents a single check in the CI summary.
354+
type CISummaryCheck struct {
355+
Name string `json:"name"`
356+
Status string `json:"status"`
357+
Conclusion string `json:"conclusion,omitempty"`
358+
}
359+
360+
// CISummaryResult represents the aggregated CI summary for a pull request.
361+
type CISummaryResult struct {
362+
Verdict CISummaryVerdict `json:"verdict"`
363+
CombinedStatus string `json:"combinedStatus"`
364+
TotalCheckRuns int `json:"totalCheckRuns"`
365+
Passing int `json:"passing"`
366+
Failing int `json:"failing"`
367+
Pending int `json:"pending"`
368+
Checks []CISummaryCheck `json:"checks"`
369+
FailingChecks []CISummaryCheck `json:"failingChecks"`
370+
}
371+
372+
func GetPullRequestCISummary(ctx context.Context, client *github.Client, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) {
373+
// Get the PR to get the head SHA
374+
pr, resp, err := client.PullRequests.Get(ctx, owner, repo, pullNumber)
375+
if err != nil {
376+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
377+
"failed to get pull request",
378+
resp,
379+
err,
380+
), nil
381+
}
382+
defer func() { _ = resp.Body.Close() }()
383+
384+
if resp.StatusCode != http.StatusOK {
385+
body, err := io.ReadAll(resp.Body)
386+
if err != nil {
387+
return nil, fmt.Errorf("failed to read response body: %w", err)
388+
}
389+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get pull request", resp, body), nil
390+
}
391+
392+
headSHA := pr.GetHead().GetSHA()
393+
394+
// Get combined status
395+
status, resp, err := client.Repositories.GetCombinedStatus(ctx, owner, repo, headSHA, nil)
396+
if err != nil {
397+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
398+
"failed to get combined status",
399+
resp,
400+
err,
401+
), nil
402+
}
403+
defer func() { _ = resp.Body.Close() }()
404+
405+
if resp.StatusCode != http.StatusOK {
406+
body, err := io.ReadAll(resp.Body)
407+
if err != nil {
408+
return nil, fmt.Errorf("failed to read response body: %w", err)
409+
}
410+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get combined status", resp, body), nil
411+
}
412+
413+
// Get check runs (use PerPage: 100 to reduce risk of incomplete results)
414+
checkRunOpts := &github.ListCheckRunsOptions{
415+
ListOptions: github.ListOptions{PerPage: 100},
416+
}
417+
checkRuns, resp, err := client.Checks.ListCheckRunsForRef(ctx, owner, repo, headSHA, checkRunOpts)
418+
if err != nil {
419+
return ghErrors.NewGitHubAPIErrorResponse(ctx,
420+
"failed to get check runs",
421+
resp,
422+
err,
423+
), nil
424+
}
425+
defer func() { _ = resp.Body.Close() }()
426+
427+
if resp.StatusCode != http.StatusOK {
428+
body, err := io.ReadAll(resp.Body)
429+
if err != nil {
430+
return nil, fmt.Errorf("failed to read response body: %w", err)
431+
}
432+
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get check runs", resp, body), nil
433+
}
434+
435+
// Build check lists
436+
var checks []CISummaryCheck
437+
var failingChecks []CISummaryCheck
438+
passingCount := 0
439+
failingCount := 0
440+
pendingCount := 0
441+
442+
for _, cr := range checkRuns.CheckRuns {
443+
check := CISummaryCheck{
444+
Name: cr.GetName(),
445+
Status: cr.GetStatus(),
446+
Conclusion: cr.GetConclusion(),
447+
}
448+
checks = append(checks, check)
449+
450+
if cr.GetStatus() != "completed" {
451+
pendingCount++
452+
} else {
453+
switch cr.GetConclusion() {
454+
case "success", "neutral", "skipped":
455+
passingCount++
456+
case "failure", "cancelled", "timed_out", "action_required", "stale":
457+
failingCount++
458+
failingChecks = append(failingChecks, check)
459+
}
460+
}
461+
}
462+
463+
// Aggregate combined status failures into failingChecks before computing counts
464+
combinedState := status.GetState()
465+
if combinedState == "failure" || combinedState == "error" {
466+
for _, s := range status.Statuses {
467+
if s.GetState() == "failure" || s.GetState() == "error" {
468+
failingChecks = append(failingChecks, CISummaryCheck{
469+
Name: s.GetContext(),
470+
Status: "completed",
471+
Conclusion: s.GetState(),
472+
})
473+
failingCount++
474+
}
475+
}
476+
}
477+
478+
// Ensure non-nil slices for JSON serialization
479+
if checks == nil {
480+
checks = []CISummaryCheck{}
481+
}
482+
if failingChecks == nil {
483+
failingChecks = []CISummaryCheck{}
484+
}
485+
486+
// Compute verdict after all failure sources are aggregated
487+
verdict := computeCIVerdict(combinedState, len(status.Statuses), len(checkRuns.CheckRuns), failingCount, pendingCount)
488+
489+
result := CISummaryResult{
490+
Verdict: verdict,
491+
CombinedStatus: combinedState,
492+
TotalCheckRuns: len(checkRuns.CheckRuns),
493+
Passing: passingCount,
494+
Failing: failingCount,
495+
Pending: pendingCount,
496+
Checks: checks,
497+
FailingChecks: failingChecks,
498+
}
499+
500+
return MarshalledTextResult(result), nil
501+
}
502+
503+
func computeCIVerdict(combinedState string, statusCount, checkRunCount, failingCount, pendingCount int) CISummaryVerdict {
504+
// If combined status is failure/error, verdict is failing
505+
if combinedState == "failure" || combinedState == "error" {
506+
return CISummaryVerdictFailing
507+
}
508+
509+
// If any check run is failing, verdict is failing
510+
if failingCount > 0 {
511+
return CISummaryVerdictFailing
512+
}
513+
514+
// If any check run is pending (not completed), verdict is pending
515+
if pendingCount > 0 {
516+
return CISummaryVerdictPending
517+
}
518+
519+
// If combined status is pending with actual statuses, verdict is pending
520+
if combinedState == "pending" && statusCount > 0 {
521+
return CISummaryVerdictPending
522+
}
523+
524+
// If no checks at all, verdict is no_checks
525+
if combinedState == "pending" && statusCount == 0 && checkRunCount == 0 {
526+
return CISummaryVerdictNoChecks
527+
}
528+
529+
// Everything is passing
530+
return CISummaryVerdictPassing
531+
}
532+
339533
func GetPullRequestFiles(ctx context.Context, client *github.Client, owner, repo string, pullNumber int, pagination PaginationParams) (*mcp.CallToolResult, error) {
340534
opts := &github.ListOptions{
341535
PerPage: pagination.PerPage,

0 commit comments

Comments
 (0)