keepcalculator

06 Numbers and Powers

Group Failed GitHub Actions Runs by Shared Errors

Card No. 06·4548 · Updated · Standard formula, shown below

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 repo scope, or a fine-grained token with Actions: Read permission. 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 raw curl.

Step-by-Step

  1. 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 with gh auth login and then gh auth token to get the token.
  2. 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.yml in the repository owner/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/runs instead of the workflow-specific endpoint.

  3. Extract the job IDs for failed runs. The runs list gives you id for each run. You need the job IDs to fetch the logs. Use jq to 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.

  4. 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 conclusion field to be failure. Extract the job IDs with jq and append them to a file, say job_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.txt

    This may take a while if you have many runs; consider adding a small sleep to respect rate limits.

  5. 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.txt

    This gives you a folder of log files per job.

  6. Extract error messages from the logs. The error lines are usually those containing Error:, error:, ##[error], or Failed to. Use grep to 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.

  7. 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.

  8. Group by exact match. Use sort and uniq -c to 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.

  9. Optionally, cluster by similarity. If your errors vary slightly, use a simple fuzzy matching approach in Python (e.g., using difflib or the rapidfuzz library) 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.
  10. Review the groups. Open error_groups.txt and 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:read or repo will 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=100 and follow the Link header 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 use gh api which 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_run event. If you want to analyze failures as they happen, trigger a separate workflow on workflow_run with conclusion: 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 1 between 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. or The 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 jq are Unix-oriented. Use Git Bash or WSL, or rewrite the script in Python using requests and zipfile.

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

The whole Numbers and Powers drawer →