Ted Kern
on 6 March 2020
Please note that this blog post has old information that may no longer be correct. We invite you to read the content as a starting point but please search for more updated information in the ROS documentation.
The ROS 2 Tooling Working Group (chaired by AWS RoboMaker) has been hard at work producing a neat set of GitHub Actions for building and testing ROS packages on a variety of different systems. They work great on Ubuntu targets and preliminary support is already present for MacOS and Windows, making them a great option for verifying your code works across all supported ROS 2 platforms. We’ve been using them ourselves in NoDL, a library/tool we’re developing to allow developers to specify the topics, services, actions, and parameters for a given ROS 2 node, and have been thrilled with how easy it is to get unit and integration tests running directly in GitHub.
GitHub’s workflows are defined in a file within the repository itself, similar to GitLab CI/CD workflows. If you’re familiar with the latter, getting started with GitHub Actions shouldn’t be too hard (they can be defined with a similar YAML format), so you may want to skip ahead to the example configuration.
What is CI?
To those unfamiliar with the term, Continuous Integration/Continuous Deployment (CI/CD) is the process by which automated systems monitor and act on a codebase.
A common example would be a test runner that is attached to source control. This runner can be scripted to intake new commits, proposed merges, etc. and invoke whatever test suite the user defines on the code, showing the results to other contributors and possibly blocking changes if they don’t meet requirements. Other examples might include scripts that output packages or executables whenever a new release is tagged, or upload documentation to a web host whenever markdown or html files are touched
Source code hosting providers like GitHub and GitLab offer tools to define these workflows and run them on your code for free (with limitations, of course). Other solutions include the external service Travis-CI (formerly the de-facto solution for GitHub) and the self hosted automation suite Jenkins.
What are GitHub Actions?
GitHub Actions are programs that serve as the building blocks of a repository’s workflow. While a workflow can be defined line by line with a large shell script, actions can automate large chunks of script.
A GitHub action for running a specific linter (for instance, ESLint) would handle installing and invoking the linter, while an action for uploading files to an S3 bucket would take arguments for destination url and keys and handle authenticating and uploading the target. Actions can also run on events in GitHub itself, like watching for magic words in issues or PRs (/revert, /submodules, etc).
ROS Github Actions
A number of actions are being maintained by the tooling working group specifically for working with ROS packages. We’ll focus on the two that every testing workflow should include, setup-ros and action-ros-ci.
The action-ros-ci-template repository contains a minimal example of running a number of linters across the codebase. However, If you want to ensure code style is enforced and users are aware of the formatting requirements, we recommend instead implementing explicit tests for the linters. That way, other contributors are explicitly notified of your code style conventions, and will be able acquire the linters with rosdep.
CI Example
In your package, create a directory named .github/workflows
. In it, create a workflow.yaml
file (any name works). Paste the following into it:
name: Test Example
on:
pull_request:
push:
branches:
- master
jobs:
build-and-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
steps:
- name: Setup ROS 2
uses: ros-tooling/[email protected]
with:
required-ros-distributions: eloquent
- name: Run Tests
uses: ros-tooling/[email protected]
with:
package-name: example_package
- name: Upload Logs
uses: actions/upload-artifact@v1
with:
name: colcon-logs
path: ros_ws/log
if: always()
Going through this step-by-step:
name: Test Example
Name of the workflow contained in this file. There can be multiple files with workflows in them.
on:
pull_request:
push:
branches:
- master
Triggers to run this workflow. Here we state that any commit in a pull request, as well as any commit pushed to master will trigger this workflow.
jobs:
Block containing all the discrete jobs to run. Each job starts in a fresh container, and can only receive data from other jobs through artifacts.
build-and-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
fail-fast: false
We define a single job, build-and-test
, and set low-level configuration options for it. In this case, we want to run the test on the mainline supported operating systems, so we define a matrix, which means the job will run for each permutation of values in the matrix. In addition, we set fail-fast to false, so a failure on one platform does not cancel the others.
We use the matrix configuration in the runs-on
option, setting the value to ${{ matrix.os }}
which will expand to the value in os
that the current matrix is running.
steps:
- name: Setup ROS 2
uses: ros-tooling/[email protected]
with:
required-ros-distributions: eloquent
We now enter the steps of the workflow itself, beginning with invoking the setup-ros2 action. We create a step, give it a name, and for its contents simply state that it uses
the action in question, at a fixed version (0.0.15, though if you’re copy-pasting you should lookup the latest release and use that instead). We use the with
block to pass arguments to the setup-ros
action, specifying that it should setup ROS 2 Eloquent only.
- name: Run Tests
uses: ros-tooling/[email protected]
with:
package-name: example_package
This step is the meat of our pipeline – the action-ros-ci action will collect our package (no need to use the checkout action), grab any dependencies, setup a workspace, and build and test our package. We pass our packages’ names (as in their package.xml
s) as the package-name
argument, and let the action handle the rest.
- name: Upload Logs
uses: actions/upload-artifact@v1
with:
name: colcon-logs
path: ros_ws/log
if: always()
Finally, we create and export an artifact, a compressed copy of the log directory for further inspection, using the action/upload-artifact
action. We use the if: always()
clause to ensure that the log uploads even when the previous steps fail (since that’s likely when we’ll actually want to see them!).