diff --git a/pkg/github/actions.go b/pkg/github/actions.go index 0f22c3cc5f..ebee489e30 100644 --- a/pkg/github/actions.go +++ b/pkg/github/actions.go @@ -50,17 +50,27 @@ const ( // handleFailedJobLogs gets logs for all failed jobs in a workflow run func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, *ActionsJobLogsOutput, error) { // First, get all jobs for the workflow run - jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{ - Filter: "latest", - }) - if err != nil { - return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil + opts := &github.ListWorkflowJobsOptions{ + Filter: "latest", + ListOptions: github.ListOptions{PerPage: 100}, + } + var allJobs []*github.WorkflowJob + for { + jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, opts) + if err != nil { + return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to list workflow jobs", resp, err), nil, nil + } + _ = resp.Body.Close() + allJobs = append(allJobs, jobs.Jobs...) + if resp.NextPage == 0 { + break + } + opts.Page = resp.NextPage } - defer func() { _ = resp.Body.Close() }() // Filter for failed jobs var failedJobs []*github.WorkflowJob - for _, job := range jobs.Jobs { + for _, job := range allJobs { if job.GetConclusion() == "failure" { failedJobs = append(failedJobs, job) } @@ -70,7 +80,7 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo result := &ActionsFailedJobLogsOutput{ Message: "No failed jobs found in this workflow run", RunID: runID, - TotalJobs: len(jobs.Jobs), + TotalJobs: len(allJobs), } r, _ := json.Marshal(result) return utils.NewToolResultText(string(r)), &ActionsJobLogsOutput{Failed: result}, nil @@ -95,7 +105,7 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo result := &ActionsFailedJobLogsOutput{ Message: fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)), RunID: runID, - TotalJobs: len(jobs.Jobs), + TotalJobs: len(allJobs), FailedJobs: len(failedJobs), Logs: &logResults, ReturnFormat: &ActionsLogsReturnFormat{Content: returnContent, URLs: !returnContent}, diff --git a/pkg/github/actions_test.go b/pkg/github/actions_test.go index 5caf7d269b..8ba9a65e60 100644 --- a/pkg/github/actions_test.go +++ b/pkg/github/actions_test.go @@ -751,4 +751,57 @@ func Test_ActionsGetJobLogs_FailedJobs(t *testing.T) { require.NoError(t, err) assert.Equal(t, "No failed jobs found in this workflow run", response["message"]) }) + + t.Run("failed job on a later page", func(t *testing.T) { + mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ + GetReposActionsRunsJobsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + jobs := &github.Jobs{TotalCount: new(31)} + if r.URL.Query().Get("page") == "2" { + jobs.Jobs = []*github.WorkflowJob{ + {ID: new(int64(31)), Name: new("test-job-31"), Conclusion: new("failure")}, + } + } else { + for i := int64(1); i <= 30; i++ { + jobs.Jobs = append(jobs.Jobs, &github.WorkflowJob{ + ID: new(i), + Name: new("passing-job"), + Conclusion: new("success"), + }) + } + w.Header().Set("Link", `; rel="next"`) + } + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(jobs) + }), + GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Location", "https://github.com/logs/job/31") + w.WriteHeader(http.StatusFound) + }), + }) + + client := mustNewGHClient(t, mockedClient) + deps := BaseDeps{ + Client: client, + ContentWindowSize: 5000, + } + handler := toolDef.Handler(deps) + + request := createMCPRequest(map[string]any{ + "owner": "owner", + "repo": "repo", + "run_id": float64(456), + "failed_only": true, + }) + result, err := handler(ContextWithDeps(context.Background(), deps), &request) + + require.NoError(t, err) + require.False(t, result.IsError) + + textContent := getTextResult(t, result) + var response map[string]any + err = json.Unmarshal([]byte(textContent.Text), &response) + require.NoError(t, err) + assert.Equal(t, float64(1), response["failed_jobs"]) + assert.Equal(t, float64(31), response["total_jobs"]) + }) }