How CI Pipeline Scripts and Exit Codes Interact

What is An Exit Code?

An exit code, sometimes called a return code, is a number returned by a shell command or script. While simple, exit codes remain one of the most important mechanisms for controlling modern CI/CD pipelines, automated testing, static analysis, and deployment workflows in embedded software development.

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. 

The same concepts apply whether you’re using GitLab CI, GitHub Actions, Jenkins, Azure DevOps, or another CI platform. Regardless of the tooling, pipeline jobs ultimately rely on exit codes to determine whether a step succeeds, fails, or should halt the build process.

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

Discover why Dojo Five EmbedOps is the embedded enterprise choice for build tool and test management.

Sign up to receive a free account to the EmbedOps platform and start building with confidence..

  • Connect a repo
  • Use Dev Containers with your Continuous Integration (CI) provider
  • Analyze memory usage
  • Integrate and visualize static analysis results
  • Perform Hardware-in-the-Loop (HIL) tests
  • Install the Command Line Interface for a developer-friendly experience

Subscribe to our Monthly Newsletter

Subscribe to our monthly newsletter for development insights delivered straight to your inbox.

Interested in learning more?

Best-in-class embedded firmware content, resources and best practices

Secure Firmware in the Modern World: Building with Microchip CryptoAuthentication

The minimum standard of quality for firmware devices has risen dramatically in the past years. When people spend even a modest amount of money on a device, they expect a certain level of functionality, reliability, and security. If someone buys a dog feeder with a camera on it, the expectation is that the live stream is visible to only them and not the rest of the world. Likewise, a medical implant shouldn’t allow just anyone to connect over BLE and

Read More »
Man using a macbook track pad

I’ve been using Markdown all this time??

Sometimes you need something lightweight for text files that doesn’t require all the fancy tools and formatting that is offered by other languages, such as HTML – but is still readable and mergeable as a text file. If that resonates with you, chances are good that Markdown is what you are looking for. Even if Markdown isn’t the desired result, it can be converted to match your needs. In fact, starting with Markdown can be an easy way to write

Read More »
Where AI Actually Helps in Embedded

Where AI Actually Helps in Embedded … and Where It Doesn’t

If you only read headlines, you might think AI is ready to “write all your code.” In web and app development, AI tools already feel close to magic—but embedded firmware is a different world. In this post, you get a simple map of where AI can genuinely help in embedded, and where it quietly increases risk and long-term cost. Where AI fits into embedded workflows AI delivers the most value when it supports engineering foundations—things like unified source control, environment

Read More »