There's a specific skill that separates engineers who debug CI failures in 2 minutes from those who spend 45 minutes. It's not intelligence or experience with a specific tech stack. It's how they read logs.
Junior engineers look at the red text at the bottom of the log. Senior engineers read the log like a story — chronologically, with hypotheses, looking for the first thing that went wrong rather than the last thing that complained.
This post teaches you that framework. It's a skill, not a tool feature.
The Order Senior Engineers Actually Read Logs
When a GitHub Actions workflow fails, resist the urge to scroll to the bottom. Here's the order that produces the fastest diagnosis:
1. Check Which Step Failed
In the GitHub UI, each step has a status icon. Find the first step with a red X — not the last one. If multiple steps failed, the first failure usually caused the rest.
2. Read the Full Log of That Step
Don't just read the last 10 lines. Expand the entire step log and read from the top. CI logs are chronological — the cause comes before the effect.
3. Check Timestamps
Look at the timestamps on the left side of the log. If there's a gap of several minutes between two lines, something hung. If the log output suddenly stops, the process was killed (OOM or timeout).
2026-03-12T10:15:23.123Z Installing dependencies...
2026-03-12T10:15:24.456Z added 847 packages in 1.2s
2026-03-12T10:15:24.789Z Running tests...
2026-03-12T10:22:31.012Z ##[error]The operation was canceled.
^^^^^^^
7 minutes of silence = process hung or OOM
4. Expand Collapsed Groups
GitHub Actions auto-collapses log groups. The actual error might be inside a collapsed group that you never expanded. Click every collapsed section in the failed step — especially "Post" steps and setup steps that look routine.
5. Look for Warnings Before Errors
Warnings that appear before the error often explain the root cause. A deprecation warning about a changed API, a notice about a missing file, a hint about a version mismatch — these are the breadcrumbs.
What "Ephemeral Environment" Means Practically
GitHub Actions runners are ephemeral — each job gets a fresh VM. This has practical debugging implications that junior engineers often miss:
- No state carries over between jobs — if Job A creates a file, Job B can't see it unless you use artifacts or caching
- No state carries over between runs — "it worked yesterday" doesn't help, because yesterday's runner is gone. The environment might have changed
- You can't SSH in after the fact — the VM is destroyed after the run. If you need an interactive session, you have to set it up before the failure (e.g., with
tmateormxschmitt/action-tmate) ubuntu-latestis a moving target — the runner image is updated regularly. What was installed last week might not be installed this week
This means your debugging approach must be log-first. You can't attach a debugger. You can't inspect the filesystem. You only have what's in the log output.
The Most Common Junior Mistake
Fixing the symptom shown in the last 5 lines instead of the actual failure higher up.
Example:
# Line 247: WARNING: Could not find package 'sharp' binary, falling back to build from source
# Line 248: npm warn deprecated...
# ...50 lines of C++ compilation output...
# Line 312: error: 'vips/vips8' file not found
# Line 313: npm ERR! code 1
# Line 314: npm ERR! sharp@0.33.2 install failed
# A junior engineer sees "sharp install failed" and googles that.
# A senior engineer sees "vips/vips8 file not found" and knows
# the system dependency (libvips) is missing from the runner.
The fix isn't to change your sharp version. It's to install libvips-dev on the runner before npm install:
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libvips-dev
- name: Install npm packages
run: npm ci
A Repeatable Debugging Framework
Use this framework every time a CI job fails. It works for any CI platform, not just GitHub Actions:
Step 1: Reproduce
Can you reproduce the failure consistently? Re-run the workflow. If it passes on re-run, you have a flaky failure (a different debugging path — see our article on flaky tests vs flaky pipelines).
Step 2: Isolate
Which step failed? Is it a build step, test step, or deploy step? Can you reproduce it locally? If npm test passes locally but fails in CI, the difference is the environment, not the code.
Step 3: Read Chronologically
Open the full log of the failed step. Read from the first line to the error. Note:
- The first warning or error message
- Any version numbers that were printed
- Any timeout or resource indicators
- Any differences from what you expected
Step 4: Form a Hypothesis
Based on what you read, what do you think went wrong? Common hypotheses:
- "A dependency version changed" → check lockfile and cache
- "The runner environment changed" → check runner version and OS
- "A secret is missing" → check secret scope and fork status
- "We ran out of memory" → check for exit code 137
Step 5: Verify
Test your hypothesis with a targeted change. Don't change 5 things at once — change one, re-run, and see if it's fixed.
Quick-Reference: Exit Codes
| Exit Code | Meaning | Common Cause in CI |
|---|---|---|
| 0 | Success | — |
| 1 | General error | Script returned error, test failure |
| 2 | Misuse of shell | Syntax error in shell script |
| 124 | Timeout | Command exceeded timeout duration |
| 126 | Not executable | Missing execute permission on script |
| 127 | Command not found | Binary not installed on runner |
| 128+N | Signal N | Process killed by signal |
| 137 | SIGKILL (128+9) | OOM killer terminated the process |
| 143 | SIGTERM (128+15) | Job cancelled or timeout |
This is a skill worth building manually — you'll need it for cases nothing catches automatically. But for the repetitive 80% of failures, this is exactly the triage Daxtack does in seconds so you can spend the reading-logs skill on the genuinely novel 20%.