GitHub Actions Process completed with exit code 1, and what stopped
GitHub Actions Process completed with exit code 1 is the runner reporting the exit status of the script it just ran, with no opinion about which line produced it. Because every run step is invoked with errexit on, that status is usually the status of the first command that returned non-zero, and the number is a better clue than the sentence is.

What this error means
A run step ends red immediately after a command you did not consider a failure. Common examples are a grep that matched nothing, a test expression that was false, a diff that found a difference, or a linter that found one warning. The log shows the command, shows its ordinary output or no output at all, and then the runner's one-line report. Everything after that command in the same block did not run, and nothing in the log says so.
Run grep "WARN" build.log
Error: Process completed with exit code 1.The message is a number, and the number is a fingerprint
The runner does one thing here. It executes the script, asks for the process exit code without requiring it to be zero, and if the code is not zero it writes a single line naming the code and marks the step failed. There is no analysis, no parsing of the output and no attempt to identify the offending line. Whatever number appears in the sentence came out of the process.
Under errexit the shell exits with the status of the command that failed, so the number in the message is the failing tool's own code. That makes 1 informative rather than generic: it is the code that grep returns when it matches nothing, that test and [ return when the expression is false, that false returns by definition, and that a large family of linters and formatters return when they found something to report. A missing binary would have given you 127, a signal would have given you something above 128.
So the productive question is not why the step failed but which command exited 1, and the answer is almost always the last command that produced output before the message. That is the line to decide about, and there are only three reasonable decisions: it is a real failure, it is a test you meant to branch on, or it is noise you are prepared to ignore.
Common causes
A command used a non-zero exit to report an ordinary result
grep returns 1 when nothing matched, diff returns 1 when files differ, and many linters return 1 when they have findings. None of these are errors in the sense you meant, and all of them end the step, because errexit does not know the difference between a report and a failure.
The failing command was on its own line rather than in a test
The same command is harmless in the test of an if, in a while condition, after a !, or on the left of && or ||. Standing alone it is an ordinary command in the list and errexit ends the script with its status.
A multi-line block stops partway and nothing says so
A run block is one script. When errexit ends it at line three, lines four onwards never execute and no message marks the boundary, so the log looks as though the last command that printed anything is the last command in the step.
A helper function ran in an exempt context
The Bash manual is explicit that commands inside a function called from an exempt context are not affected by errexit either. In our experience this produces the opposite complaint, a step that stays green through a failure, and it is worth knowing because it means the exemption list is transitive.
How to fix it
Find the command, not the step
- Scroll to the last command in the block that produced output before the runner's message.
- Run that command locally and check its exit status with
echo $?. - Decide which of three it is: a real failure, a test you meant to branch on, or a result you will ignore.
- Apply the matching change rather than suppressing the status of the whole line.
Branch on the result when it is a test
Move the command into an if or a while condition, which the Bash manual exempts from errexit explicitly. This keeps the exit status meaningful, because a status you are reading is not a status the shell needs to act on.
- run: |
if grep -q "WARN" build.log; then
echo "warnings present"
else
echo "clean"
fiSuppress narrowly, and only where you mean it
Use || true on the single command whose failure you accept, never on a whole compound line, and add a comment saying which non-zero status you are forgiving. A suppression without that note becomes permanent, because the next reader cannot tell whether it was deliberate.
- run: grep -c "WARN" build.log || true # 1 means no matches, which is fine hereLet the step fail visibly when the whole step is optional
If the point is that this work may fail without stopping the job, put continue-on-error: true on the step. The step is recorded as failed and the job carries on, which leaves a mark in the run that a swallowed exit code does not. Be aware of what the same key does on a matrix job, which is a separate page in this hub.
What errexit does and does not abort on
Errexit has a list of exemptions, and knowing it converts most of these failures from a mystery into a one-character edit. The Bash manual states that the shell exits immediately if a pipeline, a list or a compound command returns non-zero, and then names the cases where it does not: a command in the list following a while or until keyword, a command that is part of the test in an if statement, any command in an && or || list except the one after the final operator, any command in a pipeline except the last, and a command whose return status is inverted with !.
Read that list against your own step and the pattern is clear. A failure used as a decision is exempt, and a failure standing on its own line is not. grep WARN build.log on its own aborts. if grep -q WARN build.log; then ...; fi does not. grep -q WARN build.log || true does not, because the grep is not the command after the final operator.
The manual adds a detail worth keeping. When a compound command or a shell function runs in a context where errexit is being ignored, none of the commands inside it are affected either, even if the option is set. A helper function called from the test of an if is therefore unprotected all the way down, which is a real source of surprise in longer scripts.
| How the command appears in the block | Non-zero ends the step | Why |
|---|---|---|
grep WARN build.log | yes | an ordinary command in the list |
if grep -q WARN build.log; then | no | part of the test in an if |
grep -q WARN build.log || echo none | no | not the command after the final operator |
! grep -q WARN build.log | no | the status is inverted with ! |
grep WARN build.log | tee out.txt | no | not the last command in the pipeline |
Deciding, rather than suppressing
Appending || true to the failing line makes the step green and is the right answer exactly when you genuinely do not care about the result. It is the wrong answer when you do care, because it also swallows every other reason that command might fail, including the ones you have not met yet: a missing file, a permissions problem, a tool that changed its exit codes in a minor version.
When the result matters, capture it and branch on it. Assigning to a variable inside a command substitution, or following the command with an explicit status check, keeps the distinction between "found nothing" and "could not run" visible. Most tools distinguish those with different codes, and a blanket suppression discards the distinction.
When the whole step is genuinely allowed to fail, say so on the step rather than inside the script. continue-on-error on the step records a failure that did not fail the job, which is honest, whereas a suppressed exit code inside a script leaves no trace anywhere.
- name: Count warnings
run: |
count=$(grep -c "WARN" build.log || true)
echo "warnings=$count" >> "$GITHUB_OUTPUT"
if [ "$count" -gt 100 ]; then
echo "too many warnings" >&2
exit 1
fiWhy there is no recorded run on this page
This failure does reach a runner, and it would be easy to record. It would also prove nothing. Any recording would show a script written for the recording failing on a command chosen for the recording, and a reader's step failed on their command, for their reason, with their tool's exit code. The recorded number would be ours, and the number is the only part of the message that carries information.
The claims this page makes are about the shell's option set and about the list of shapes errexit exempts. The first is readable in the runner's argument table and the second is stated in the Bash manual, both quoted above. A log would add a timestamp to facts that are not in dispute and leave the reader's actual question, which command exited 1, exactly where it was.
How to prevent it
- Treat exit code 1 as a tool's own report and look up what that tool means by it.
- Keep one logical action per
runblock, so an abort cannot hide later work. - Write tests as conditions rather than as bare commands followed by a suppression.
- Comment every
|| truewith the status it is forgiving.
Frequently asked questions
Why does my step fail when grep finds nothing?
if, invert it with !, or append || true if the result genuinely does not matter.Which commands does set -e not abort on?
while or until, a command in the test of an if, any command in an && or || list except the one after the final operator, any command in a pipeline except the last, and a command whose status is inverted with !. Anything else that returns non-zero ends the script.