Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions pkg/github/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
Expand All @@ -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},
Expand Down
53 changes: 53 additions & 0 deletions pkg/github/actions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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", `<https://api.github.com/repos/owner/repo/actions/runs/456/jobs?page=2>; 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"])
})
}