GitHub Actions Error: 'Process completed with exit code 1.' — Cause, Fix, and Troubleshooting Guide
Fix GitHub Actions 'Error: Process completed with exit code 1.' — find the real error above the generic line: failing tests, missing deps, secrets, and set -e.
- #automation
- #troubleshooting
- #github-actions
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
Error: Process completed with exit code 1. is the single most common — and least informative — line in a red GitHub Actions run. It is not the error. It is the runner reporting that the shell it spawned for a run: step exited non-zero, which fails the step. The actual cause is always printed above this line, in the step’s own output:
Run pytest -q
............................F.... [ 42%]
=================================== FAILURES ===================================
____________________________ test_parse_timestamp _____________________________
assert parse("2026-13-01") is None
E ValueError: month must be in 1..12
tests/test_parse.py:31: ValueError
1 failed, 46 passed in 3.12s
Error: Process completed with exit code 1.
The ValueError two lines up is the real failure; exit code 1 is just the messenger. Every fix starts by scrolling up to the last command’s output, not by reacting to the generic line. A step fails on any non-zero exit, and by default the job stops there.
Symptoms
- The job is red and the failing step ends with
Error: Process completed with exit code 1. - Expanding the step reveals a test failure, compiler error, linter finding, or
command not foundimmediately above. - The same command passes locally but fails on the runner (or vice versa) — an environment, dependency, or secret difference.
- A multi-command
run:block fails partway; because ofset -e, later commands never execute. - The annotation at the top of the run only shows the generic line, with no detail, if you do not expand the step.
Common Root Causes
1. A test, lint, or build command genuinely failed
The most frequent case: pytest, eslint, go build, tsc, or npm run build returned non-zero because something is actually broken. Exit code 1 faithfully reports a real failure.
2. A missing dependency or tool
A command is not installed on the runner, or an install step was skipped/failed, so a later command errors with command not found (which itself exits 127, but a wrapping script often normalizes it to 1).
3. A missing or misnamed secret / environment variable
A step references ${{ secrets.API_TOKEN }} that is empty (secret not set, or not exposed to a fork PR), so the command it feeds fails — an auth 401, an empty variable, or a script that exit 1s on the missing value.
4. set -e in the run block
GitHub’s default shell for run: is bash -e, so the first failing command aborts the whole step even if you expected later commands to continue. A non-critical grep that finds nothing (exit 1) can sink the entire step.
5. Wrong working directory or missing file
The step assumes a path that does not exist on the runner (a build artifact not produced, a cd into a directory that was never created), so a script exits with “No such file or directory.”
6. A script explicitly returns 1
Your own script, Makefile target, or a policy check calls exit 1 on a condition you defined — a failed assertion, a coverage threshold, a lint gate.
How to Diagnose
Expand the failing step and read the last command’s output — the real error is always directly above the generic line. If the log is truncated, re-run with debug logging enabled:
# Re-run the failed job with step debug logging (from your machine)
gh run rerun <run-id> --debug
# or set the repo/secret variable ACTIONS_STEP_DEBUG=true and re-run
Reproduce the exact command locally, in a clean checkout, to separate “broken code” from “broken CI environment”:
git clean -fdx
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt
pytest -q
tests/test_parse.py:31: ValueError: month must be in 1..12
1 failed, 46 passed in 2.98s
If it passes locally but fails in CI, the difference is the environment. Add set -x to the run block to see exactly which command exits non-zero and with what inputs:
- name: build
run: |
set -x
echo "PWD=$PWD"
echo "NODE=$(node -v 2>&1)"
npm ci
npm run build
+ echo PWD=/home/runner/work/app/app
PWD=/home/runner/work/app/app
+ node -v
NODE=v18.20.4
+ npm run build
sh: 1: vite: not found
Error: Process completed with exit code 1.
Here set -x reveals that npm ci did not install vite (dev dependency skipped because NODE_ENV=production), which the generic line alone would never tell you.
Confirm a suspected empty secret without printing its value:
- name: check secret present
run: |
if [ -z "${API_TOKEN}" ]; then echo "API_TOKEN is EMPTY"; exit 1; fi
echo "API_TOKEN length: ${#API_TOKEN}"
env:
API_TOKEN: ${{ secrets.API_TOKEN }}
API_TOKEN is EMPTY
Error: Process completed with exit code 1.
For a multi-command block, add continue-on-error: true temporarily to a diagnostic step so the job runs far enough to surface downstream problems — then remove it, since it masks real failures.
Fixes
Fix the underlying command the log points to — the exit code is not the bug. Correct the failing test, install the missing tool, or set the absent secret:
# Ensure dev dependencies are installed for the build
- run: npm ci --include=dev
- run: npm run build
If a benign non-zero command is sinking the step under set -e, neutralize just that command instead of disabling error handling for the whole block:
- name: count warnings (must not fail the step)
run: |
set -euo pipefail
count=$(grep -c WARN build.log || true) # grep exits 1 on no match
echo "warnings=$count"
Set the working directory explicitly rather than relying on a cd that may run in the wrong place:
- name: test
working-directory: ./service
run: pytest -q
Add or expose the missing secret, and remember that secrets are not passed to workflows triggered by pull requests from forks — gate those steps or use pull_request_target deliberately:
gh secret set API_TOKEN --body "$(cat token.txt)"
gh secret list
For your own exit 1 policy gates, print a clear reason above the exit so future runs are self-explanatory:
if [ "$coverage" -lt 80 ]; then
echo "::error::coverage ${coverage}% is below the 80% threshold"
exit 1
fi
What to Watch Out For
- The generic line is never the diagnosis. If a bug report or dashboard only shows
Process completed with exit code 1, the real error was one scroll up — capture that, not this. bash -eis the default shell. A pipeline likecmd | tee logcan hide the real exit status; addset -o pipefailso the step fails for the right reason.continue-on-error: trueis a triage tool, not a fix. Left in place, it turns a broken step green and lets bad artifacts flow downstream.- Secrets are empty (not just hidden) in forked-PR runs. A step that works on
pushcan fail on external contributors’ PRs for exactly this reason. exit 0at the end of a script silently swallows every earlier failure. Do not “fix” a red job by appending it — that hides the problem instead of solving it.
Related Guides
- GitHub Actions: concurrency cancelled in-progress run
- GitHub Actions Reusable Workflows for Automation at Scale
- Git Hook Error: pre-commit hook not found or not executable
Fixed it? Get 500 Automation & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Did this fix your issue?
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.