GitHub Actions

Why Your GitHub Actions Workflow Fails With No Clear Error (And How to Actually Find It)

GitHub Actions showing a red X with no useful error? Walk through the exact diagnostic steps to find hidden failures — from debug logging to OOM kills that look like random crashes.

D

Daxtack Team

Engineering

9 min read

You push a commit. The workflow runs. A red X appears. You click into the logs expecting an obvious error message — and there isn't one. The step just… stopped. Or maybe there's a vague "Process completed with exit code 1" with no context. You re-run the workflow. It passes. You move on, slightly unsettled.

This is one of the most common and most frustrating experiences in GitHub Actions. The pipeline failed, but it won't tell you why. And if you don't understand why, you can't prevent it from happening again.

This post is a diagnostic walkthrough — not opinions, not theory. Step-by-step, here's how to find the actual failure when GitHub Actions gives you nothing.

Why This Happens More Than You'd Expect

GitHub Actions runners are ephemeral. Each job gets a fresh virtual machine that's destroyed after the run. This means:

  • No persistent state to inspect — you can't SSH in after the fact (unless you set up tmate or similar)
  • Collapsed log groups hide critical output — GitHub auto-collapses log sections, and the error might be in a collapsed group you never expanded
  • Silent step skips — if a step's if condition evaluates to false, it's skipped silently. If you expected it to run, this looks like nothing happened
  • OOM kills look like random failures — when the Linux OOM killer terminates your process, you often get just "Killed" or exit code 137 with no stack trace
  • Timeouts produce misleading output — a 6-hour job timeout kills the process mid-output, so the last line in the log is whatever happened to be printing at that moment

Step 1: Enable Debug Logging

GitHub Actions has two built-in debug modes that most developers never use:

# Set these as repository secrets or re-run with debug enabled
ACTIONS_RUNNER_DEBUG: true    # Verbose runner-level diagnostics
ACTIONS_STEP_DEBUG: true      # Step-level debug output

You can also enable these per-run: click "Re-run all jobs" → check "Enable debug logging". This surfaces internal Actions machinery — setup steps, environment resolution, path manipulation — that's hidden by default.

What to look for: Runner debug logs will show you if the runner itself had issues (disk space, network, permissions). Step debug logs show the exact commands executed and their full output, including parts that are normally suppressed.

Step 2: Read the Full Log, Not Just the Red Text

The most common mistake: scrolling to the red text and reading only the last 5 lines. In CI logs, the actual failure is almost never in the last 5 lines. It's usually 30-200 lines above the final error.

Here's why: when a build step fails, downstream steps often produce their own errors. A missing dependency causes a compilation error, which causes a test import error, which causes a test runner crash. The red text shows the test runner crash — the root cause is the missing dependency, hundreds of lines earlier.

Practical approach:

  1. Download the raw log (gear icon → "Download log archive" on the workflow run page)
  2. Open in a text editor and search for the first occurrence of error, Error, ERROR, fatal, or FAILED
  3. Read the 20 lines before that first error — that's where the cause usually is

Step 3: Check for Resource Limits

GitHub-hosted runners have hard resource limits that are poorly documented and produce terrible error messages:

ResourceLimit (Standard Runner)Failure Signature
RAM7 GB"Killed", exit code 137, or just silence
Disk14 GB"No space left on device" (ENOSPC)
Job timeout6 hours"The job running on runner … has exceeded the maximum execution time"
Step timeoutNone by defaultHangs forever unless you set timeout-minutes

OOM kills are the sneakiest. The Linux kernel's OOM killer sends SIGKILL to the process using the most memory. There's no stack trace, no error handler, no graceful shutdown. Your process just disappears.

How to check: Add a monitoring step that runs in parallel:

- name: Monitor resources
  run: |
    while true; do
      echo "=== $(date) ==="
      free -h
      df -h /
      echo "---"
      sleep 30
    done &

If memory usage climbs to 6.5+ GB right before the failure, you've found your culprit.

Step 4: Investigate Silent Step Skips

Every step with an if condition can be silently skipped. Check your workflow for conditional steps:

- name: Deploy
  if: github.ref == 'refs/heads/main'
  run: ./deploy.sh

If you're on a feature branch, this step is skipped with zero indication that it was supposed to run. In the logs, skipped steps show a grey dash (—) instead of a green check or red X — easy to miss.

Common gotcha: if: success() is the default condition. If a previous step failed and you didn't set if: always(), your cleanup or reporting steps get skipped silently.

Step 5: Verify Runner Environment Assumptions

Your workflow might be making assumptions about the runner environment that aren't true:

  • Tool versions changeubuntu-latest updates its preinstalled tools regularly. The Node version, Python version, or Go version you assumed was there might have changed
  • PATH differences — tools installed with npm install -g might not be on the PATH in subsequent steps
  • Working directory resets — each run step starts in the workspace root, not where the previous step left off (unless you use working-directory)

Add a diagnostic step at the start of your workflow:

- name: Environment info
  run: |
    echo "Node: $(node -v)"
    echo "npm: $(npm -v)"
    echo "Python: $(python3 --version)"
    echo "OS: $(cat /etc/os-release | head -2)"
    echo "Disk: $(df -h / | tail -1)"
    echo "RAM: $(free -h | grep Mem)"
    echo "PWD: $(pwd)"
    echo "PATH: $PATH"

Step 6: Check for Workflow-Level Issues

Sometimes the problem isn't in a step — it's in the workflow configuration itself:

  • Concurrency conflicts — if you have concurrency groups, a newer run might cancel your current one. The cancellation shows as a failure with no error
  • Required checks mismatch — your branch protection requires a check named "build" but your workflow renamed the job to "ci". The check never runs, so it never passes
  • Trigger mismatch — your workflow triggers on push to main but you're on a PR branch. The workflow simply doesn't run, which looks like it "failed" if you're waiting for a status check
# Check for concurrency cancellation
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # This cancels running jobs!

The Debugging Checklist

Bookmark this for next time your workflow fails with no clear error:

  1. ☐ Re-run with debug logging enabled
  2. ☐ Download and read the full raw log (not just the red text)
  3. ☐ Search for the first error, not the last
  4. ☐ Check exit code — 137 = OOM, 143 = SIGTERM, 124 = timeout
  5. ☐ Look for skipped steps (grey dash in the UI)
  6. ☐ Verify concurrency settings aren't cancelling the run
  7. ☐ Check if ubuntu-latest updated and broke an assumption
  8. ☐ Add resource monitoring to spot memory/disk pressure
  9. ☐ Check GitHub's status page — sometimes it's just an outage

This is a five-minute process once you know what to look for. Daxtack automates the same triage by parsing the full log and surfacing the actual failing line, so you're not doing this by hand every time.

GitHub ActionsDebuggingCI/CDWorkflow FailuresNo Error MessageDevOpsPipeline Debugging

Debug CI/CD failures in 30 seconds

Daxtack uses AI to automatically analyze your build logs, find the root cause, and suggest fixes — right in your pull request.

Related Articles