Skip to main content
Version: 9

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.

note

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 .deb to a root-owned cache, installs it, and deletes the file.
  • Systemd template unit (moveit-pro@<user>.service) — runs moveit-pro-run as the specified user. That wrapper starts moveit_pro run with the arguments the installed release needs, selected by reading moveit_pro --version. Does not restart on failure, so a crash surfaces as a notification instead of a silent retry loop. On 9.4 it passes --headless, which drops the web_ui service and lets the Runtime start with no X server. Override its ExecStart only if you drive Objectives from your own rosbridge client; see Step 3.
  • Narrow sudoers.d drop-in — grants the CI user passwordless sudo on 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, and python3). Nothing needs a runtime beyond the system python3, and no X server is required on either MoveIt Pro series.
  • Copies install-moveit-pro and moveit-pro-run to /usr/local/sbin/ (root-owned).
  • Creates the root-owned cache directory /var/cache/moveit-pro/.
  • Installs the moveit-pro@.service systemd template.
  • Enables moveit-pro@<ci-user>.service for the current user.
  • Drops a sudoers.d/<ci-user>-ci entry granting NOPASSWD on the installer and the user's own systemd restart/stop commands — and nothing else.

Step 3 — Put rosbridge back on port 3201

Required Version
This feature requires MoveIt Pro version 9.4.2 or newer.

This step is only for driving Objectives from your own roslibpy or other rosbridge client. Skip it if you use the example scripts or a native rclpy client — both send the goal over native ROS 2 and use neither bridge.

Since 9.4, 3201 belongs to the always-on MoveIt Pro web bridge and the opt-in rosbridge sidecar defaults to 3204. A rosbridge client that hard-codes 3201 reaches it only on a target that swaps the two ports: web bridge to 3204, rosbridge back to 3201.

Of the three flags this needs, only --web-bridge-port also reads an environment variable (MOVEIT_PRO_WEB_BRIDGE_PORT); --enable-rosbridge and --rosbridge-port are flags only. Put all three on the unit's ExecStart with a systemd drop-in:

sudo systemctl edit moveit-pro@<ci-user>.service
[Service]
ExecStart=
ExecStart=/usr/local/sbin/moveit-pro-run --web-bridge-port=3204 --enable-rosbridge --rosbridge-port=3201

Keep moveit-pro-run as the command and pass only the extra flags after it. The wrapper forwards whatever it is given and supplies run -v itself, along with the --headless that a 9.4 target needs to start without an X server. Naming moveit_pro directly here would drop both.

The empty ExecStart= is required — it clears the template's value so the second line replaces the original command instead of adding a second one. Run sudo systemctl daemon-reload and restart the unit to apply it.

MoveIt Pro reaches the web bridge through its /ros proxy, so nothing else needs reconfiguring after the swap. See Legacy Websocket Compatibility for the full reference.

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 9.4.2

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, 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: 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.

To kick off an Objective right after install, append a detached SSH call:

- name: Smoke-test the deploy
run: |
tailscale ssh ${{ vars.CI_USER }}@${{ vars.TARGET_HOST }} \
"setsid -f /usr/bin/3-waypoint-pick-and-place.py </dev/null >/tmp/cd-objective.log 2>&1"

The SSH session returns immediately; 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 the 3 Waypoints Pick and Place Objective.
  • ml-segment-image.py — runs the ML Segment Image Loop Objective. Exercises the inference path on a GPU-enabled target.
  • move-all-boxes.py — runs the Move Boxes Looping Objective. 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.

Neither bridge is involved

The scripts send the goal over native ROS 2, so they need no rosbridge sidecar, no port swap, and nothing from Step 3. The target does need 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.

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 send the same goal from a native ROS 2 client with rclpy directly, or from a websocket client through either bridge — the action interface is identical across transports.

For the full reference of the /do_objective action, cancellation, blackboard parameter overrides, and side-by-side rclpy / roslibpy examples, see Run an Existing Objective. The Programmatic SDKs Overview covers when to pick each transport.