Reference installer, systemd unit, and sudoers template for running MoveIt Pro as a service on hardware targeted by a Continuous Deployment pipeline.
The full setup walkthrough lives at Set Up CI/CD in the MoveIt Pro docs. This README covers what is in the repo and how to use it directly.
Ubuntu 22.04, 24.04, and 26.04, on each release's stock sudo (26.04 defaults to
sudo-rs) and stock python3 (3.10, 3.12, and 3.14 — everything here is
standard library, so nothing is pip-installed and PEP 668 never comes up).
Nothing here needs a language runtime beyond the system python3. The objective
runner drives the /do_objective action through moveit_pro shell, so ROS 2 and
moveit_studio_sdk_msgs stay inside the Runtime container and the host installs
no ROS 2 packages of its own.
test/container_smoke.sh runs the installer end-to-end in a bare container and
checks the result. CI runs it on all three releases; to run it yourself:
docker run --rm -v "$PWD:/src:ro" ubuntu:26.04 bash /src/test/container_smoke.shinstall.sh— one-shot installer. Installs apt prerequisites, copies the wrapper, systemd unit, and sudoers drop-in into place, and enables the CI user's persistent systemd user manager. Run it throughsudofrom the non-root CI account on each target machine; direct root invocation is rejected so the target identity cannot fall back toroot.bin/moveit-pro-run— the unit'sExecStart. Passes--headlesson every series: below 10.x the launcher refuses to start without aDISPLAYunless it is set, and it drops only theweb_uiservice, so the REST API, the web bridge on3201, and video stay reachable and no browser opens. It also readsmoveit_pro --versionto add--no-discoveryfrom 10.x, where that flag stops the unit from supervising a discovery daemon the MoveIt Pro deploy script owns; 9.4.x has no such option and its CLI rejects unknown ones, so an unreadable version omits it.bin/install-moveit-pro— root-owned installer wrapper. Validates the version string against a strict regex, downloads the.debto a root-owned cache, installs it, and deletes the file.bin/moveit-pro@.service— systemd template unit. Runsmoveit-pro-runas%i. Does not restart on failure (Restart=no) — theExecStopPosthook reports the crash instead. Reads optional environment from/etc/default/moveit-pro.bin/notify-crash.py— posts to Slack and opens/updates a GitHub issue viaExecStopPostwhen the service exits non-zero. ReadsSLACK_WEBHOOK_URLandMOVEIT_CD_GITHUB_TOKENfrom the environment; each notification is skipped if its variable is unset.bin/notify_lib.py— shared notification helpers (slack_post,github_issue) used bynotify-crash.py(Python import) and by the CD objective runner (cd_objective_lib.py, via a Python import). Installed to/usr/lib/moveit-pro-scripts/.github_issuededuplicates by exact title within a label: a repeated failure bumps an occurrence counter and appends a row instead of opening a new issue.bin/ci-runner.sudoers.template— sudoers drop-in.install.shsubstitutes__CI_USER__with the local account and installs at/etc/sudoers.d/<user>-ci. Grants NOPASSWD on the installer and the user's own systemd unit only.example_scripts/cd_objective_lib.py— helper library for sending an Objective goal to the/do_objectiveaction. It shells out tomoveit_pro shell ros2 action send_goal, which runs inside the Runtime container where ROS 2 already lives, and blocks until the goal reaches a terminal state. Nothing touches the web bridge, so there is no TLS handshake, noMOVEIT_FRONTEND_KEY, and no port to keep in sync. On timeout or an unreachable action server it posts to Slack, opens/updates a GitHub issue, and stops the systemd unit (vianotify_lib.py).test/container_smoke.sh— runsinstall.shin a bare Ubuntu container and verifies the result. See Supported systems.example_scripts/3-waypoint-pick-and-place.py,example_scripts/ml-segment-image.py,example_scripts/move-all-boxes.py— example smoke-test scripts, each a two-line wrapper aroundcd_objective_lib.run_objective(<name>).
On the target machine:
git clone https://github.com/PickNikRoboticsInfra/moveit_pro_hardware_scripts.git
cd moveit_pro_hardware_scripts
sudo ./install.shThis installs:
- Prerequisites via apt (
ca-certificates,curl,python3). - The objective scripts to
/usr/bin/. cd_objective_lib.pyandnotify_lib.pyto/usr/lib/moveit-pro-scripts/.notify-crash.pyto/usr/bin/.install-moveit-proandmoveit-pro-runto/usr/local/sbin/(root-owned,0755)./var/cache/moveit-pro/as a root-owned download cache.moveit-pro@.serviceto/etc/systemd/system/./etc/sudoers.d/<user>-ci(validated withvisudo -cf) granting NOPASSWD on the installer andsystemctl restart/stopof the user's own service unit.- Persistent systemd user-manager state for the CI account (
loginctl enable-lingerplus an initialuser@<uid>.servicestart), so headless deploy sessions can manage discovery user units.
The install script enables — but does not start — the MoveIt Pro service for the current user.
install-moveit-pro reads /etc/moveit-pro-cd.conf (if present, root-owned) to pick the workspace repo cloned on each CD run. Without a config file, it clones moveit_pro_example_ws pinned to the release version. Pass --config to install.sh to lay down a per-machine override:
sudo ./install.sh --config moveit-pro-cd.<machine>.confSchema:
# Public example_ws pinned to release (default — equivalent to no file):
WORKSPACE_REPO=https://github.com/PickNikRobotics/moveit_pro_example_ws.git
WORKSPACE_DIR=moveit_pro_example_ws
WORKSPACE_PIN_TO_RELEASE=true
# Private workspace on a fixed branch:
WORKSPACE_REPO=git@github.com:<owner>/<repo>.git
WORKSPACE_DIR=<repo>
WORKSPACE_BRANCH=main
WORKSPACE_PIN_TO_RELEASE=falseWORKSPACE_REPO is regex-restricted to https://github.com/<owner>/<repo>.git or git@github.com:<owner>/<repo>.git. For the SSH form, the CI user needs a deploy key with read-only access.
Both notifiers read their config from /etc/default/moveit-pro (root-owned). The systemd unit loads this file via EnvironmentFile=, so notify-crash.py and the CD objective runner pick it up for crash and CD-failure events. Each notifier is independent: set only the variables you want.
sudo install -m 0640 -o root -g root /dev/stdin /etc/default/moveit-pro <<'EOF'
# Slack incoming webhook. Unset -> Slack skipped.
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/XXX/YYY/ZZZ
# GitHub issue on failure. Unset -> issue creation skipped.
MOVEIT_CD_GITHUB_TOKEN=github_pat_xxx
# Optional overrides (defaults shown):
# MOVEIT_CD_ISSUE_REPO=PickNikRobotics/moveit_pro
# MOVEIT_CD_ISSUE_LABEL=qa-deployment-failure
EOFIf a variable is unset, that notification is silently skipped — this is how non-QA machines opt out of issue creation.
MOVEIT_CD_GITHUB_TOKEN must be a fine-grained PAT scoped to the issue repo with Issues: Read and write and nothing else — the narrowest credential that can file an issue. Do not grant Contents or any other scope: a QA machine is a higher-exposure host, and the token only needs to open and comment on issues. The qa-deployment-failure label must already exist on the repo (the API does not create labels on demand).
Repeated failures of the same kind on the same machine deduplicate to a single issue (matched by title within the label) — each recurrence bumps an occurrence counter, appends a table row with the version/time/reason, and adds a comment for visibility.
A QA machine is only reachable from the MoveIt Pro Desktop App if the host runs an instance discovery daemon and the Runtime registers with it. Those are two jobs with two owners, and this repo owns only the second.
Starting the daemon is not done here. It is a systemd user service, and the MoveIt
Pro deploy script sets it up with moveit_pro discovery up. This installer does enable
lingering and starts the account's user manager, because Tailscale SSH does not create a
PAM/logind session from which the unprivileged deploy command could bootstrap one.
Registering with it is done here, by staying out of the way. From 10.x the unit
passes --no-discovery, so moveit_pro run never starts or supervises a daemon of its
own; it registers with whichever one is already serving. That behavior needs
moveit_pro#21965, merged to
v10.0, which made the flag mean "do not supervise" rather than "do not pair". On a
10.x release built before that merge the flag still suppresses pairing, so the machine
will not be discoverable.
To check the daemon side of the arrangement on a target:
moveit_pro discovery status # as the CI user# Sudo without password
sudo -n /usr/local/sbin/install-moveit-pro 9.4.0
# Start the service
sudo systemctl start moveit-pro@$USER.service
# Status / logs
systemctl status moveit-pro@$USER.service
journalctl -u moveit-pro@$USER.service -eA password prompt on the first command means the sudoers drop-in did not land. Re-run install.sh and check sudo visudo -c.
The CI runner SSHes into each target machine over a mesh VPN (Tailscale, WireGuard, or any other) and runs these commands in order:
sudo -n /usr/local/sbin/install-moveit-pro <version>— downloads and installs the.deb.moveit_pro discovery up— MoveIt Pro 10 and later only. Starts the host's instance discovery daemon so a Desktop App can find this machine. 9.4.x has nodiscoverysubcommand; skip it there.sudo -n /bin/systemctl restart moveit-pro@<user>.service— restarts the service./usr/bin/<objective>.py— optional smoke test of an Objective throughmoveit_pro shell.
Four things about step 2 are easy to get wrong:
- Run it as the CI user, not through
sudo. The daemon is a systemd user service and its owner-local socket path derives from that account's uid, so a root-owned daemon is one the Runtime cannot register with. - Provision the account first.
install.shenables lingering and starts the account's user manager. A Tailscale SSH shell does not create a PAM/logind session, and the unprivileged CLI cannot authorizeloginctl enable-lingerby itself. - Run it on every deploy, not once at provisioning. It is idempotent, and re-running it refreshes the unit files after a release upgrade replaces the daemon's code.
- Keep daemon installation in the CD job rather than
install.sh. Provisioning establishes only the persistent user manager; it often runs before any MoveIt Pro release is installed. The release-specific CLI must still install or refresh the discovery units after every package deployment.
The Runtime does not fight this: from 10.x the unit passes --no-discovery, so it registers with the daemon step 2 started instead of supervising one of its own. See Desktop App pairing.
The sudoers drop-in grants NOPASSWD on only steps 1 and 3. The installer validates the version string with a strict regex and downloads to a root-owned path, so a compromised CI account cannot escalate by planting a malicious .deb.
See Set Up CI/CD for the full pipeline, a sample GitHub Actions workflow, and the security model.
BSD 3-Clause. See LICENSE.