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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,12 +628,12 @@ The following sets of tools are available:

- **get_job_logs** - Get GitHub Actions workflow job logs
- **Required OAuth Scopes**: `repo`
- `failed_only`: When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided. (boolean, optional)
- `job_id`: The unique identifier of the workflow job. Required when getting logs for a single job. (number, optional)
- `failed_only`: When true, returns logs only for failed jobs. With run_id, returns failed jobs in the run. With job_id, returns logs only if that job failed. (boolean, optional)
- `job_id`: The unique identifier of the workflow job. Provide either job_id or run_id, not both. (number, optional)
- `owner`: Repository owner (string, required)
- `repo`: Repository name (string, required)
- `return_content`: Returns actual log content instead of URLs (boolean, optional)
- `run_id`: The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run. (number, optional)
- `run_id`: The unique identifier of the workflow run. Provide either run_id or job_id, not both. (number, optional)
- `tail_lines`: Number of lines to return from the end of the log (number, optional)

</details>
Expand Down
96 changes: 61 additions & 35 deletions pkg/github/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ const (
actionsMethodDeleteWorkflowRunLogs = "delete_workflow_run_logs"
)

// 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, any, error) {
// handleRunJobLogs gets logs for workflow jobs in a run, optionally limited to failed jobs.
func handleRunJobLogs(ctx context.Context, client *github.Client, owner, repo string, runID int64, failedOnly bool, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) {
// First, get all jobs for the workflow run
jobs, resp, err := client.Actions.ListWorkflowJobs(ctx, owner, repo, runID, &github.ListWorkflowJobsOptions{
Filter: "latest",
Expand All @@ -57,28 +57,37 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo
}
defer func() { _ = resp.Body.Close() }()

// Filter for failed jobs
var failedJobs []*github.WorkflowJob
for _, job := range jobs.Jobs {
if job.GetConclusion() == "failure" {
failedJobs = append(failedJobs, job)
selectedJobs := jobs.Jobs
if failedOnly {
selectedJobs = nil
for _, job := range jobs.Jobs {
if job.GetConclusion() == "failure" {
selectedJobs = append(selectedJobs, job)
}
}
}

if len(failedJobs) == 0 {
if len(selectedJobs) == 0 {
message := "No jobs found in this workflow run"
if failedOnly {
message = "No failed jobs found in this workflow run"
}
result := map[string]any{
"message": "No failed jobs found in this workflow run",
"run_id": runID,
"total_jobs": len(jobs.Jobs),
"failed_jobs": 0,
"message": message,
"run_id": runID,
"total_jobs": len(jobs.Jobs),
"returned_jobs": 0,
}
if failedOnly {
result["failed_jobs"] = 0
}
r, _ := json.Marshal(result)
return utils.NewToolResultText(string(r)), nil, nil
}

// Collect logs for all failed jobs
// Collect logs for the selected jobs.
var logResults []map[string]any
for _, job := range failedJobs {
for _, job := range selectedJobs {
jobResult, resp, err := getJobLogData(ctx, client, owner, repo, job.GetID(), job.GetName(), returnContent, tailLines, contentWindowSize)
if err != nil {
// Continue with other jobs even if one fails
Expand All @@ -94,14 +103,21 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo
logResults = append(logResults, jobResult)
}

message := fmt.Sprintf("Retrieved logs for %d jobs", len(selectedJobs))
if failedOnly {
message = fmt.Sprintf("Retrieved logs for %d failed jobs", len(selectedJobs))
}
result := map[string]any{
"message": fmt.Sprintf("Retrieved logs for %d failed jobs", len(failedJobs)),
"message": message,
"run_id": runID,
"total_jobs": len(jobs.Jobs),
"failed_jobs": len(failedJobs),
"returned_jobs": len(selectedJobs),
"logs": logResults,
"return_format": map[string]bool{"content": returnContent, "urls": !returnContent},
}
if failedOnly {
result["failed_jobs"] = len(selectedJobs)
}

r, err := json.Marshal(result)
if err != nil {
Expand All @@ -111,8 +127,20 @@ func handleFailedJobLogs(ctx context.Context, client *github.Client, owner, repo
return utils.NewToolResultText(string(r)), nil, nil
}

// handleSingleJobLogs gets logs for a single job
func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) {
// handleSingleJobLogs gets logs for a single job, optionally requiring a failed conclusion.
func handleSingleJobLogs(ctx context.Context, client *github.Client, owner, repo string, jobID int64, failedOnly bool, returnContent bool, tailLines int, contentWindowSize int) (*mcp.CallToolResult, any, error) {
if failedOnly {
job, resp, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, jobID)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get workflow job", resp, err), nil, nil
}
defer func() { _ = resp.Body.Close() }()

if job.GetConclusion() != "failure" {
return utils.NewToolResultError(fmt.Sprintf("job %d did not fail; conclusion is %s", jobID, job.GetConclusion())), nil, nil
}
}

jobResult, resp, err := getJobLogData(ctx, client, owner, repo, jobID, "", returnContent, tailLines, contentWindowSize)
if err != nil {
return ghErrors.NewGitHubAPIErrorResponse(ctx, "failed to get job logs", resp, err), nil, nil
Expand Down Expand Up @@ -650,8 +678,8 @@ func ActionsGetJobLogs(t translations.TranslationHelperFunc) inventory.ServerToo
mcp.Tool{
Name: "get_job_logs",
Description: t("TOOL_GET_JOB_LOGS_CONSOLIDATED_DESCRIPTION", `Get logs for GitHub Actions workflow jobs.
Use this tool to retrieve logs for a specific job or all failed jobs in a workflow run.
For single job logs, provide job_id. For all failed jobs in a run, provide run_id with failed_only=true.
Use this tool to retrieve logs for a specific job or jobs in a workflow run.
Provide job_id for one job, run_id for all jobs in a run, or run_id with failed_only=true for failed jobs only.
`),
Annotations: &mcp.ToolAnnotations{
Title: t("TOOL_GET_JOB_LOGS_CONSOLIDATED_USER_TITLE", "Get GitHub Actions workflow job logs"),
Expand All @@ -670,15 +698,15 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i
},
"job_id": {
Type: "number",
Description: "The unique identifier of the workflow job. Required when getting logs for a single job.",
Description: "The unique identifier of the workflow job. Provide either job_id or run_id, not both.",
},
"run_id": {
Type: "number",
Description: "The unique identifier of the workflow run. Required when failed_only is true to get logs for all failed jobs in the run.",
Description: "The unique identifier of the workflow run. Provide either run_id or job_id, not both.",
},
"failed_only": {
Type: "boolean",
Description: "When true, gets logs for all failed jobs in the workflow run specified by run_id. Requires run_id to be provided.",
Description: "When true, returns logs only for failed jobs. With run_id, returns failed jobs in the run. With job_id, returns logs only if that job failed.",
},
"return_content": {
Type: "boolean",
Expand Down Expand Up @@ -739,11 +767,8 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i
}

// Validate parameters
if failedOnly && runID == 0 {
return utils.NewToolResultError("run_id is required when failed_only is true"), nil, nil
}
if !failedOnly && jobID == 0 {
return utils.NewToolResultError("job_id is required when failed_only is false"), nil, nil
if jobID > 0 && runID > 0 {
return utils.NewToolResultError("provide either job_id or run_id, not both"), nil, nil
}

// attachIFC adds the IFC label to a successful result when IFC
Expand All @@ -754,17 +779,18 @@ For single job logs, provide job_id. For all failed jobs in a run, provide run_i
return attachRepoVisibilityIFCLabel(ctx, deps, client, owner, repo, r, ifc.LabelActionsResult)
}

if failedOnly && runID > 0 {
// Handle failed-only mode: get logs for all failed jobs in the workflow run
result, payload, err := handleFailedJobLogs(ctx, client, owner, repo, int64(runID), returnContent, tailLines, deps.GetContentWindowSize())
if runID > 0 {
// Handle run mode: get logs for all jobs, optionally filtered to failed jobs.
result, payload, err := handleRunJobLogs(ctx, client, owner, repo, int64(runID), failedOnly, returnContent, tailLines, deps.GetContentWindowSize())
return attachIFC(result), payload, err
} else if jobID > 0 {
// Handle single job mode
result, payload, err := handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), returnContent, tailLines, deps.GetContentWindowSize())
}
if jobID > 0 {
// Handle single job mode.
result, payload, err := handleSingleJobLogs(ctx, client, owner, repo, int64(jobID), failedOnly, returnContent, tailLines, deps.GetContentWindowSize())
return attachIFC(result), payload, err
}

return utils.NewToolResultError("Either job_id must be provided for single job logs, or run_id with failed_only=true for failed job logs"), nil, nil
return utils.NewToolResultError("Either job_id or run_id must be provided"), nil, nil
},
)
return tool
Expand Down
Loading