How to Build a CI/CD Pipeline for Your Robot Software (Yes, It’s Worth It)
Adrian Krzemiński,

TL;DR: A ROS 2 CI/CD pipeline automates building, testing, and deploying your robot software every time you push code. With GitHub Actions and the industrial_ci framework, you can catch broken dependencies, failing unit tests, and linter violations within minutes instead of discovering them in the field. This guide shows you a working workflow, explains why it matters for mobile robots like Leo Rover, and gives you a reproducible YAML you can drop into your repository today.
Why a ROS 2 CI/CD pipeline matters for mobile robots
Robotics code rots faster than most software. A package that built cleanly last month can fail today because an upstream dependency changed its ABI, a transitive package was removed or renamed, or your colleague pushed a node that compiles but crashes at runtime. On a mobile robot deployed in the field, that failure costs you a site visit.
A ROS 2 CI/CD pipeline reduces that risk by running the full build-test-package loop in a clean container on every commit. You learn within minutes whether your code still works against the target distribution (Humble, Jazzy, Rolling), whether your launch files parse if they are covered by tests, and whether your tests pass on ARM64 if you add ARM64 jobs for the Raspberry Pi on a Leo Rover.
The payoff is concrete. Teams running CI on robotics stacks typically observe:
- Faster onboarding – new contributors get instant feedback instead of waiting for code review.
- Fewer regressions reaching the robot – linter and unit tests catch issues before they touch hardware.
- Reproducible builds – the same Docker image runs in CI and on your development machine.
- Confidence in refactors – automated tests let you restructure code without fear.
What a minimal ROS 2 CI/CD pipeline looks like
At its core, a pipeline for ROS 2 has four stages: fetch the source, resolve dependencies with rosdep, build with colcon, and test. Optionally, you add a package stage that produces a Debian package or Docker image for deployment.
You do not need to write this from scratch. The industrial_ci project, maintained by ROS-Industrial, wraps these stages into a single entry point. It handles ROS 1 and ROS 2, multiple distributions, custom apt dependencies, and optional clang-tidy or clang-format checks.
How industrial_ci works under the hood
industrial_ci spins up a Docker container matching your declared ROS_DISTRO, clones your workspace into it, runs rosdep install, then calls colcon build and colcon test. Test results are parsed and the job fails if any test fails. You configure behavior through environment variables, which keeps your workflow file short.
How to write a working GitHub Actions workflow
Here is a minimal but production-ready workflow for a ROS 2 package targeting Humble and Jazzy. Save it as .github/workflows/ci.yml in your repository.
This workflow runs on every push and pull request, tests against two ROS 2 distributions in parallel, and uploads test logs as artifacts on failure.
name: ROS 2 CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
industrial_ci:
strategy:
fail-fast: false
matrix:
ROS_DISTRO: [humble, jazzy]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Cache ccache
uses: actions/cache@v4
with:
path: .ccache
key: ccache-${{ runner.os }}-${{ matrix.ROS_DISTRO }}-${{ github.ref }}
restore-keys: |
ccache-${{ runner.os }}-${{ matrix.ROS_DISTRO }}-
- uses: ros-industrial/industrial_ci@master
env:
ROS_DISTRO: ${{ matrix.ROS_DISTRO }}
ROS_REPO: main
TARGET_WORKSPACE: /github/workspace/target_ws
CCACHE_DIR: /github/workspace/.ccache
ADDITIONAL_DEBS: "ros-${{ matrix.ROS_DISTRO }}-ament-cmake-clang-format"
CLANG_FORMAT_CHECK: file
- name: Upload test results
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.ROS_DISTRO }}
path: |
target_ws/log
target_ws/build/*/test_results
What this gives you:
- Matrix builds – your code is tested against Humble and Jazzy in parallel, so you catch distribution-specific breakage early.
- Format enforcement –
CLANG_FORMAT_CHECK: fileuses your.clang-formatand fails the build on style violations. - Cached compilation –
ccachereuses object files between runs, often reducing build time significantly on incremental changes. - Diagnosable failures – logs are uploaded as artifacts so you can download and inspect them locally.
How to add custom dependencies and private repositories
Real robot stacks pull in drivers, third-party message packages, and sometimes private code. industrial_ci supports this through additional variables:
UPSTREAM_WORKSPACE– path to a.reposfile (vcs format) listing extra source dependencies.ROSDEP_SKIP_KEYS– space-separated list of rosdep keys to skip when an upstream package is not yet released.BEFORE_INIT– shell command run before the workspace is initialized, useful for adding apt repositories or SSH keys for private GitHub access.
For a Leo Rover-based project, you might point UPSTREAM_WORKSPACE at a .repos file that pulls in the leo_common-ros2 packages so your custom navigation stack is tested against the actual rover interfaces.
How to extend the pipeline toward CD (continuous deployment)
CI proves your code compiles and passes tests. CD pushes verified artifacts to where the robot can pick them up. For ROS 2 on field robots, the most practical CD targets are:
- Docker images pushed to GitHub Container Registry or Docker Hub, tagged by git SHA and branch. The robot pulls the latest
maintag on boot or on demand. - Debian packages built with bloom or
bloom-generateand hosted on a private apt repository. Suitable when you want to mix ROS packages with system services. - OTA bundles – tarballs of pre-built workspaces fetched over a VPN. Lower overhead than Docker but require careful versioning.
A pragmatic starting point is Docker. Add a second job to your workflow that builds a multi-arch image (amd64 for simulation, arm64 for the Raspberry Pi on a Leo Rover) using docker/build-push-action and pushes it on every merge to main.
What to test beyond unit tests
Unit tests catch logic bugs. Integration tests catch the interesting failures. Consider adding:
- Launch tests using
launch_testing– verify that nodes come up, publish expected topics, and respond to services. - Headless simulation runs – spawn the robot in Gazebo for a fixed duration and assert that no node crashed and key topics stayed above a minimum rate.
- Static analysis –
ament_cppcheck,ament_flake8,ament_mypyare run bycolcon testif you add them to your package’s test configuration and declare them inpackage.xml.
When CI/CD pays off and when it does not
A pipeline is not free. You pay in setup time (a day for a working workflow, a week to refine it), CI minutes (GitHub Actions gives 2,000 free minutes/month per personal account on private repos at the time of writing – verify current limits), and maintenance when ROS distributions change.
It pays off when:
- You have more than one developer or contributor.
- Your robot is deployed somewhere physical access is expensive (field robotics, remote sites, customer installations).
- You target multiple ROS distributions or hardware variants.
- You ship updates more than once a month.
It pays off less when the project is a one-week prototype run by one person on a single laptop. Even then, a 20-line workflow file gives you free regression protection.
For teams building custom robotics platforms or running long-term research deployments, the pipeline is not optional – it is the only way to keep velocity as the codebase grows.
Frequently asked questions
Do I need industrial_ci, or can I just use plain colcon in GitHub Actions?
You can use plain colcon with OSRF-maintained osrf/ros Docker images. industrial_ci saves you from writing the apt setup, rosdep invocation, and test-result parsing yourself. For a single package it adds little; for a workspace with mixed dependencies it removes meaningful boilerplate.
How long should a ROS 2 CI run take?
A clean build of a small package (5-10 nodes) on GitHub’s standard runners typically takes 4-8 minutes including dependency installation. With ccache warmed up, incremental runs can drop to 2-4 minutes. Larger workspaces with Gazebo simulation tests can reach 20-30 minutes.
Can I test ARM64 builds for Raspberry Pi without owning a build farm?
Yes. Use docker/setup-qemu-action to enable QEMU emulation in GitHub Actions, then build inside an arm64 Docker image. Emulated builds are 3-5x slower than native, but for CI verification of a Leo Rover payload package the runtime is usually acceptable. For production builds, consider self-hosted ARM64 runners or Oracle Cloud’s free Ampere instances where available.
How do I handle secrets like SSH keys for private dependencies?
Store them as GitHub Actions secrets and inject them through BEFORE_INIT. Never commit keys to the repository. For deploy keys, restrict scope to read-only on the specific private repo.
What happens when a new ROS 2 distribution is released?
Add the new distro to your matrix as experimental with continue-on-error: true. Once your code passes there reliably, promote it. Drop end-of-life distros from the matrix to keep CI minutes under control.
Should I run hardware-in-the-loop tests in CI?
Not in the cloud CI. Run them on a self-hosted runner connected to the actual robot or a dedicated test bench, triggered on a schedule or on release tags. Cloud CI handles software-only verification; HIL belongs on a separate, slower pipeline.
How does CI/CD differ from simply running tests locally before pushing?
What CI guarantees that local testing does not is a clean, reproducible environment. Your laptop accumulates installed packages, environment variables, and built artifacts that mask missing dependencies. CI runs in a fresh container every time, so if it builds in CI, it is much more likely to build for the next person who clones the repo.
Build it once, ship with confidence
A ROS 2 CI/CD pipeline is one of the highest-leverage investments you can make in your robotics codebase. The workflow above gets you from zero to multi-distribution testing in under an hour. From there, you extend it toward simulation tests, multi-arch Docker images, and eventually fleet-wide OTA updates.
If you are building a custom robot on top of Leo Rover or Raph Rover and want a development workflow that matches Fictionlab’s own engineering practices, explore Fictionlab’s custom robotics services to discuss integration, software architecture, and deployment pipelines tailored to your application.