{"record":{"id":"0f7c463feb160980","repo":"github/github-mcp-server","slug":"failed-to-get-job-logs-for-job-d-w","errorCode":null,"errorMessage":"failed to get job logs for job %d: %w","messagePattern":"failed to get job logs for job (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/github/actions.go","lineNumber":134,"sourceCode":"\tjobResult, resp, err := getJobLogData(ctx, client, owner, repo, jobID, \"\", returnContent, tailLines, contentWindowSize)\n\tif err != nil {\n\t\treturn ghErrors.NewGitHubAPIErrorResponse(ctx, \"failed to get job logs\", resp, err), nil, nil\n\t}\n\n\tr, err := json.Marshal(jobResult)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"failed to marshal response: %w\", err)\n\t}\n\n\treturn utils.NewToolResultText(string(r)), nil, nil\n}\n\n// getJobLogData retrieves log data for a single job, either as URL or content\nfunc 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) {\n\t// Get the download URL for the job logs\n\turl, resp, err := client.Actions.GetWorkflowJobLogs(ctx, owner, repo, jobID, 1)\n\tif err != nil {\n\t\treturn nil, resp, fmt.Errorf(\"failed to get job logs for job %d: %w\", jobID, err)\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tresult := map[string]any{\n\t\t\"job_id\": jobID,\n\t}\n\tif jobName != \"\" {\n\t\tresult[\"job_name\"] = jobName\n\t}\n\n\tif returnContent {\n\t\t// Download and return the actual log content\n\t\tcontent, originalLength, httpResp, err := downloadLogContent(ctx, url.String(), tailLines, contentWindowSize) //nolint:bodyclose // Response body is closed in downloadLogContent, but we need to return httpResp\n\t\tif err != nil {\n\t\t\tvar ghResp *github.Response\n\t\t\tif httpResp != nil {\n\t\t\t\tghResp = &github.Response{Response: httpResp}\n\t\t\t}","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/pkg/github/actions.go#L116-L152","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify job_id is a job ID, not a run ID - list jobs first with list_workflow_jobs for the run","Check the wrapped error: 404 means deleted/expired logs (raise log retention if needed); 403 means missing Actions read permission or rate limit","For expired logs, nothing is recoverable - re-run the workflow to regenerate logs","On rate limit, back off per X-RateLimit-Reset and retry"],"exampleFix":"// before\nresult, resp, err := getJobLogData(ctx, client, owner, repo, jobID, \"\", true, tailLines, win)\nif err != nil {\n    return err\n}\n\n// after - validate the job exists and its logs are still retained\njob, jresp, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, jobID)\nif err != nil {\n    return fmt.Errorf(\"job %d not found (check it is a job id, not a run id): %w\", jobID, err)\n}\nif job.CompletedAt != nil && time.Since(job.CompletedAt.Time) > 90*24*time.Hour {\n    return fmt.Errorf(\"job %d logs likely expired (completed %s)\", jobID, job.CompletedAt)\n}","handlingStrategy":"retry","validationCode":"// Before fetching logs, confirm the job exists and is a job (not a run)\njob, _, err := client.Actions.GetWorkflowJobByID(ctx, owner, repo, jobID)\nif err != nil {\n    return fmt.Errorf(\"job %d invalid (verify via list_workflow_jobs): %w\", jobID, err)\n}","typeGuard":"func isRateLimitOrNotFound(err error) bool {\n    var ghErr *github.ErrorResponse\n    if errors.As(err, &ghErr) {\n        return ghErr.Response != nil && (ghErr.Response.StatusCode == 404 || ghErr.Response.StatusCode == 403)\n    }\n    return false\n}","tryCatchPattern":"if err != nil {\n    if errors.Is(err, context.DeadlineExceeded) || is5xx(err) {\n        time.Sleep(backoff) // retry with backoff\n        return retry()\n    }\n    if isRateLimitOrNotFound(err) {\n        return err // fix input or wait for reset; retry will not help\n    }\n    return err\n}","preventionTips":["Always source job IDs from list_workflow_jobs output, never hand-copy run IDs","Give the PAT 'Actions: Read' (fine-grained) or repo scope (classic)","Raise log retention or export logs promptly for important runs","Rate-limit your own polling loops"],"tags":["github-actions","logs","permissions","rate-limit","go"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}