11#! /usr/bin/env bash
2- # Extract logs from failed GitHub Actions runs for a PR
3- # Usage: ./scripts/extract_pr_logs.sh <pr_number_or_run_id> [job_name_pattern] [--wait ]
2+ # Extract logs from GitHub Actions runs for a PR (including in-progress jobs)
3+ # Usage: ./scripts/extract_pr_logs.sh <pr_number_or_run_id> [job_name_pattern] [--all ]
44#
55# Examples:
66# ./scripts/extract_pr_logs.sh 329 # Latest failed run for PR #329
77# ./scripts/extract_pr_logs.sh 329 Integration # Only Integration Test jobs
8- # ./scripts/extract_pr_logs.sh 329 --wait # Wait for logs to be available
8+ # ./scripts/extract_pr_logs.sh 329 --all # Show all jobs (not just failed)
99# ./scripts/extract_pr_logs.sh 18640062283 # Specific run ID
1010
1111set -euo pipefail
1212
1313INPUT=" ${1:- } "
1414JOB_PATTERN=" ${2:- } "
15- WAIT_FOR_LOGS =false
15+ SHOW_ALL_JOBS =false
1616
1717# Parse flags
18- if [[ " $JOB_PATTERN " == " --wait" ]]; then
19- WAIT_FOR_LOGS=true
18+ for arg in " $@ " ; do
19+ if [[ " $arg " == " --all" ]]; then
20+ SHOW_ALL_JOBS=true
21+ fi
22+ done
23+
24+ # Remove flags from JOB_PATTERN if they were set as second arg
25+ if [[ " $JOB_PATTERN " == " --all" ]]; then
2026 JOB_PATTERN=" "
21- elif [[ " ${3:- } " == " --wait" ]]; then
22- WAIT_FOR_LOGS=true
2327fi
2428
2529if [[ -z " $INPUT " ]]; then
26- echo " ❌ Usage: $0 <pr_number_or_run_id> [job_name_pattern]" >&2
30+ echo " ❌ Usage: $0 <pr_number_or_run_id> [job_name_pattern] [--all] " >&2
2731 echo " " >&2
2832 echo " Examples:" >&2
29- echo " $0 329 # Latest failed run for PR #329 (RECOMMENDED) " >&2
33+ echo " $0 329 # Latest failed run for PR #329" >&2
3034 echo " $0 329 Integration # Only Integration Test jobs from PR #329" >&2
35+ echo " $0 329 --all # Show all jobs (not just failed/in-progress)" >&2
3136 echo " $0 18640062283 # Specific run ID" >&2
3237 exit 1
3338fi
3439
3540# Detect if input is PR number or run ID (run IDs are much longer)
3641if [[ " $INPUT " =~ ^[0-9]{1,5}$ ]]; then
3742 PR_NUMBER=" $INPUT "
38- echo " 🔍 Finding latest failed run for PR #$PR_NUMBER ..." >&2
39-
40- # Get the latest failed run for this PR
41- RUN_ID=$( gh pr checks " $PR_NUMBER " --json name,link,state --jq ' .[] | select(.state == "FAILURE") | .link' | head -1 | sed -E ' s|.*/runs/([0-9]+).*|\1|' || echo " " )
43+
44+ # If --all flag is set or no failures, get latest run regardless of status
45+ if [[ " $SHOW_ALL_JOBS " == true ]]; then
46+ echo " 🔍 Finding latest run for PR #$PR_NUMBER ..." >&2
47+ RUN_ID=$( gh pr checks " $PR_NUMBER " --json name,link,state --jq ' .[] | select(.link | contains("/runs/")) | .link' | head -1 | sed -E ' s|.*/runs/([0-9]+).*|\1|' || echo " " )
48+ else
49+ echo " 🔍 Finding latest failed run for PR #$PR_NUMBER ..." >&2
50+ # Get the latest failed run for this PR
51+ RUN_ID=$( gh pr checks " $PR_NUMBER " --json name,link,state --jq ' .[] | select(.state == "FAILURE") | select(.link | contains("/runs/")) | .link' | head -1 | sed -E ' s|.*/runs/([0-9]+).*|\1|' || echo " " )
52+ fi
4253
4354 if [[ -z " $RUN_ID " ]]; then
4455 echo " ❌ No failed runs found for PR #$PR_NUMBER " >&2
4556 echo " " >&2
4657 echo " Current check status:" >&2
4758 gh pr checks " $PR_NUMBER " 2>&1 || true
59+ echo " " >&2
60+ echo " 💡 Tip: Use --all flag to see logs from any run (not just failed)" >&2
4861 exit 1
4962 fi
5063
51- echo " 📋 Found failed run: $RUN_ID " >&2
64+ echo " 📋 Found run: $RUN_ID " >&2
5265else
5366 RUN_ID=" $INPUT "
5467 echo " 📋 Fetching logs for run $RUN_ID ..." >&2
@@ -65,12 +78,18 @@ if [[ -z "$JOBS" ]]; then
6578 exit 1
6679fi
6780
68- # Filter to failed jobs only (unless specific pattern requested)
69- if [[ -z " $JOB_PATTERN " ]]; then
70- FAILED_JOBS=$( echo " $JOBS " | jq -r ' select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED")' )
81+ # Filter jobs based on flags and pattern
82+ if [[ -z " $JOB_PATTERN " ]] && [[ " $SHOW_ALL_JOBS " == false ]]; then
83+ # Show failed/timed out/cancelled jobs, OR in-progress/pending jobs if no failures exist
84+ FAILED_JOBS=$( echo " $JOBS " | jq -r ' select(.conclusion == "failure" or .conclusion == "timed_out" or .conclusion == "cancelled")' )
85+ IN_PROGRESS_JOBS=$( echo " $JOBS " | jq -r ' select(.status == "in_progress" or .status == "queued" or .status == "pending")' )
86+
7187 if [[ -n " $FAILED_JOBS " ]]; then
72- echo " 🎯 Showing only failed jobs (use job_pattern to see others )" >&2
88+ echo " 🎯 Showing only failed jobs (use --all to see all jobs )" >&2
7389 JOBS=" $FAILED_JOBS "
90+ elif [[ -n " $IN_PROGRESS_JOBS " ]]; then
91+ echo " ⏳ No failures yet - showing in-progress/pending jobs (use --all to see all)" >&2
92+ JOBS=" $IN_PROGRESS_JOBS "
7493 fi
7594fi
7695
@@ -111,43 +130,57 @@ suggest_local_command() {
111130 esac
112131}
113132
133+ # Show step-by-step progress for in-progress/pending jobs
134+ show_job_steps () {
135+ local job_id=" $1 "
136+ local job_status=" $2 "
137+
138+ if [[ " $job_status " == " in_progress" ]] || [[ " $job_status " == " queued" ]] || [[ " $job_status " == " pending" ]]; then
139+ echo " " >&2
140+ echo " 📊 Step-by-step status:" >&2
141+ gh api " /repos/coder/cmux/actions/jobs/$job_id " | jq -r ' .steps[] | " [\(.status | ascii_upcase)] \(.name)\(if .conclusion then " (\(.conclusion))" else "" end)"' >&2
142+ echo " " >&2
143+ fi
144+ }
145+
114146# Extract and display logs for each job
115147for JOB_ID in $JOB_IDS ; do
116148 JOB_INFO=$( echo " $JOBS " | jq -r " select(.databaseId == $JOB_ID )" )
117149 JOB_NAME=$( echo " $JOB_INFO " | jq -r ' .name' )
118- JOB_STATUS=$( echo " $JOB_INFO " | jq -r ' .conclusion // .status' )
150+ JOB_STATUS=$( echo " $JOB_INFO " | jq -r ' .status' )
151+ JOB_CONCLUSION=$( echo " $JOB_INFO " | jq -r ' .conclusion // "N/A"' )
152+
153+ # Display status: show conclusion if completed, otherwise show status
154+ if [[ " $JOB_STATUS " == " completed" ]]; then
155+ DISPLAY_STATUS=" $JOB_CONCLUSION "
156+ else
157+ DISPLAY_STATUS=" $JOB_STATUS "
158+ fi
119159
120160 echo " " >&2
121161 echo " ════════════════════════════════════════════════════════════" >&2
122- echo " Job: $JOB_NAME (ID: $JOB_ID ) - $JOB_STATUS " >&2
162+ echo " Job: $JOB_NAME (ID: $JOB_ID )" >&2
163+ echo " Status: $DISPLAY_STATUS " >&2
123164 echo " ════════════════════════════════════════════════════════════" >&2
124165
125166 # Suggest local reproduction command
126167 suggest_local_command " $JOB_NAME " >&2
127- echo " " >&2
128-
129- # Fetch logs with retry logic if --wait flag is set
130- MAX_RETRIES=3
131- RETRY_COUNT=0
168+
169+ # Show step-by-step status for in-progress/pending jobs
170+ show_job_steps " $JOB_ID " " $JOB_STATUS "
132171
133- while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
134- # Use gh api to fetch logs (works for individual completed jobs even if run is in progress)
172+ # Try to fetch logs
173+ if [[ " $JOB_STATUS " == " completed" ]]; then
174+ # Completed job - logs should be available
135175 if gh api " /repos/coder/cmux/actions/jobs/$JOB_ID /logs" 2> /dev/null; then
136- break
176+ echo " " >&2
137177 else
138- RETRY_COUNT=$(( RETRY_COUNT + 1 ))
139- if [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ " $WAIT_FOR_LOGS " = true ]; then
140- echo " ⏳ Logs not ready yet, waiting 5 seconds... (attempt $RETRY_COUNT /$MAX_RETRIES )" >&2
141- sleep 5
142- else
143- echo " ⚠️ Could not fetch logs for job $JOB_ID " >&2
144- if [ " $WAIT_FOR_LOGS " = false ]; then
145- echo " Tip: Use --wait flag to retry if logs are still processing" >&2
146- else
147- echo " (logs may have expired or are still processing)" >&2
148- fi
149- break
150- fi
178+ echo " ⚠️ Could not fetch logs for completed job $JOB_ID (logs may have expired)" >&2
179+ echo " " >&2
151180 fi
152- done
181+ else
182+ # In-progress/pending/queued job - logs not yet available
183+ echo " ℹ️ Job is $JOB_STATUS - logs will be available when job completes" >&2
184+ echo " " >&2
185+ fi
153186done
0 commit comments