What is An Exit Code?
An exit code, sometimes called a return code, is a number returned by a shell command or script. It tells the caller the status of the command execution. A successful command call usually returns an exit code of 0, whereas an exit code of any other value would indicate an error or failure.
How CI Pipeline Scripts Work
A CI Pipeline is typically defined using either YAML – a type of script widely adopted by CI tools – or raw shell scripts. The format used depends on the CI provider, but YAML is increasingly becoming the de facto standard for pipeline configurations. A YAML script can configure multiple jobs per pipeline, and each of these jobs can be set up to use a shell command, array of commands, or a script.
Taking a GitLab CI pipeline for example – each job within a YAML file has a script section where a command, array of commands, or script can be added. These command(s) are run in a shell by the pipeline runner. If any command(s) return a non-zero exit code, the job fails and the rest of the commands or scripts in the job do not execute. Depending on the job’s configuration, it may also fail the entire pipeline. In GitLab CI you can set the “allow_failure” option as true in the corresponding job’s YAML file. Doing this is one way to prevent the entire pipeline from failing due to a single job failure. Generally, if a job fails for a valid reason, you probably want to fail the whole pipeline.
Determine Pipeline Job Results With Exit Codes
You can decide to pass or fail a CI pipeline job from your own scripts by returning an exit code. For example, if you want to fail a job due to the absence of a directory or the number of high severity warnings from static code analysis, all you have to do is to return a non-zero exit code from a script. The following bash script will fail the pipeline job if the directory doesn’t exist, and continue on if it does.
#!/bin/bash
# If a directory exists
if [ -d "/path/to/dir" ]; then
echo "Directory exists."
exit 0
else
echo "ERROR: Directory does not exist."
exit 1
fi
The following Python script will fail the pipeline job if there is at least one high severity warning from a code analyzer that has been run.
#!/usr/bin/env python3
# If there is at least one high severity warning
if num_of_high_severity_warn > 1:
print("Number of high severity warnings: {}".format(num_of_high_severity_warn))
sys.exit(1)
else:
print("No high severity warnings")
sys.exit(0)
When a Non-Zero Exit Code Doesn’t Mean The Job Failed
There are times when scripts will return a non-zero value that shouldn’t cause your pipeline job(s) to fail. Let’s look at an example of this using a Powershell script on Windows which invokes Robocopy (a file copy utility).
If we observe the following code, it’s executing Robocopy with the /S option. The /S option tells Robocopy to copy all subdirectories, but exclude empty ones. This means /path/to/source will be copied to /path/to/destination with all the contents except for empty directories. For the sake of this example let’s assume that /path/to/source contains an empty directory.Robocopy is a unique command because non-zero return codes do not necessarily indicate a failure. If we look at the Robocopy documentation, we can see a return code of 1 means “All files were copied successfully”. In the example below, a return code of 3 comes back which indicates: “Some files were copied. Additional files were present. No failure occurred.” This is because in /path/to/source we had an empty directory and the /S option told Robocopy not to copy it. It’s not an error, but exactly what we told Robocopy to do.
# Powershell
PS> robocopy /MOVE /S /path/to/source /path/to/destination
PS> echo $?
False
PS> robocopy /MOVE /S /path/to/source /path/to/destination
PS> echo $lastexitcode
3
The Robocopy documentation link above states that any return codes from 0-7 aren’t considered a failure. If we don’t want the pipeline job to fail, we have to let it know that a return code from Robocopy in the 0-7 range is acceptable.
To accomplish this, we need two commands in a single line so the pipeline job script sees it as one command. The first command will call Robocopy, and the second command will check whether or not the exit/return code is less than 8. If it’s < 8, the job script will return 0. If it’s >= 8, the job script will return the actual return code from Robocopy. One way to write this is shown in the following code snippet:
( robocopy <Source> <Destination> [<Options>] );if ($lastexitcode -lt 8){write-host 0}else{write-host $lastexitcode};
The above code snippet is for Windows, but the same concept can be used on other platforms. In Linux, you can do a similar exit code capture to prevent a job from failing:
<command> <arg> || exit_code=$?; if [ $exit_code -ne 0 ]; then echo "The exit code of the previous command is $exit_code"; fi;
These methods aren’t just for jumping through hoops to work with unique commands. You may have a command that isn’t critical to the success of the pipeline job, or even have an intentionally incorrect command to test log output. With either of those scenarios you can use the methods presented in this blog to prevent a pipeline build from failing on non-zero return codes.
By understanding how exit codes and pipelines interact, you will now hopefully have more tools to customize your next CI pipeline!
Next Steps
If you are interested in bringing your project to the next level, let’s schedule a call with our team. Or, you can check out EmbedOps, our CI build platform.
We look forward to hearing from you! -D5


