Introduction
GitHub Actions treats any non-zero exit code as a failed step unless you explicitly tell it otherwise. That is usually the correct behavior, but it becomes confusing when a custom script hides the real failing command, a pipeline masks the wrong exit code, or a tool returns non-zero for a condition you did not intend to make fatal. The fix is to understand exactly which command is exiting and whether that exit should actually fail CI.
Symptoms
- A
run:step ends withProcess completed with exit code 1or another non-zero code - The logs do not clearly show which command triggered the failure
- A shell pipeline looks successful in partial output but still fails the step
- Local execution seems to work while CI stops immediately
Common Causes
- A command inside a shell script fails and
set -eexits the script early - A pipeline returns the wrong status because
pipefailwas not handled consciously - A tool uses a non-zero exit code for warnings, test failures, or partial success
- The script swallows logs or error context before exiting
Step-by-Step Fix
- 1.Run the step with explicit shell behavior
- 2.Make error handling visible rather than relying on whatever shell defaults the environment happens to use.
set -euo pipefail- 1.Capture the exact command that fails
- 2.Break long shell one-liners into named steps or echo progress between commands so the failing action is obvious.
- 3.Handle expected non-zero outcomes deliberately
- 4.If a command is allowed to fail under known conditions, capture and branch on its exit code instead of pretending every non-zero code is success.
./scripts/check.sh || CHECK_EXIT=$?
CHECK_EXIT=${CHECK_EXIT:-0}- 1.**Use
continue-on-erroronly for truly non-critical steps** - 2.Letting a step fail without breaking the job is useful for advisory checks, but dangerous for build or deploy logic.
Prevention
- Use
set -euo pipefailin shell-based CI scripts - Split complex multi-command scripts into smaller observable steps
- Treat non-zero exit codes as intentional API contracts, not accidents
- Log enough context before exiting so the next failure is obvious