When a GitHub Actions workflow fails across multiple runs, the causes are often repeated: a missing secret, a flaky dependency, a syntax error, or an environment issue. Grouping failed runs by their shared error messages lets you spot patterns, prioritize fixes, and avoid re-diagnosing the same problem. This guide shows you how to extract error messages from the GitHub API and cluster them using a simple script, so you can see at a glance which failures are the same root cause.
What You’ll Need
- A GitHub repository with at least a few failed workflow runs.
- A personal access token (classic) with
reposcope, or a fine-grained token withActions: Readpermission. You can create one under GitHub Settings → Developer settings → Personal access tokens. - Basic familiarity with the command line and either
jq(for JSON parsing) or Python 3. - Optionally, a tool like
gh(GitHub CLI) if you prefer it over rawcurl.
Step-by-Step
- Set up your environment. Open a terminal and export your token as an environment variable to avoid typing it each time:
export GH_TOKEN=your_token_here. If you’re using the GitHub CLI, authenticate withgh auth loginand thengh auth tokento get the token. - List recent failed runs for a workflow. Use the GitHub API to fetch runs for a specific workflow file. For example, to get the last 30 runs of a workflow named
ci.ymlin the repositoryowner/repo:curl -s -H "Authorization: token $GH_TOKEN" \ "https://api.github.com/repos/owner/repo/actions/workflows/ci.yml/runs?per_page=30&status=failure"
Save the output to a file:
> runs.json. If you want all failed runs across all workflows, use/actions/runsinstead of the workflow-specific endpoint. - Extract the job IDs for failed runs. The runs list gives you
idfor each run. You need the job IDs to fetch the logs. Usejqto pull them:jq -r '.workflow_runs[] | .id' runs.json > run_ids.txt
This gives you a list of run IDs. For each run, you’ll fetch its jobs.
- Fetch the jobs for each failed run. For each run ID, call the jobs endpoint:
curl -s -H "Authorization: token $GH_TOKEN" \ "https://api.github.com/repos/owner/repo/actions/runs/RUN_ID/jobs"
You want the
conclusionfield to befailure. Extract the job IDs withjqand append them to a file, sayjob_ids.txt. A simple loop in bash:while read run_id; do curl -s -H "Authorization: token $GH_TOKEN" \ "https://api.github.com/repos/owner/repo/actions/runs/$run_id/jobs" | \ jq -r '.jobs[] | select(.conclusion=="failure") | .id' done < run_ids.txt > job_ids.txtThis may take a while if you have many runs; consider adding a small
sleepto respect rate limits. - Download the logs for each failed job. The logs endpoint returns a zip file. For each job ID, download and extract:
while read job_id; do curl -s -L -H "Authorization: token $GH_TOKEN" \ "https://api.github.com/repos/owner/repo/actions/jobs/$job_id/logs" -o "log_$job_id.zip" unzip -o "log_$job_id.zip" -d "logs_$job_id" done < job_ids.txtThis gives you a folder of log files per job.
- Extract error messages from the logs. The error lines are usually those containing
Error:,error:,##[error], orFailed to. Usegrepto pull them into a single file per job:grep -rhiE "(error|failed|exception)" logs_$job_id/ > errors_$job_id.txt
Do this for all jobs, or combine into one big file with job IDs as prefixes.
- Normalize the error messages. To group similar errors, you need to strip variable parts like timestamps, IP addresses, or random numbers. For example, replace digits with
#and remove anything in parentheses that looks like a UUID. A simple Python script can do this:import re, glob for f in glob.glob('errors_*.txt'): with open(f) as fh: for line in fh: line = re.sub(r'\b\d+\b', '#', line) line = re.sub(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', 'UUID', line) print(line.strip())Save the normalized lines to
normalized_errors.txt. - Group by exact match. Use
sortanduniq -cto count identical lines:sort normalized_errors.txt | uniq -c | sort -rn > error_groups.txt
This gives you a ranked list of error messages with counts. The top entries are your most common failures.
- Optionally, cluster by similarity. If your errors vary slightly, use a simple fuzzy matching approach in Python (e.g., using
difflibor therapidfuzzlibrary) to group lines that are similar. For example, group lines with a similarity ratio above 0.8. This helps when the same error has different numbers or paths. - Review the groups. Open
error_groups.txtand look at the top groups. For each, click into one of the failed runs in the GitHub UI to see the full context and confirm the root cause. Then create a GitHub issue or a checklist to track fixes.
Common Mistakes
- Using the wrong token scope. A token without
actions:readorrepowill return 404 or 403. Double-check your token permissions. - Not handling pagination. The API returns up to 30 runs per page. If you have more than 30 failed runs, you’ll miss some. Use
?per_page=100and follow theLinkheader for next pages. - Mixing up run IDs and job IDs. The logs endpoint expects a job ID, not a run ID. If you use a run ID, you’ll get a 404.
- Forgetting to normalize errors. If you skip the normalization step, you’ll get many groups that are actually the same error with different numbers, making the grouping useless.
- Downloading logs for successful jobs. Only fetch logs for jobs with
conclusion: failure, otherwise you waste time and API calls.
Tips
- Use the GitHub CLI for simplicity. Instead of raw
curl, you can usegh apiwhich handles authentication automatically. For example:gh api repos/owner/repo/actions/runs?per_page=30. - Cache the downloaded logs. If you run the script multiple times, you don’t need to re-download logs for the same job IDs. Keep the zip files and skip if they already exist.
- Focus on the first failing step. A job may have multiple errors; the root cause is usually the first error in the log. When grouping, consider only the first error line per job to avoid noise.
- Automate with a scheduled workflow. You can turn this into a GitHub Action that runs weekly and posts a summary issue. Use a cron trigger and the same script.
- Use the
workflow_runevent. If you want to analyze failures as they happen, trigger a separate workflow onworkflow_runwithconclusion: failure.
Troubleshooting
- Getting 404 or 403 errors. Check that your token has the correct scopes. For a private repository, ensure the token belongs to a user with access. Also verify the repository name is spelled correctly (case-sensitive).
- Logs download as empty or HTML. This usually means the job ID is wrong or the token lacks permission. Confirm the job ID is from the jobs endpoint, not the runs endpoint.
- Rate limit exceeded. The GitHub API allows 5,000 requests per hour for authenticated users. If you have many runs, add a
sleep 1between requests or use conditional requests with ETags. - No errors found in logs. Some failures are caused by a step being skipped or a timeout. Look for lines like
##[error]Process completed with exit code 1.orThe operation was canceled.Also check the raw logs for the step that failed. - Grouping gives too many unique errors. Increase the similarity threshold in your clustering script, or normalize more aggressively (e.g., replace all paths with
PATH). - Script fails on Windows. The bash loops and
jqare Unix-oriented. Use Git Bash or WSL, or rewrite the script in Python usingrequestsandzipfile.
Frequently Asked Questions
Can I group failures without downloading logs?
Yes, you can use the conclusion and name fields from the jobs endpoint to group by job name, but that only groups by the job that failed, not the actual error. For true error grouping, logs are necessary.
How do I get the workflow ID instead of the file name?
Use the /actions/workflows endpoint to list workflows and find the id for the file you want. Then use that ID in the runs URL.
What if my errors are in the setup steps, not the main script?
Grouping by error message still works; you’ll see common setup errors like “Unable to locate executable file” or “Failed to download”. Address those as a separate group.
Can I do this with the GitHub web UI instead?
There’s no built-in grouping feature, but you can filter runs by status and click through each. The script saves time when you have many failures.
How do I handle errors that are multi-line?
Multi-line errors are tricky. Consider joining lines that start with whitespace to the previous line, or just take the first line of each error block. The normalization step can help.
Is there a way to automatically create issues for each group?
Yes, after grouping, you can use the GitHub API to create an issue for each unique error message, but be careful not to spam. Better to create one issue per top group manually or with a script that checks for duplicates.
Related
Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.
More cards in this drawer
- Mac Start Up Automatically After Power Failure?
- Expose Self-Hosted Service Securely Without Cloudflare Tunnel
- Nano Delivery for Functional Foods: What It Can and Can’t Do4.00
- Disable Apple Watch Turn-by-Turn Vibration While Driving
- Convert Play FLAC in iTunes: Full Guide
- Soda Foundation Charter: What It Means