fetchJobDetailsWithCounts fetches all job information for a workflow run in a single API call and returns the full detail slice together with the count of failed jobs. It is the single source of truth for the jobs endpoint; fetchJobDetails and fetchJobStatuses are thin wrappers that each return only
(runID int64, verbose bool)
| 77 | // It is the single source of truth for the jobs endpoint; fetchJobDetails and |
| 78 | // fetchJobStatuses are thin wrappers that each return only the value they need. |
| 79 | func fetchJobDetailsWithCounts(runID int64, verbose bool) ([]JobInfoWithDuration, int, error) { |
| 80 | logsGitHubAPILog.Printf("Fetching job details: runID=%d", runID) |
| 81 | if verbose { |
| 82 | fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Fetching job details for run %d", runID))) |
| 83 | } |
| 84 | |
| 85 | output, err := workflow.RunGHCombined("Fetching job details...", "api", |
| 86 | fmt.Sprintf("repos/{owner}/{repo}/actions/runs/%d/jobs", runID), |
| 87 | "--jq", ".jobs[] | {name: .name, status: .status, conclusion: (.conclusion // \"\"), started_at: .started_at, completed_at: .completed_at, steps: ((.steps // []) | map({name: .name, status: .status, conclusion: (.conclusion // \"\")}))}") |
| 88 | if err != nil { |
| 89 | if verbose { |
| 90 | fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Failed to fetch job details for run %d: %v", runID, err))) |
| 91 | } |
| 92 | return nil, 0, err |
| 93 | } |
| 94 | |
| 95 | var jobs []JobInfoWithDuration |
| 96 | failedJobs := 0 |
| 97 | lines := strings.SplitSeq(strings.TrimSpace(string(output)), "\n") |
| 98 | for line := range lines { |
| 99 | if strings.TrimSpace(line) == "" { |
| 100 | continue |
| 101 | } |
| 102 | |
| 103 | var job JobInfo |
| 104 | if err := json.Unmarshal([]byte(line), &job); err != nil { |
| 105 | if verbose { |
| 106 | fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Failed to parse job info: "+line)) |
| 107 | } |
| 108 | continue |
| 109 | } |
| 110 | |
| 111 | jobWithDuration := JobInfoWithDuration{JobInfo: job} |
| 112 | if !job.StartedAt.IsZero() && !job.CompletedAt.IsZero() { |
| 113 | jobWithDuration.Duration = job.CompletedAt.Sub(job.StartedAt) |
| 114 | } |
| 115 | jobs = append(jobs, jobWithDuration) |
| 116 | |
| 117 | if isFailureConclusion(job.Conclusion) { |
| 118 | failedJobs++ |
| 119 | logsGitHubAPILog.Printf("Found failed job: name=%s, conclusion=%s", job.Name, job.Conclusion) |
| 120 | if verbose { |
| 121 | fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Found failed job '%s' with conclusion '%s'", job.Name, job.Conclusion))) |
| 122 | } |
| 123 | } |
| 124 | } |
| 125 | |
| 126 | logsGitHubAPILog.Printf("Job fetch complete: total=%d failed=%d", len(jobs), failedJobs) |
| 127 | return jobs, failedJobs, nil |
| 128 | } |
| 129 | |
| 130 | // fetchJobDetails gets detailed job information including durations for a workflow run. |
| 131 | // Errors from the underlying API call are suppressed so that callers can continue |