Skip to content

Instantly share code, notes, and snippets.

@ericboehs
Last active April 26, 2025 15:17
Show Gist options
  • Save ericboehs/ce06ffcaefb1cd4742cc5b6961ffd655 to your computer and use it in GitHub Desktop.
Save ericboehs/ce06ffcaefb1cd4742cc5b6961ffd655 to your computer and use it in GitHub Desktop.
πŸ”Ž Quickly list and inspect all attempts for a job in a GitHub Actions run, with options to output full links or job IDs. Handy for debugging retries.
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<EOF
Usage: $(basename "$0") [options] <github-actions-run-url>
Options:
--only-links Only print the job links
--only-job-ids Only print the job IDs
--help Show this help message
Examples:
$(basename "$0") https://github.com/org/repo/actions/runs/123456789/job/12345
$(basename "$0") --only-links https://github.com/org/repo/actions/runs/123456789/job/12345
$(basename "$0") --only-job-ids https://github.com/org/repo/actions/runs/123456789/job/12345
EOF
exit 0
}
# --- Parse flags ---
ONLY_LINKS=false
ONLY_JOB_IDS=false
if [[ $# -eq 0 ]]; then
usage
fi
if [[ "$1" == "--help" ]]; then
usage
fi
if [[ $# -eq 2 ]]; then
case "$1" in
--only-links) ONLY_LINKS=true ;;
--only-job-ids) ONLY_JOB_IDS=true ;;
*) usage ;;
esac
INPUT_URL="$2"
elif [[ $# -eq 1 ]]; then
INPUT_URL="$1"
else
usage
fi
# Check for GNU grep
if command -v ggrep >/dev/null 2>&1; then
GREP="ggrep"
elif grep --version 2>&1 | grep -q "GNU"; then
GREP="grep"
else
echo "❌ GNU grep (supporting -P) is required. Install 'ggrep' (e.g., via 'brew install grep') or use a system with GNU grep."
exit 1
fi
# Parse owner, repo, and run ID from the URL
OWNER=$(echo "$INPUT_URL" | "$GREP" -oP 'github\.com/\K[^/]+')
REPO=$(echo "$INPUT_URL" | "$GREP" -oP 'github\.com/[^/]+/\K[^/]+')
RUN_ID=$(echo "$INPUT_URL" | "$GREP" -oP 'runs/\K[0-9]+')
if [[ -z "$OWNER" || -z "$REPO" || -z "$RUN_ID" ]]; then
echo "❌ Failed to parse owner/repo/run_id from URL."
exit 1
fi
# Get latest attempt number
LATEST_ATTEMPT=$(gh api "repos/${OWNER}/${REPO}/actions/runs/${RUN_ID}" --jq '.run_attempt')
# Function to fetch Test job info
fetch_test_job() {
local owner=$1
local repo=$2
local run_id=$3
local attempt=$4
local path=$5
gh api "repos/${owner}/${repo}/actions/runs/${run_id}${path}/jobs" --jq '
.jobs[]? |
select(.name == "Test") |
{
id: .id,
url: .html_url,
conclusion: .conclusion
}
' 2>/dev/null | jq -r --arg attempt "$attempt" '
if . == null then
"\($attempt)\t❓ No Test Job\t-\t-"
else
"\($attempt)\t\(.conclusion | ascii_downcase | if test("success") then "βœ… Passed" else "❌ Failed" end)\t\(.url)\t\(.id)"
end
'
}
# Fetch all attempts
ATTEMPTS_JSON=$(curl -s "https://github.com/${OWNER}/${REPO}/actions/runs/${RUN_ID}/new_attempts?current_attempt_number=${LATEST_ATTEMPT}&retry_blankstate=false" \
-H "X-Requested-With: XMLHttpRequest" \
-H "Accept: text/html" |
pup 'a json{}')
if [[ -z "$ATTEMPTS_JSON" ]]; then
echo "❌ Failed to fetch attempt data."
exit 1
fi
ATTEMPTS=$(echo "$ATTEMPTS_JSON" | jq -r '.[].children[1].children[0].text | capture("Attempt #(?<n>\\d+)").n')
# Add latest attempt manually if missing
ATTEMPTS=$(echo -e "$LATEST_ATTEMPT\n$ATTEMPTS" | sort -nr | uniq)
# Collect all output
OUTPUT_LINES=()
for ATTEMPT in $ATTEMPTS; do
if [[ "$ATTEMPT" == "$LATEST_ATTEMPT" ]]; then
path=""
else
path="/attempts/$ATTEMPT"
fi
result=$(fetch_test_job "$OWNER" "$REPO" "$RUN_ID" "$ATTEMPT" "$path")
OUTPUT_LINES+=("$result")
done
# Display
if [[ "$ONLY_LINKS" == true ]]; then
printf "%s\n" "${OUTPUT_LINES[@]}" | awk -F'\t' '{print $3}' | grep -v '^-$'
elif [[ "$ONLY_JOB_IDS" == true ]]; then
printf "%s\n" "${OUTPUT_LINES[@]}" | awk -F'\t' '{print $4}' | grep -v '^-$'
else
printf "%s\n" "${OUTPUT_LINES[@]}" | awk -F'\t' '{printf "%2s %-10s %s\n", $1, $2, $3}'
fi
@ericboehs
Copy link
Author

ericboehs commented Apr 25, 2025

gh-job-attempts

A lightweight utility to list a specific GitHub Actions job across all attempts of a given workflow run.

✨ Features:

  • Works from any GitHub Actions job URL (.../actions/runs/<run_id>/job/<job_id>)
  • Dynamically detects the job name from the provided job ID (no hardcoded names)
  • Shows:
    • Attempt number (latest + retries)
    • βœ… Passed / ❌ Failed status
    • Direct link to the matching job logs for each attempt
  • Supports:
    • --only-links β€” Output just the job URLs
    • --only-job-ids β€” Output just the job IDs
  • Proper column alignment
  • Super fast: uses GitHub CLI gh and lightweight curl scraping
  • Handles retries and multiple attempts cleanly

πŸ“¦ Requirements

  • gh (GitHub CLI)
  • jq
  • pup (HTML parser)
  • GNU grep:
    • Either ggrep (macOS via Homebrew: brew install grep)
    • Or native GNU grep on Linux

Note: If GNU grep is not found, the script will exit with a helpful message.


πŸš€ Usage

# Basic usage
gh-job-attempts <github-actions-job-url>

# Output only the job URLs (for piping into other scripts)
gh-job-attempts --only-links <github-actions-job-url>

# Output only the job IDs
gh-job-attempts --only-job-ids <github-actions-job-url>

Example:

gh-job-attempts "https://github.com/ericboehs/sprintosaurus-sinatra/actions/runs/14669964121/job/41178086659"

# Output:
 9  βœ… Passed  https://github.com/.../job/41184112808
 8  ❌ Failed  https://github.com/.../job/41178681373
 7  βœ… Passed  https://github.com/.../job/41178086659
 6  βœ… Passed  https://github.com/.../job/41177495183
 ...

⚑ Installation Tip

Put the script into your ~/bin/ or any folder in your $PATH, and make it executable:

chmod +x ~/bin/gh-job-attempts

Then run it anywhere!


⚠️ Caveats

  • Private GitHub API:
    This script scrapes from GitHub’s internal /new_attempts endpoint, which is not officially supported and could change or break without notice.

License

MIT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment