로봇 프로젝트의 CI/CD 코드는 어떻게 작성되어 있을까 — Reachy Mini Physical CI 분석
Pollen Robotics의 오픈소스 로봇 Reachy Mini 프로젝트에서 실제로 사용하는 Physical CI 파이프라인 코드를 분석합니다.
최근 Reachy Mini라는 오픈소스 로봇을 구입했습니다. GitHub 저장소를 둘러보다가 reachy_mini_physical_ci.yml이라는 워크플로우 파일을 발견했습니다. 물리 로봇을 대상으로 CI를 돌리는 파이프라인이었습니다.

“로봇 CI”라고 하면 뭔가 특별한 프레임워크나 도구가 있을 것 같지만, 열어보니 workflow_dispatch, needs:, if: always(), pytest 마커, Self-hosted runner — 전부 익숙한 도구였습니다. 다만 그 도구들이 쓰이는 맥락이 달랐습니다.
일반 CI와 Physical CI
Reachy Mini 프로젝트에는 워크플로우 파일이 여러 개 있습니다. 일반 CI와 Physical CI를 나란히 놓고 보겠습니다.
일반 CI
# pytest.yml — PR 올리면 자동 실행on: pull_request: paths: ["src/**", "tests/**"]
jobs: pytest: runs-on: ubuntu-latest steps: - name: Run tests run: | uv run pytest -vv \ -m 'not audio and not video and not wireless' \ --tb=short-m 'not audio and not video and not wireless' — 하드웨어가 필요한 테스트를 pytest 마커로 제외합니다. @pytest.mark.wireless가 붙은 테스트는 클라우드 runner에서 돌려봐야 의미가 없으니까요. 마커 자체는 pyproject.toml에 등록되어 있습니다:
[tool.pytest.ini_options]markers = [ "audio: mark test as requiring audio hardware", "video: mark test as requiring video hardware", "wireless: mark test as requiring wireless Reachy Mini",]Physical CI
on: workflow_dispatch: # 수동 실행만
jobs: linux_wireless: runs-on: ci-runner-linux # Self-hosted runner timeout-minutes: 10workflow_dispatch로 수동 실행만 합니다. 물리 로봇은 하나뿐이니, 사람이 “지금 돌려도 되는 상황인지” 판단한 뒤 실행합니다. ci-runner-linux는 실제 로봇이 연결된 물리 서버의 Self-hosted runner입니다.
여기까지는 별다를 게 없습니다. workflow_dispatch도, Self-hosted runner도, pytest 마커도 다들 쓰는 기능이니까요.
도구는 같은데 맥락이 다른 부분
curl로 로봇을 활성화하고 슬립시킨다
Reachy Mini의 Daemon은 FastAPI 서버로 REST API를 노출합니다:
graph LR A[SDK<br>Python Client] -->|REST / WebSocket| B[Daemon<br>FastAPI Server] B --> C[Backend] C --> D[물리 로봇<br>Serial/USB] C --> E[MuJoCo<br>시뮬레이션] C --> F[Mockup<br>가상 백엔드]덕분에 CI 스크립트에서 로봇을 이렇게 다룹니다:
# 로봇 깨우기- name: Start Reachy Mini Wireless daemon run: | curl -X POST "http://reachy-mini-ci.local:8000/api/daemon/start?wake_up=true"reachy-mini-ci.local은 네트워크에 상시 연결된 CI 전용 로봇입니다. Raspberry Pi 위의 Daemon은 항상 켜져 있고, wake_up=true는 Daemon에게 모터 전원을 넣고 로봇을 활성화하라는 요청입니다. 상태를 폴링해서 running이 되면 테스트를 시작하고, 끝나면 다시 슬립 상태로 되돌립니다.
Lite 모드에서는 CI runner에서 직접 데몬을 띄웁니다:
- name: Start Reachy Mini Lite daemon run: | reachy-mini-daemon --robot-name reachy_mini_lite \ --serialport auto --localhost-only & echo $! > lite_daemon.pidSimulation 모드에서는 MuJoCo 물리 시뮬레이터로 로봇 없이 검증합니다:
- name: Start Reachy Mini Simulation daemon run: | reachy-mini-daemon --robot-name reachy_mini_sim \ --sim --headless --localhost-only &세 모드 모두 같은 Daemon API를 사용합니다. 백엔드만 다를 뿐(robot → mujoco → mockup_sim), CI 스크립트는 동일한 HTTP 엔드포인트를 호출합니다.
cleanup의 의미가 다르다
소프트웨어 CI에서 if: always()는 임시 파일 삭제나 캐시 정리에 쓰입니다. 여기선 다릅니다:
- name: Stop Reachy Mini Wireless daemon if: always() run: | curl -X POST "http://reachy-mini-ci.local:8000/api/daemon/stop?goto_sleep=true"goto_sleep=true — 테스트가 실패하든, 타임아웃이 나든, 로봇을 물리적으로 안전한 슬립 자세로 되돌립니다. cleanup을 빼먹으면 디스크가 차는 게 아니라, 로봇이 비정상 자세로 멈춰있거나 모터에 계속 전류가 흐릅니다.
Lite 모드에서는 이중 안전장치를 둡니다:
- name: Stop Reachy Mini Lite daemon if: always() run: | curl -X POST "http://localhost:8000/api/daemon/stop?goto_sleep=true" \ || echo "Failed to stop daemon via API" sleep 2 if [ -f lite_daemon.pid ]; then kill $(cat lite_daemon.pid) 2>/dev/null || echo "Daemon already stopped" rm lite_daemon.pid fiAPI로 정상 종료를 시도하되, 실패하면 PID로 프로세스를 직접 종료합니다. Windows에서는 한 단계 더 나아가서, Job 시작 시점에 이전 실행의 잔여 프로세스(*reachy*)를 먼저 정리합니다.
로봇이 하나뿐이면 needs로 순서를 정한다
SDK가 Linux, Windows, Mac을 지원하기 때문에 OS별 Self-hosted runner가 있습니다. Wireless 테스트는 네트워크로 연결된 같은 로봇 한 대를 사용하므로, 동시에 돌리면 충돌합니다:
windows_wireless: needs: [linux_wireless, windows_lite]
mac_wireless: needs: [linux_wireless, windows_wireless, mac_lite]needs: 자체는 기본 문법이지만, 여기서 순서를 정하는 이유가 “빌드 의존성” 때문이 아니라 **“물리 로봇을 한 번에 하나만 쓸 수 있어서”**라는 점이 다릅니다. Lite 모드는 각 runner에 USB로 직결된 별도 로봇을 쓰므로 독립 실행이 가능합니다.
그런데 아직 데몬 시작/종료만 검증한다
현재 Physical CI가 실제로 검증하는 것은 데몬이 정상적으로 부팅되고, 상태 API가 running을 반환하고, 안전하게 종료되는지까지입니다. 모터를 움직이거나 특정 동작을 검증하는 단계는 아직 없습니다.
더 넓은 그림 — SIL, HIL, Fleet CD
Reachy Mini만의 구조가 아닙니다. 로봇, 임베디드, IoT, 자동차 분야의 CI/CD를 살펴보면 공통된 3계층이 반복됩니다:
| 계층 | 방식 | 특징 | Reachy Mini 대응 |
|---|---|---|---|
| SIL (Software-in-the-Loop) | 클라우드 runner에서 시뮬레이션 | 빠르고 확장 가능하지만, 하드웨어 고유 버그를 못 잡음 | pytest.yml + MuJoCo 시뮬레이션 |
| HIL (Hardware-in-the-Loop) | Self-hosted runner + 물리 디바이스 | 실제 하드웨어 버그를 잡지만, 경합 관리와 안전 정리가 필요 | Physical CI (이 글에서 분석한 부분) |
| Fleet CD | OTA로 프로덕션 디바이스에 배포 | 수천 대의 이기종 디바이스에 안전하게 롤아웃 | (해당 없음) |
아래로 갈수록 실행 비용과 리스크가 높아지고, 자동화 난이도도 올라갑니다. 임베디드 DevOps 서베이 논문에 따르면, SIL 수준의 CI는 컨테이너화와 시뮬레이션 덕분에 점점 보편화되고 있지만, 물리 하드웨어에 대한 CD는 인증 요구사항과 하드웨어 제약 때문에 아직 성숙도가 낮습니다.
IoT 플랫폼(Golioth)은 PR마다 8개의 물리 보드에서 541개의 HIL 테스트를 돌리고, 오픈소스 하드웨어 프로젝트(Hardware CI Arena)는 USB 8포트 테스트 리그를 직접 설계해서 마이크로컨트롤러 CI를 자동화합니다. Boston Dynamics는 “실제 로봇에서 자주 돌려야 시뮬레이션이 놓치는 문제를 잡는다”고 말합니다.
Reachy Mini의 Physical CI는 이 중 HIL 계층에 해당합니다.
정리
로봇 프로젝트의 CI/CD 코드를 열어봤습니다. 특별한 프레임워크는 없었습니다. workflow_dispatch, needs:, if: always(), pytest 마커 — 전부 평소에 쓰던 도구입니다.
다만 같은 도구가 다른 의미로 쓰이는 지점이 있었습니다. if: always()의 cleanup이 파일 삭제가 아니라 로봇을 안전한 자세로 되돌리는 것이고, needs:의 이유가 빌드 의존성이 아니라 물리 로봇이 하나뿐이라서이고, workflow_dispatch를 쓰는 이유가 편의가 아니라 하드웨어 상태를 사람이 먼저 확인해야 해서입니다.
관련 콘텐츠
EC2 해킹당하고, DevSecOps 파이프라인을 구축하다
사이드 프로젝트 개발 서버가 털린 경험을 계기로 DevSecOps 파이프라인을 구축한 이야기입니다.
DevOpsLinkedIn에서 발견한 Tencent WeKnora, GraphRAG PoC하고 PR까지 Merged
LinkedIn에서 발견한 Tencent WeKnora를 홈 Kubernetes 클러스터에서 PoC하고, Helm Chart PR까지 Merge한 여정
DevOpsn8n v1 → v2 업그레이드: Kubernetes에서 메이저 버전 넘기
n8n v1.120.4에서 v2.9.4로 2단계 순차 업그레이드한 기록. 연쇄 CVE 대응, Migration Report 활용, community node emptyDir 트레이드오프, Queue 모드 호환성 튜닝.