github/github-mcp-server · error

failed to get job logs for job %d: %w

Error message

failed to get job logs for job %d: %w

What it means

Returned by getJobLogData (pkg/github/actions.go:134) when client.Actions.GetWorkflowJobLogs fails - i.e. GET /repos/{owner}/{repo}/actions/jobs/{job_id}/logs errored before any redirect to the blob URL. The %w wraps the go-github error, so auth, permission, not-found, rate-limit, and network causes are all distinguishable by unwrapping. The jobID in the message identifies the failing job.

Source

Thrown at pkg/github/actions.go:134

	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
	}

	r, err := json.Marshal(jobResult)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to marshal response: %w", err)
	}

	return utils.NewToolResultText(string(r)), nil, nil
}

// getJobLogData retrieves log data for a single job, either as URL or content
func getJobLogData(ctx context.Context, client *github.Client, owner, repo string, jobID int64, jobName string, returnContent bool, tailLines int, contentWindowSize int) (map[string]any, *github.Response, error) {
	// Get the download URL for the job logs
	url, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1)
	if err != nil {
		return nil, resp, fmt.Errorf("failed to get job logs for job %d: %w", jobID, err)
	}
	defer func() { _ = resp.Body.Close() }()

	result := map[string]any{
		"job_id": jobID,
	}
	if jobName != "" {
		result["job_name"] = jobName
	}

	if returnContent {
		// Download and return the actual log content
		content, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp
		if err != nil {
			var ghResp *github.Response
			if httpResp != nil {
				ghResp = &github.Response{Response: httpResp}
			}

View on GitHub (pinned to 0ea1f775a7)

Solutions

  1. Verify job_id is a job ID, not a run ID - list jobs first with list_workflow_jobs for the run
  2. Check the wrapped error: 404 means deleted/expired logs (raise log retention if needed); 403 means missing Actions read permission or rate limit
  3. For expired logs, nothing is recoverable - re-run the workflow to regenerate logs
  4. On rate limit, back off per X-RateLimit-Reset and retry

Example fix

// before
result, resp, err := getJobLogData(ctx, client, owner, repo, jobID, "", true, tailLines, win)
if err != nil {
    return err
}

// after - validate the job exists and its logs are still retained
job, jresp, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, jobID)
if err != nil {
    return fmt.Errorf("job %d not found (check it is a job id, not a run id): %w", jobID, err)
}
if job.CompletedAt != nil && time.Since(job.CompletedAt.Time) > 90*24*time.Hour {
    return fmt.Errorf("job %d logs likely expired (completed %s)", jobID, job.CompletedAt)
}
Defensive patterns

Strategy: retry

Validate before calling

// Before fetching logs, confirm the job exists and is a job (not a run)
job, _, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, jobID)
if err != nil {
    return fmt.Errorf("job %d invalid (verify via list_workflow_jobs): %w", jobID, err)
}

Type guard

func isRateLimitOrNotFound(err error) bool {
    var ghErr *github.ErrorResponse
    if errors.As(err, &ghErr) {
        return ghErr.Response != nil && (ghErr.Response.StatusCode == 404 || ghErr.Response.StatusCode == 403)
    }
    return false
}

Try / catch

if err != nil {
    if errors.Is(err, context.DeadlineExceeded) || is5xx(err) {
        time.Sleep(backoff) // retry with backoff
        return retry()
    }
    if isRateLimitOrNotFound(err) {
        return err // fix input or wait for reset; retry will not help
    }
    return err
}

Prevention

When it happens

Trigger: Calling action_job_logs with a nonexistent or deleted job_id; a job whose logs exceeded the repository's retention period (default 90 days) and were purged; a token without Actions read permission; 403 primary/secondary rate limit; network failure reaching the API.

Common situations: Passing a workflow run ID where a job ID is expected (very common); fine-grained PAT missing 'Actions: Read'; logs already expired on repos with short retention; aggressive polling loops hitting secondary rate limits.

Related errors


AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15). Data as JSON: /api/errors/0f7c463feb160980. Report an issue: GitHub.