Skip to content
Open
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
129 changes: 86 additions & 43 deletions scripts/extract_pr_logs.sh
Original file line number Diff line number Diff line change
@@ -1,54 +1,67 @@
#!/usr/bin/env bash
# Extract logs from failed GitHub Actions runs for a PR
# Usage: ./scripts/extract_pr_logs.sh <pr_number_or_run_id> [job_name_pattern] [--wait]
# Extract logs from GitHub Actions runs for a PR (including in-progress jobs)
# Usage: ./scripts/extract_pr_logs.sh <pr_number_or_run_id> [job_name_pattern] [--all]
#
# Examples:
# ./scripts/extract_pr_logs.sh 329 # Latest failed run for PR #329
# ./scripts/extract_pr_logs.sh 329 Integration # Only Integration Test jobs
# ./scripts/extract_pr_logs.sh 329 --wait # Wait for logs to be available
# ./scripts/extract_pr_logs.sh 329 --all # Show all jobs (not just failed)
# ./scripts/extract_pr_logs.sh 18640062283 # Specific run ID

set -euo pipefail

INPUT="${1:-}"
JOB_PATTERN="${2:-}"
WAIT_FOR_LOGS=false
SHOW_ALL_JOBS=false

# Parse flags
if [[ "$JOB_PATTERN" == "--wait" ]]; then
WAIT_FOR_LOGS=true
for arg in "$@"; do
if [[ "$arg" == "--all" ]]; then
SHOW_ALL_JOBS=true
fi
done

# Remove flags from JOB_PATTERN if they were set as second arg
if [[ "$JOB_PATTERN" == "--all" ]]; then
JOB_PATTERN=""
elif [[ "${3:-}" == "--wait" ]]; then
WAIT_FOR_LOGS=true
fi

if [[ -z "$INPUT" ]]; then
echo "❌ Usage: $0 <pr_number_or_run_id> [job_name_pattern]" >&2
echo "❌ Usage: $0 <pr_number_or_run_id> [job_name_pattern] [--all]" >&2
echo "" >&2
echo "Examples:" >&2
echo " $0 329 # Latest failed run for PR #329 (RECOMMENDED)" >&2
echo " $0 329 # Latest failed run for PR #329" >&2
echo " $0 329 Integration # Only Integration Test jobs from PR #329" >&2
echo " $0 329 --all # Show all jobs (not just failed/in-progress)" >&2
echo " $0 18640062283 # Specific run ID" >&2
exit 1
fi

# Detect if input is PR number or run ID (run IDs are much longer)
if [[ "$INPUT" =~ ^[0-9]{1,5}$ ]]; then
PR_NUMBER="$INPUT"
echo "πŸ” Finding latest failed run for PR #$PR_NUMBER..." >&2

# Get the latest failed run for this PR
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 "")

# If --all flag is set or no failures, get latest run regardless of status
if [[ "$SHOW_ALL_JOBS" == true ]]; then
echo "πŸ” Finding latest run for PR #$PR_NUMBER..." >&2
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 "")
else
echo "πŸ” Finding latest failed run for PR #$PR_NUMBER..." >&2
# Get the latest failed run for this PR
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 "")
fi

if [[ -z "$RUN_ID" ]]; then
echo "❌ No failed runs found for PR #$PR_NUMBER" >&2
Comment on lines +44 to 55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Fallback to pending runs never triggers

When invoked with a PR number and no --all flag, the script still looks exclusively for failed runs (select(.state == "FAILURE")) and immediately exits if none are found. This means a PR whose checks are only PENDING/IN_PROGRESS still returns β€œNo failed runs found” and never reaches the new logic that should display step-by-step status for in-progress jobs. The commit message promises automatic handling of in-progress jobs, but that path is unreachable unless the user manually supplies --all or a run ID. Consider falling back to the latest run when no failures exist so the script can surface pending job steps as advertised.

Useful? React with πŸ‘Β / πŸ‘Ž.

echo "" >&2
echo "Current check status:" >&2
gh pr checks "$PR_NUMBER" 2>&1 || true
echo "" >&2
echo "πŸ’‘ Tip: Use --all flag to see logs from any run (not just failed)" >&2
exit 1
fi

echo "πŸ“‹ Found failed run: $RUN_ID" >&2
echo "πŸ“‹ Found run: $RUN_ID" >&2
else
RUN_ID="$INPUT"
echo "πŸ“‹ Fetching logs for run $RUN_ID..." >&2
Expand All @@ -65,12 +78,18 @@ if [[ -z "$JOBS" ]]; then
exit 1
fi

# Filter to failed jobs only (unless specific pattern requested)
if [[ -z "$JOB_PATTERN" ]]; then
FAILED_JOBS=$(echo "$JOBS" | jq -r 'select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED")')
# Filter jobs based on flags and pattern
if [[ -z "$JOB_PATTERN" ]] && [[ "$SHOW_ALL_JOBS" == false ]]; then
# Show failed/timed out/cancelled jobs, OR in-progress/pending jobs if no failures exist
FAILED_JOBS=$(echo "$JOBS" | jq -r 'select(.conclusion == "failure" or .conclusion == "timed_out" or .conclusion == "cancelled")')
IN_PROGRESS_JOBS=$(echo "$JOBS" | jq -r 'select(.status == "in_progress" or .status == "queued" or .status == "pending")')

