Skip to content

Commit 2a812a5

Browse files
authored
ci: build & publish & smoke test (#40)
1 parent af2dee1 commit 2a812a5

5 files changed

Lines changed: 113 additions & 45 deletions

File tree

.github/workflows/build_and_push_docker.yaml

Lines changed: 28 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,45 @@ on:
77
- development
88
workflow_dispatch:
99

10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
1014
jobs:
1115
build-and-push:
12-
runs-on: self-hosted
16+
runs-on: [self-hosted, cpu, heavy, gpu]
1317
steps:
1418
- name: Checkout code
15-
uses: actions/checkout@v4
19+
uses: actions/checkout@v6
20+
with:
21+
lfs: true
1622

1723
- name: Log in to Docker Hub
1824
uses: docker/login-action@v3
1925
with:
2026
username: ${{ secrets.DOCKERHUB_USERNAME }}
2127
password: ${{ secrets.DOCKERHUB_TOKEN }}
2228

23-
- name: Set up Docker Buildx
24-
uses: docker/setup-buildx-action@v3
29+
- name: Build and push all images
30+
env:
31+
AMM_IMG_SUFFIX: ${{ env.SUFFIX }}
32+
run: |
33+
docker compose -f docker/compose.yaml build
34+
docker compose -f docker/compose.yaml push
2535
26-
- name: Build and push ROS 2 image
27-
uses: docker/build-push-action@v6
28-
with:
29-
context: .
30-
file: docker/Dockerfile.ros2
31-
push: true
32-
tags: |
33-
robotecai/mobile-manipulator-demo-ros2${{ github.ref_name == 'development' && '-development' || '' }}:latest
34-
35-
- name: Build and push Agents image
36-
uses: docker/build-push-action@v6
36+
smoke-test:
37+
needs: build-and-push
38+
runs-on: [self-hosted, cpu, heavy, gpu]
39+
steps:
40+
- name: Log in to Docker Hub
41+
uses: docker/login-action@v3
3742
with:
38-
context: .
39-
file: docker/Dockerfile.agents
40-
push: true
41-
build-args: |
42-
ROS2_IMAGE=robotecai/mobile-manipulator-demo-ros2${{ github.ref_name == 'development' && '-development' || '' }}:latest
43-
tags: |
44-
robotecai/mobile-manipulator-demo-agents${{ github.ref_name == 'development' && '-development' || '' }}:latest
43+
username: ${{ secrets.DOCKERHUB_USERNAME }}
44+
password: ${{ secrets.DOCKERHUB_TOKEN }}
4545

46-
- name: Build and push O3DE image
47-
uses: docker/build-push-action@v6
48-
with:
49-
context: .
50-
file: docker/Dockerfile.o3de
51-
push: true
52-
build-args: |
53-
ROS2_IMAGE=robotecai/mobile-manipulator-demo-ros2${{ github.ref_name == 'development' && '-development' || '' }}:latest
54-
tags: |
55-
robotecai/mobile-manipulator-demo-o3de${{ github.ref_name == 'development' && '-development' || '' }}:latest
46+
- name: Headless sim smoke test (/clock)
47+
run: |
48+
IMAGE="robotecai/agentic-mobile-manipulator-o3de:latest"
49+
docker pull "$IMAGE"
50+
docker run --rm --entrypoint pixi "$IMAGE" run sim-smoke
51+

docker/compose.yaml

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# Build everything: docker compose -f docker/compose.yaml build
1212
# Run the demo: xhost +local:root && docker compose -f docker/compose.yaml up demo
13-
# Attach to a session: docker exec -it $(docker ps -qf ancestor=robotecai/mobile-manipulator-demo:latest) tmux attach -t agentic-mobile-manipulator-sim
13+
# Attach to a session: docker exec -it $(docker ps -qf ancestor=robotecai/agentic-mobile-manipulator:latest) tmux attach -t agentic-mobile-manipulator-sim
1414
# (sessions: -sim -stack -llm-servers -agents -hmi)
1515
#
1616
# Target is single-pc-gpu-and-npu (the default). For a GPU-only image — llama.cpp
@@ -28,14 +28,14 @@
2828
services:
2929
# ── Base: RoboStack ROS 2 + built workspace ────────────────────────────────
3030
ros2:
31-
image: robotecai/mobile-manipulator-demo-ros2:latest
31+
image: robotecai/agentic-mobile-manipulator-ros2:latest
3232
build:
3333
context: ..
3434
dockerfile: docker/Dockerfile.ros2
3535

3636
# ── Base: + O3DE SDK and built simulation ──────────────────────────────────
3737
o3de:
38-
image: robotecai/mobile-manipulator-demo-o3de:latest
38+
image: robotecai/agentic-mobile-manipulator-o3de:latest
3939
build:
4040
context: ..
4141
dockerfile: docker/Dockerfile.o3de
@@ -44,7 +44,7 @@ services:
4444

4545
# ── The demo: + agents, local inference engines; runs the whole stack ───────
4646
demo:
47-
image: robotecai/mobile-manipulator-demo:latest
47+
image: robotecai/agentic-mobile-manipulator:latest
4848
build:
4949
context: ..
5050
dockerfile: docker/Dockerfile.demo
@@ -68,7 +68,6 @@ services:
6868
- /dev/kfd:/dev/kfd # AMD GPU compute (ROCm)
6969
- /dev/dri:/dev/dri # AMD GPU render (Vulkan)
7070
- /dev/accel/accel0:/dev/accel/accel0 # Ryzen AI NPU (amdxdna)
71-
# The NPU pins memory; without unlimited memlock FastFlowLM fails to allocate.
7271
ulimits:
7372
memlock:
7473
soft: -1
@@ -77,10 +76,5 @@ services:
7776
- /tmp/.X11-unix:/tmp/.X11-unix
7877
- ${XAUTHORITY:-$HOME/.Xauthority}:/root/.Xauthority
7978
- ../config.toml:/root/MobileManipulatorDemo/config.toml
80-
# All weights live under the repo's models/: llama ggufs in models/, NPU
81-
# models in models/flm (FLM_MODEL_PATH below). `pixi run download-models`
82-
# fetches both. No host-home (~/.config/flm) coupling.
8379
- ../models:/root/MobileManipulatorDemo/models
84-
# Single host: ROS 2 DDS + every inference endpoint talk over localhost, and
85-
# host networking lets RViz/ros2 on the host introspect the running demo.
8680
network_mode: host

docs/quickstart.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,24 @@ git clone https://github.com/RobotecAI/agentic-mobile-manipulator.git
1717
cd agentic-mobile-manipulator
1818
```
1919

20-
## 2. Build the image
20+
## 2. Get the image: build _or_ pull
21+
22+
**Build locally:**
2123

2224
```bash
2325
docker compose -f docker/compose.yaml build
2426
```
2527

2628
This builds three cacheable layers — `…-ros2` (RoboStack ROS 2 + workspace) → `…-o3de` (O3DE SDK + simulation) → `…demo` (Python agents + llama.cpp + FastFlowLM). The O3DE layer is large, so the first build takes a while; later rebuilds reuse the cached bases.
2729

30+
**Or pull the prebuilt images:**
31+
32+
```bash
33+
docker compose -f docker/compose.yaml pull
34+
```
35+
36+
If you pull, remember to pass `--no-build` when starting the demo (see step 4).
37+
2838
## 3. Download the model weights
2939

3040
Weights are **not** baked into the image. They are downloaded into the repo's `models/` directory, bind-mounted at run time. `config.toml` is the single source of truth for which models are fetched. Pull them once into `models/` by running:
@@ -44,9 +54,12 @@ Allow local connections to your X server, then start the `demo` service:
4454

4555
```bash
4656
xhost +local:root
47-
docker compose -f docker/compose.yaml up demo
57+
docker compose -f docker/compose.yaml up demo # if you built (step 2)
58+
docker compose -f docker/compose.yaml up --no-build demo # if you pulled (step 2)
4859
```
4960

61+
> [!IMPORTANT] > `docker compose up` rebuilds any service that has a `build:` section — even when the image is already present locally. So after `pull`, a bare `up demo` ignores the pulled image and rebuilds `ros2 → o3de → demo`. Pass `--no-build` to run the pulled image as-is.
62+
5063
## What to expect
5164

5265
![Demo Windows](demo_windows.jpg)
@@ -60,7 +73,7 @@ docker compose -f docker/compose.yaml up demo
6073
Each component runs in its own tmux session inside the container. Attach to one (e.g. the simulation) from the host:
6174

6275
```bash
63-
docker exec -it $(docker ps -qf ancestor=robotecai/mobile-manipulator-demo:latest) \
76+
docker exec -it $(docker ps -qf ancestor=robotecai/agentic-mobile-manipulator:latest) \
6477
tmux attach -t agentic-mobile-manipulator-sim
6578
```
6679

pixi.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,10 @@ description = "Launch O3DE simulation in its own tmux session"
162162
cmd = "$DEMO_ROOT/sim/build/linux/bin/profile/MobileManipulatorDemo.GameLauncher -bg_ConnectToAssetProcessor=0"
163163
description = "Launch O3DE simulation in the foreground (no tmux)"
164164

165+
[feature.sim.tasks.sim-smoke]
166+
cmd = "bash scripts/sim_smoke.sh"
167+
description = "Headless sim smoke test: boot with --rhi=null, pass if /clock ticks (no GPU/display)"
168+
165169
[feature.sim.tasks.editor]
166170
cmd = "$O3DE_ENGINE_PATH/bin/Linux/profile/Default/Editor --project-path $DEMO_ROOT/sim"
167171
description = "Launch O3DE Editor from SDK (project must be built first)"

scripts/sim_smoke.sh

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#!/usr/bin/env bash
2+
# Headless smoke test for the O3DE sim.
3+
#
4+
# Boots the GameLauncher with the null renderer (--rhi=null) — no GPU, no X
5+
# display needed — and passes if the simulation is actually *ticking*, proven by
6+
# catching a message on /clock within a timeout. That single check exercises the
7+
# whole cold path: binary runs, assets + level load, PhysX ticks, and the ROS 2
8+
# bridge publishes.
9+
#
10+
# NOT checked: camera / rendered sensors. Those need the Atom (Vulkan) renderer,
11+
# which headless needs a DRI3-capable display for — Xvfb doesn't provide DRI3, so
12+
# the GPU+Vulkan path wedges on swapchain creation. rhi=null has no renderer at
13+
# all, so camera topics are advertised but never publish. Verifying rendered
14+
# output headless is a separate, unsolved problem; keep it out of the fast gate.
15+
#
16+
# Run: pixi run sim-smoke
17+
# In Docker: docker run --rm --entrypoint pixi <o3de-image> run sim-smoke
18+
# Tune: SIM_SMOKE_TIMEOUT=120 pixi run sim-smoke
19+
# SIM_SMOKE_TOPIC=/joint_states SIM_SMOKE_TYPE=sensor_msgs/msg/JointState pixi run sim-smoke
20+
set -uo pipefail
21+
22+
# Hermetic: only ever see our own sim on loopback, so a neighbouring sim on a
23+
# shared CI runner can't cause a false pass.
24+
export ROS_LOCALHOST_ONLY=1
25+
26+
LAUNCHER="${DEMO_ROOT:-$PWD}/sim/build/linux/bin/profile/MobileManipulatorDemo.GameLauncher"
27+
TIMEOUT="${SIM_SMOKE_TIMEOUT:-90}" # boot + first message; rhi=null is faster than Vulkan
28+
TOPIC="${SIM_SMOKE_TOPIC:-/clock}"
29+
# Type must be explicit: with no publisher up yet, `ros2 topic echo` can't infer
30+
# the type from the graph and bails instantly instead of waiting.
31+
TYPE="${SIM_SMOKE_TYPE:-rosgraph_msgs/msg/Clock}"
32+
LOG="$(mktemp)"
33+
34+
[ -x "$LAUNCHER" ] || { echo "[FAIL] launcher not built: $LAUNCHER"; exit 1; }
35+
36+
# GameLauncher ignores SIGINT (won't stop on Ctrl+C), so hard-kill on exit.
37+
# Kill the captured PID, plus a pkill by full command line as a fallback (the
38+
# comm name isn't reliably set during early boot). pkill -f is safe: this shell's
39+
# own argv is just "bash scripts/sim_smoke.sh", so it can't match itself.
40+
LAUNCHER_PID=""
41+
cleanup() {
42+
[ -n "$LAUNCHER_PID" ] && kill -9 "$LAUNCHER_PID" 2>/dev/null
43+
pkill -9 -f 'MobileManipulatorDemo.GameLauncher' 2>/dev/null
44+
}
45+
trap cleanup EXIT
46+
47+
echo "[smoke] launching sim headless (--rhi=null)"
48+
env -u DISPLAY "$LAUNCHER" -bg_ConnectToAssetProcessor=0 --rhi=null >"$LOG" 2>&1 &
49+
LAUNCHER_PID=$!
50+
51+
# /clock is published BEST_EFFORT; a default (reliable) subscriber gets nothing.
52+
echo "[smoke] waiting up to ${TIMEOUT}s for a message on ${TOPIC} (${TYPE})"
53+
if timeout "$TIMEOUT" ros2 topic echo --once --qos-reliability best_effort "$TOPIC" "$TYPE" >/dev/null 2>&1; then
54+
echo "[PASS] ${TOPIC} is publishing — sim ticks headless"
55+
exit 0
56+
fi
57+
58+
echo "[FAIL] no message on ${TOPIC} within ${TIMEOUT}s"
59+
echo "----- last 30 log lines -----"
60+
tail -30 "$LOG"
61+
exit 1

0 commit comments

Comments
 (0)