Skip to content

How to Debug GitHub Actions Workflow Failures: Common Mistakes and Solutions

I pushed a commit and watched the GitHub Actions workflow fail with a red X. The error message was cryptic: “Process completed with exit code 1”. That was it—no explanation, no stack trace, just a generic failure.

After wasting two hours pushing commits to “fix” things blindly, I realized I needed a systematic approach to debugging GitHub Actions workflows.

The Problem: CI Failures Are Opaque

When a GitHub Actions workflow fails, you get a red X next to your commit. You click it, expand the failed job, and see a wall of text with an exit code. But what actually went wrong?

The issue is that CI environments are isolated—you can’t SSH in, you can’t add breakpoints, and each retry burns your CI minutes. I needed to understand what was actually happening inside the runner.

Step 1: Actually Read the Failure Output

My first mistake was not reading the full log output. GitHub collapses each step by default, and I was skimming instead of expanding.

Workflow run output
Run npm test
npm test
shell: /usr/bin/bash -e {0}
env:
NODE_VERSION: 18
> jest
FAIL src/utils.test.js
✕ should format date correctly (5ms)
● should format date correctly
expect(received).toBe(expected)
Expected: "2026-04-13"
Received: "2026-04-12"
3 | test('should format date correctly', () => {
4 | const result = formatDate(new Date('2026-04-13'));
> 5 | expect(result).toBe('2026-04-13');
| ^
6 | });
7 | });
Tests: 1 failed, 1 total
Time: 0.5s

The error was right there—my date formatting was off by one day due to a timezone issue. The CI runner was in UTC, my local machine was in a different timezone.

Lesson: Always expand the failed step and read the actual error, not just the exit code.

Step 2: Understand Exit Codes

Exit codes are how GitHub Actions determines success or failure:

  • 0 = success
  • Non-zero = failure

I added explicit exit code logging to see what was happening:

debug-exit-codes.yml
steps:
- name: Run tests
run: |
npm test
TEST_EXIT_CODE=$?
echo "Test exit code: $TEST_EXIT_CODE"
exit $TEST_EXIT_CODE

This revealed that my npm test was returning exit code 1, but I also had cases where a pre-step was failing with code 127 (command not found). The exit code tells you the failure category:

Common exit codes
0 - Success
1 - General error
2 - Misuse of shell command
126 - Command not executable
127 - Command not found
128 - Invalid exit code
130 - Script terminated by Ctrl+C
137 - Process killed (out of memory)
139 - Segmentation fault

Step 3: Enable Debug Logging

GitHub Actions has a hidden debug mode that reveals what the runner is actually doing. Set a repository secret:

Repository Settings → Secrets → Actions
ACTIONS_STEP_DEBUG = true

With this enabled, the runner outputs verbose diagnostic information:

Debug output example
##[debug]Evaluating condition for step: 'Run tests'
##[debug]Evaluating: success()
##[debug]Evaluating success:
##[debug]=> true
##[debug]Result: true
##[debug]Starting: Run tests
##[debug]Loading inputs
##[debug]Evaluating: github.event.inputs.environment
##[debug]Evaluating Index:
##[debug]..Evaluating github:
##[debug]..=> Object
##[debug]..Evaluating event:
##[debug]..=> Object

This showed me that my environment variables weren’t being passed correctly—I had a typo in the secret name (API_KEY vs API_KEY_PROD).

Step 4: Test Locally with act

The breakthrough came when I discovered act—a tool that runs GitHub Actions locally using Docker.

Install act
brew install act

Then run your workflow locally:

Run workflow locally
# Simulate push event
act push
# Run specific job
act -j test
# Use specific workflow file
act push -W .github/workflows/ci.yml
# See what would run without executing
act -n push

Running locally revealed the issue immediately: my npm ci was failing because package-lock.json had a conflict. I could interactively debug it:

Interactive debugging with act
# Run with shell access
act --container-architecture linux/amd64 -j build --bash
# Inside container:
ls -la
cat package.json
npm ci --verbose

The --bash flag drops you into a shell inside the container, letting you inspect the environment just like you would on your local machine.

Common Failure Patterns (and Fixes)

After debugging dozens of failed workflows, these patterns kept appearing:

1. Missing or Wrong Dependencies

Fix: Ensure lockfile is committed
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Enable caching
- run: npm ci # Not npm install

The npm ci command requires an exact package-lock.json. If it’s missing or out of sync, the step fails.

2. Missing Secrets

Fix: Reference secrets correctly
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.API_KEY }} # Case-sensitive!
run: |
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/deploy

I wasted hours debugging authentication failures before realizing the secret name in the workflow didn’t match the one in GitHub Settings → Secrets → Actions.

3. Permission Issues

Fix: Add permissions block
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # For OIDC
steps:
- name: Push to registry
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

GitHub Actions introduced explicit permissions. Without them, the GITHUB_TOKEN lacks necessary scopes.

4. Wrong Environment Variables

Fix: Debug environment variables
steps:
- name: Debug env
run: env | sort # Print all environment variables
- name: Run with env
env:
NODE_ENV: test
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: npm test

Environment variable names are case-sensitive. database_urlDATABASE_URL.

The Debugging Workflow I Use Now

  1. Read the logs — Expand the failed step and read the actual error message
  2. Check the exit code — Add echo "Exit code: $?" after commands
  3. Enable debug mode — Set ACTIONS_STEP_DEBUG secret to true
  4. Test locally — Use act to run the workflow on my machine
  5. Fix and verify — Make the fix locally, run act again, then push

This approach has cut my CI debugging time from hours to minutes.

Summary

In this post, I showed how to systematically debug GitHub Actions workflow failures. The key point is that reading logs carefully, enabling debug mode, and using act for local testing saves CI minutes and reveals the actual error.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments