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 with Process completed with exit code 1 or 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 -e exits the script early
  • A pipeline returns the wrong status because pipefail was 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. 1.Run the step with explicit shell behavior
  2. 2.Make error handling visible rather than relying on whatever shell defaults the environment happens to use.
bash
set -euo pipefail
  1. 1.Capture the exact command that fails
  2. 2.Break long shell one-liners into named steps or echo progress between commands so the failing action is obvious.
  3. 3.Handle expected non-zero outcomes deliberately
  4. 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.
bash
./scripts/check.sh || CHECK_EXIT=$?
CHECK_EXIT=${CHECK_EXIT:-0}
  1. 1.**Use continue-on-error only for truly non-critical steps**
  2. 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 pipefail in 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