Set Up Continuous Integration and Deployment
Continuous Integration validates your Objectives on every pull request. Continuous Deployment installs validated MoveIt Pro releases on the hardware that runs them. This guide covers both halves of the pipeline.
The examples use GitHub Actions, but the patterns translate directly to GitLab, Jenkins, or any other CI host.
Continuous Integration
Reusable Workflows
We provide a reusable workflow for integration testing your robot configurations, running each Objective in a MoveIt Pro container in your CI pipeline. This ensures that your robot configurations are compatible with MoveIt Pro and that all Objectives are functioning as expected. To use this workflow in GitHub Actions:
jobs:
integration-test-in-moveit-pro-container:
uses: PickNikRobotics/moveit_pro_ci/.github/workflows/workspace_integration_test.yaml@v0.0.2
with:
image_tag: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || github.ref_name }}
colcon_test_args: "--executor sequential"
secrets: inherit
For the full workflow source, visit the MoveIt Pro CI GitHub repository.
Custom Actions
We provide a custom action for linting your Objectives in a robot configuration. This action ensures that your Objectives are properly formatted and adhere to best practices. Add this job to your workflow:
jobs:
validate_objectives:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v5
- uses: PickNikRobotics/moveit_pro_lint@v0.0.1
For the full action source, visit the MoveIt Pro Lint GitHub repository.
Continuous Deployment
CD installs MoveIt Pro on your hardware automatically once a release is cut, so QA laptops, fleet robots, and demo machines never drift from the validated build.
Architecture
A CD pipeline for MoveIt Pro has four parts:
- Mesh VPN — gives the CI runner authenticated, encrypted access to each target machine without exposing SSH to the public internet.
- Root-owned installer wrapper (
install-moveit-pro) — validates the version string, downloads the.debto a root-owned cache, installs it, and deletes the file. - Systemd template unit (
moveit-pro@<user>.service) — runsmoveit-pro-runas the specified user. That wrapper startsmoveit_pro runwith the arguments the installed release needs:--headlesson every series, plus--no-discoveryfrom 10.x, which it selects by readingmoveit_pro --version.--no-discoverystops the unit supervising an instance discovery daemon of its own, so it registers with the one the CD job starts instead. Does not restart on failure, so a crash surfaces as a notification instead of a silent retry loop. - Narrow
sudoers.ddrop-in — grants the CI user passwordlesssudoon only the installer and the systemd commands for its own unit. No wildcards, no shells.
The moveit_pro_hardware_scripts repository ships the installer, systemd unit, and sudoers template. The steps below use it directly.
Step 1 — Set up the mesh VPN
Tailscale is the recommended choice — it ships SSH with OAuth identity and tag-based ACLs, so the CI runner authenticates as a tagged machine rather than holding a long-lived SSH key.
On each target machine:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
sudo tailscale set --ssh
Follow the authentication URL, then tag the machine (for example tag:cd-target) in the Tailscale admin console. Add an ACL rule that allows your CI runner tag to SSH as the CI user on tag:cd-target.
If your environment requires WireGuard instead, configure a point-to-point peer between the CI runner and each target machine and use standard SSH key authentication. The rest of the CD pattern is identical — only the transport and auth step change.
Step 2 — Install the hardware scripts
On the target machine:
git clone https://github.com/PickNikRoboticsInfra/moveit_pro_hardware_scripts.git
cd moveit_pro_hardware_scripts
sudo ./install.sh
install.sh must run as root, and supports Ubuntu 22.04, 24.04, and 26.04. It:
- Installs its apt prerequisites (
ca-certificates,curl, andpython3). Nothing needs a runtime beyond the systempython3, and no X server is required on either MoveIt Pro series. - Copies
install-moveit-proandmoveit-pro-runto/usr/local/sbin/(root-owned). - Creates the root-owned cache directory
/var/cache/moveit-pro/. - Installs the
moveit-pro@.servicesystemd template. - Enables
moveit-pro@<ci-user>.servicefor the current user. - Drops a
sudoers.d/<ci-user>-cientry granting NOPASSWD on the installer and the user's own systemd restart/stop commands — and nothing else.
Step 3 — Provision the Endpoint Frontend Key
MoveIt Pro Runtime endpoints fail closed without MOVEIT_FRONTEND_KEY. Generate one key per
deployment and install it for the systemd service:
FRONTEND_KEY="$(openssl rand -hex 32)"
# Work on a temporary copy so reruns preserve unrelated environment entries.
SYSTEMD_ENV_TMP="$(mktemp)"
trap 'rm -f "$SYSTEMD_ENV_TMP"' EXIT
if sudo test -f /etc/default/moveit-pro; then
sudo cat /etc/default/moveit-pro > "$SYSTEMD_ENV_TMP"
fi
# Replace the current key entry while retaining every unrelated variable.
sed -i -e '/^MOVEIT_FRONTEND_KEY=/d' "$SYSTEMD_ENV_TMP"
if test -s "$SYSTEMD_ENV_TMP" && test -n "$(tail -c 1 "$SYSTEMD_ENV_TMP")"; then
printf '\n' >> "$SYSTEMD_ENV_TMP"
fi
printf 'MOVEIT_FRONTEND_KEY=%s\n' "$FRONTEND_KEY" >> "$SYSTEMD_ENV_TMP"
# systemd reads this root-owned environment file before dropping privileges.
sudo install -m 0640 -o root -g root \
"$SYSTEMD_ENV_TMP" /etc/default/moveit-pro
rm -f "$SYSTEMD_ENV_TMP"
trap - EXIT
unset FRONTEND_KEY
This systemd workflow starts Compose without the MoveIt Pro CLI, so rotate its key by repeating this step, restarting the service, and updating its clients. See Endpoint Security for certificate overrides and the self-signed development certificate model.
Step 4 — Verify passwordless sudo
Confirm the CI user can run the installer without a password prompt before wiring up the runner:
sudo -n /usr/local/sbin/install-moveit-pro 10.0.0
A password prompt means the sudoers drop-in did not land. Re-run install.sh and check visudo -c.
Step 5 — Wire up the CI runner
The following GitHub Actions job authenticates to Tailscale, validates the version, installs the release, starts instance discovery, and restarts the service:
jobs:
deploy:
runs-on: ubuntu-22.04
steps:
- name: Authenticate to Tailscale
uses: tailscale/github-action@v3
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci-runner
- name: Install MoveIt Pro
run: |
tailscale ssh ${{ vars.CI_USER }}@${{ vars.TARGET_HOST }} \
sudo -n /usr/local/sbin/install-moveit-pro ${{ inputs.version }}
- name: Start instance discovery
run: |
tailscale ssh ${{ vars.CI_USER }}@${{ vars.TARGET_HOST }} \
moveit_pro discovery up
- name: Restart service
run: |
tailscale ssh ${{ vars.CI_USER }}@${{ vars.TARGET_HOST }} \
sudo -n /bin/systemctl restart moveit-pro@${{ vars.CI_USER }}.service
The install and restart steps use sudo -n so the job fails immediately if passwordless sudo is misconfigured instead of hanging for a password prompt.
The discovery step starts the host's instance discovery daemon, which is what lets a MoveIt Pro Desktop App elsewhere on the network find this machine. Three things about it are easy to get wrong:
- It runs as the CI user, without
sudo. The daemon is a systemd user service and its owner-local socket path derives from that account's user ID, so a root-owned daemon is one the Runtime cannot register with. - It runs on every deploy, not once at provisioning. The command is idempotent, and re-running it refreshes the daemon's unit files after a release upgrade replaces its code.
- It belongs here rather than in
install.sh. An SSH session gives the CI user a systemd user manager, whichmoveit_pro discovery upneeds in order to install its units and to keep the daemon alive after the session ends.install.shruns as root at provisioning time, often before any release is on the machine.
The step's if: skips it on MoveIt Pro 9.4.x, which has no discovery subcommand and would fail the job before the restart.
To kick off an Objective right after install, add a step that sends a /do_objective goal from a
native ROS 2 client on the robot network. See
Run an Existing Objective for an rclpy
client and for the ros2 action send_goal command line. Detach the call so the SSH session
returns while the Objective keeps running on the target machine after the CD job exits.
Example smoke-test scripts
The example_scripts/ directory in moveit_pro_hardware_scripts ships a small set of reference scripts that show how to drive an Objective from CD. install.sh copies them to /usr/bin/ so the CD job can invoke them directly over SSH:
3-waypoint-pick-and-place.py— runs the3 Waypoints Pick and PlaceObjective.ml-segment-image.py— runs theML Segment Image LoopObjective. Exercises the inference path on a GPU-enabled target.move-all-boxes.py— runs theMove Boxes LoopingObjective. Long-running mobile-manipulation soak.
Each script is a two-line wrapper around cd_objective_lib.py's run_objective(<name>):
#!/usr/bin/env python3
import sys
sys.path.insert(0, "/usr/lib/moveit-pro-scripts")
from cd_objective_lib import run_objective
if __name__ == "__main__":
run_objective("3 Waypoints Pick and Place")
cd_objective_lib.py handles the heavy lifting. It waits up to one hour for /do_objective to be advertised, then sends the goal with ros2 action send_goal through moveit_pro shell, so the client runs inside the Runtime container where ROS 2 and moveit_studio_sdk_msgs already live. On any timeout it stops moveit-pro@<user>.service, posts a Slack notification, and opens or updates a deduplicated GitHub issue (set SLACK_WEBHOOK_URL and MOVEIT_CD_GITHUB_TOKEN to enable them). Export those two from the CI user's ~/.bashrc, above any non-interactive guard, rather than in /etc/default/moveit-pro: that file is read by the systemd unit, and this script runs from the CD job's SSH session instead.
Because the goal goes over native ROS 2 rather than the web bridge, the scripts need no MOVEIT_FRONTEND_KEY, no TLS configuration, and no bridge port. Three preconditions do apply: the target needs an active user workspace for moveit_pro shell to attach to, the service must already be running so the shell attaches to the live container instead of starting an ephemeral one, and a client sharing the host with the Runtime may need CYCLONEDDS_MAX_AUTO_PARTICIPANT_INDEX raised for DDS discovery — see Configure ROS 2 DDS.
The library also exposes run_objectives_forever([...]) for chaining multiple Objectives in a loop — useful for QA soak tests against Behavior Trees that do not self-loop.
Driving Objectives from your own scripts
The example scripts shell into the Runtime container because the CD job already routes over SSH and the container is where ROS 2 lives. An application that already runs on the robot network can skip that hop and send the same goal from a native ROS 2 client with rclpy directly.
For the full reference of the /do_objective action, cancellation, and blackboard parameter overrides, see Run an Existing Objective. The Programmatic SDKs Overview covers client setup.