In my previous article, I argued in favor of using dev containers for robotics development. I demonstrated how it can enhance both the developer experience and that of the AI agents. I also argued that one can reuse the same dev container to create an initial version of a CI. If you haven't read that one yet, I'd recommend you start by doing that.
Now, in this article, I provide details on how this can be done, accompanied by an implementation that has been released as open source by my team Focus.
You can find the dev container we use for ROS2 development available here.
It provides:
As stated in the readme file, you'll need Docker installed. If you are using Windows, you will also need WSL and Docker Desktop running. You can find more details here.
After installing those, you can:
git submodule add https://<youruser>@bitbucket.org/focusuy/ros-devcontainer.git .devcontainerThen, open it in VSCode/Cursor. Accept when prompted to reopen using a devcontainer.
This step can take up to 10 minutes on a modern computer. But only the first time, when the Docker image is first built.
The dev container is also packed with TurtleBot packages, so you can easily smoke-test the whole setup.
If you execute the following, you can test the setup:
export TURTLEBOT3_MODEL=waffle
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
Refer to the readme file if you are experiencing any trouble.
We implemented a little TurtleBot-based Nav2 demo to showcase the dev container. You can get the source here.
After opening the demo in the devcontainer, you'll have to install all dependencies:
scripts/rosdep-install.sh
Then build the packages:
scripts/build.sh
Finally, source the new workspace:
source install/setup.zsh
The demo runs Nav2 on the TurtleBot robot. To try it out, you can execute
ros2 launch turtlebot_nav_sim turtlebot_nav_sim.launch.py send_to_goal:=True
In this case, a TurtleBot is spawned in a house-like environment, shown in the Gazebo GUI. The robot perception and world model are displayed in an RViz2 instance. The Nav2 stack is also fired up. Then, a goal is commanded as part of the same launch, and the robot moves to that destination.
The same simulation can be executed in headless mode, which runs significantly faster and makes it reusable for testing purposes:
ros2 launch turtlebot_nav_sim turtlebot_nav_sim.launch.py send_to_goal:=True headless:=True
To make sure things are working correctly, you can open a separate RViz to peek at what's going on:
rviz2 -d src/turtlebot_nav_sim/rviz/nav.rviz
You should see the same thing as you did in the previous section.
This simulation can be set up as an integration test to make sure changes don't break basic navigation.
If you take a look at test_turtlebot_nav_sim_launch.py, you'll see a pytest that uses launch_pytest to launch the simulation, command the robot to go to a certain point, and assert that the navigation system responds as expected in a couple of places.
The test can be run with:
scripts/test.sh
The output is not super fun, but lets you know things were alright:
➜ ws git:(main) colcon test
Starting >>> turtlebot_nav_sim
[Processing: turtlebot_nav_sim]
Finished <<< turtlebot_nav_sim [58.6s]
Summary: 1 package finished [58.7s]If you want to see more details, you can run the test by calling pytest directly, asking it to output to the console directly (-s):
python3 -m pytest src/turtlebot_nav_sim/test/test_turtlebot_nav_sim_launch.py::test_navigation_system -v -s
You should see something like this:
================ 1 passed in 44.58s ================
Once we have all this infrastructure in place, turning it into a continuous integration system (CI) is not difficult.
First, we create a script that performs all the tasks we have done so far, including installing dependencies, building, and running the test. You can run that script locally by doing:
scripts/ci.sh
Then, you add a file that will tell your platform what to run and when. At Focus, we use Bitbucket, and we like to run our CI systems in local runners:
The file bitbucket-pipelines.yml defines what to do to validate a PR:
script:
# Build the devcontainer Docker image with commit hash
- docker build -t devcontainer-ci:${BITBUCKET_COMMIT} -f .devcontainer/Dockerfile .
# Run the CI script inside the devcontainer
- docker run --rm -v $(pwd):/home/ws -w /home/ws devcontainer-ci:${BITBUCKET_COMMIT} scripts/ci.sh
If everything goes all right, you'll get a successful pipeline. It's also fun to break the system and corroborate that it fails, try it out.
If anything goes wrong with it, it can be reproduced locally and introspected, without any gap between environments.
We have now made our ROS2 Jazzy dev container with Nav2 and Gazebo publicly available. I hope you enjoy it, and pull requests are most welcome!
In this article, we've covered building an integration test based on a simulation. We then plugged it into a CI system. We have seen how dev containers can not only streamline development setup, but also provide a means to implement the CI execution environment. Sharing the same environment means the results are easily reproducible.
I hope you have enjoyed this tutorial and that you can reuse (and even contribute to!) our devcontainer. Let us know in the comments if you're planning to do so - happy to help in case you find any blockers.