if [[ -n "$FAILED_JOBS" ]]; then
echo "🎯 Showing only failed jobs (use job_pattern to see others)" >&2
echo "🎯 Showing only failed jobs (use --all to see all jobs)" >&2
JOBS="$FAILED_JOBS"
elif [[ -n "$IN_PROGRESS_JOBS" ]]; then
echo "⏳ No failures yet - showing in-progress/pending jobs (use --all to see all)" >&2
JOBS="$IN_PROGRESS_JOBS"
fi
fi

Expand Down Expand Up @@ -111,43 +130,67 @@ suggest_local_command() {
esac
}

# Show step-by-step progress for in-progress/pending jobs
show_job_steps() {
local job_id="$1"
local job_status="$2"

if [[ "$job_status" == "in_progress" ]] || [[ "$job_status" == "queued" ]] || [[ "$job_status" == "pending" ]]; then
echo "" >&2
echo "πŸ“Š Step-by-step status:" >&2
gh api "/repos/coder/cmux/actions/jobs/$job_id" | jq -r '.steps[] | " [\(.status | ascii_upcase)] \(.name)\(if .conclusion then " (\(.conclusion))" else "" end)"' >&2
echo "" >&2
fi
}

# Extract and display logs for each job
for JOB_ID in $JOB_IDS; do
JOB_INFO=$(echo "$JOBS" | jq -r "select(.databaseId == $JOB_ID)")
JOB_NAME=$(echo "$JOB_INFO" | jq -r '.name')
JOB_STATUS=$(echo "$JOB_INFO" | jq -r '.conclusion // .status')
JOB_STATUS=$(echo "$JOB_INFO" | jq -r '.status')
JOB_CONCLUSION=$(echo "$JOB_INFO" | jq -r '.conclusion // "N/A"')

# Display status: show conclusion if completed, otherwise show status
if [[ "$JOB_STATUS" == "completed" ]]; then
DISPLAY_STATUS="$JOB_CONCLUSION"
else
DISPLAY_STATUS="$JOB_STATUS"
fi

echo "" >&2
echo "════════════════════════════════════════════════════════════" >&2
echo "Job: $JOB_NAME (ID: $JOB_ID) - $JOB_STATUS" >&2
echo "Job: $JOB_NAME (ID: $JOB_ID)" >&2
echo "Status: $DISPLAY_STATUS" >&2
echo "════════════════════════════════════════════════════════════" >&2

# Suggest local reproduction command
suggest_local_command "$JOB_NAME" >&2
echo "" >&2

# Fetch logs with retry logic if --wait flag is set
MAX_RETRIES=3
RETRY_COUNT=0

# Show step-by-step status for in-progress/pending jobs
show_job_steps "$JOB_ID" "$JOB_STATUS"

while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
# Use gh api to fetch logs (works for individual completed jobs even if run is in progress)
# Try to fetch logs
if [[ "$JOB_STATUS" == "completed" ]]; then
# Completed job - logs should be available
if gh api "/repos/coder/cmux/actions/jobs/$JOB_ID/logs" 2>/dev/null; then
break
echo "" >&2
else
RETRY_COUNT=$((RETRY_COUNT + 1))
if [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$WAIT_FOR_LOGS" = true ]; then
echo "⏳ Logs not ready yet, waiting 5 seconds... (attempt $RETRY_COUNT/$MAX_RETRIES)" >&2
sleep 5
else
echo "⚠️ Could not fetch logs for job $JOB_ID" >&2
if [ "$WAIT_FOR_LOGS" = false ]; then
echo " Tip: Use --wait flag to retry if logs are still processing" >&2
else
echo " (logs may have expired or are still processing)" >&2
fi
break
fi
echo "⚠️ Could not fetch logs for completed job $JOB_ID (logs may have expired)" >&2
echo "" >&2
fi
done
else
# In-progress/pending/queued job - GitHub API doesn't provide logs until completion
echo "ℹ️ Job is $JOB_STATUS - logs not available via API until completion" >&2
echo "" >&2

# Show which step is currently running
CURRENT_STEP=$(gh api "/repos/coder/cmux/actions/jobs/$JOB_ID" 2>/dev/null | jq -r '.steps[] | select(.status == "in_progress") | .name' | head -1)
if [[ -n "$CURRENT_STEP" ]]; then
echo "πŸ”„ Currently running: $CURRENT_STEP" >&2
fi

# Construct GitHub URL for viewing live logs in browser
echo "πŸ‘οΈ View live logs: https://github.com/coder/cmux/actions/runs/$RUN_ID/job/$JOB_ID" >&2
echo "" >&2
fi
done
Loading