서론: 머지 전에 시뮬레이션에서 검증한다
로봇 코드는 하드웨어에서 직접 테스트하기 전에 시뮬레이션에서 먼저 검증해야 한다. GitHub Actions에서 Gazebo를 실행하고 내비게이션 성공 여부를 자동으로 확인하는 파이프라인을 만들면, PR마다 시뮬레이션 테스트가 돌아간다.
1. 전체 파이프라인 구조
1
2
3
4
5
6
7
8
9
10
11
12
| PR 생성
↓
[lint] 코드 스타일 검사 (병렬)
[unit-test] 단위 테스트 (병렬)
↓
[build-image] Docker 이미지 빌드
↓
[sim-test] Gazebo 시뮬레이션 테스트
↓
[push] 이미지 레지스트리 푸시 (main 머지 시)
↓
[deploy] 현장 로봇 OTA 업데이트 (태그 생성 시)
|
2. 기본 CI 워크플로
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| # .github/workflows/ci.yml
name: ROS2 CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
ROS_DISTRO: jazzy
jobs:
lint:
runs-on: ubuntu-24.04
container:
image: ros:jazzy
steps:
- uses: actions/checkout@v4
- name: ament_lint
run: |
apt-get update -q
apt-get install -y python3-ament-flake8 \
python3-ament-pep257 ament-cmake-clang-format
source /opt/ros/jazzy/setup.bash
ament_flake8 --config setup.cfg
ament_pep257
unit-test:
runs-on: ubuntu-24.04
container:
image: ros:jazzy
steps:
- uses: actions/checkout@v4
with:
path: ros2_ws/src/robot
- name: 의존성 설치
working-directory: ros2_ws
run: |
apt-get update -q
rosdep update -q
rosdep install --from-paths src --ignore-src -r -y
- name: 빌드 (테스트 포함)
working-directory: ros2_ws
run: |
source /opt/ros/jazzy/setup.bash
colcon build --cmake-args -DBUILD_TESTING=ON
- name: 테스트 실행
working-directory: ros2_ws
run: |
source /opt/ros/jazzy/setup.bash
source install/setup.bash
colcon test --return-code-on-test-failure
- name: 테스트 결과 업로드
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: ros2_ws/log/latest_test/
|
3. Docker 이미지 빌드 및 푸시
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| build-image:
needs: [lint, unit-test]
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Docker Buildx 설정
uses: docker/setup-buildx-action@v3
- name: GitHub Container Registry 로그인
uses: docker/login-action@v3
with:
registry: ghcr.io
username: $
password: $
- name: 이미지 메타데이터
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/$/robot
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern=
type=sha,prefix=sha-
- name: 빌드 및 푸시
uses: docker/build-push-action@v5
with:
context: .
push: $
tags: $
cache-from: type=gha
cache-to: type=gha,mode=max
|
4. Gazebo 시뮬레이션 테스트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| sim-test:
needs: [build-image]
runs-on: ubuntu-24.04
container:
image: ghcr.io/$/robot:sha-$
options: --privileged
steps:
- uses: actions/checkout@v4
- name: 가상 디스플레이 설정 (Xvfb)
run: |
apt-get install -y xvfb
Xvfb :99 -screen 0 1280x720x24 &
export DISPLAY=:99
- name: 시뮬레이션 테스트 실행
timeout-minutes: 10
run: |
source /opt/ros/jazzy/setup.bash
source /ros2_ws/install/setup.bash
export DISPLAY=:99
# Gazebo + Nav2 실행
ros2 launch robot_bringup sim_test.launch.py &
SIM_PID=$!
# 시뮬레이터 준비 대기
sleep 15
# 내비게이션 테스트 실행
python3 test/sim/test_navigation.py
TEST_RESULT=$?
kill $SIM_PID
exit $TEST_RESULT
- name: 테스트 영상 업로드 (실패 시)
if: failure()
uses: actions/upload-artifact@v4
with:
name: sim-failure-recording
path: /tmp/sim_recording.mp4
|
5. 시뮬레이션 테스트 스크립트
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
| # test/sim/test_navigation.py
import rclpy
import sys
import time
from geometry_msgs.msg import PoseStamped
from nav2_msgs.action import NavigateToPose
from rclpy.action import ActionClient
from action_msgs.msg import GoalStatus
def test_navigation_to_goal():
rclpy.init()
node = rclpy.create_node('nav_test')
client = ActionClient(node, NavigateToPose, 'navigate_to_pose')
# 액션 서버 대기 (최대 30초)
if not client.wait_for_server(timeout_sec=30.0):
print("FAIL: navigate_to_pose 서버 응답 없음")
return False
# 목표 위치 전송
goal = NavigateToPose.Goal()
goal.pose.header.frame_id = 'map'
goal.pose.header.stamp = node.get_clock().now().to_msg()
goal.pose.pose.position.x = 3.0
goal.pose.pose.position.y = 2.0
goal.pose.pose.orientation.w = 1.0
future = client.send_goal_async(goal)
rclpy.spin_until_future_complete(node, future, timeout_sec=5.0)
goal_handle = future.result()
if not goal_handle.accepted:
print("FAIL: 목표 거부됨")
return False
# 완료 대기 (최대 120초)
result_future = goal_handle.get_result_async()
rclpy.spin_until_future_complete(
node, result_future, timeout_sec=120.0)
status = result_future.result().status
if status == GoalStatus.STATUS_SUCCEEDED:
print("PASS: 목표 도달 성공")
return True
else:
print(f"FAIL: 내비게이션 실패 (status={status})")
return False
if __name__ == '__main__':
success = test_navigation_to_goal()
sys.exit(0 if success else 1)
|
6. 자동 배포 (태그 생성 시)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| deploy:
needs: [sim-test]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-24.04
steps:
- name: 현장 로봇 OTA 업데이트
uses: appleboy/ssh-action@v1
with:
host: $
username: robot
key: $
script: |
cd /opt/robot
./update.sh $
- name: Slack 배포 알림
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "로봇 $ 배포 완료",
"attachments": [{
"color": "good",
"text": "현장: warehouse_seoul_01"
}]
}
env:
SLACK_WEBHOOK_URL: $
|
7. 빌드 캐시 전략
1
2
3
4
5
6
7
8
9
10
11
12
| # colcon 빌드 캐시 (GitHub Actions Cache)
- name: colcon 캐시 복원
uses: actions/cache@v4
with:
path: |
ros2_ws/build
ros2_ws/install
key: colcon-$-$
restore-keys: |
colcon-$-
# Docker layer 캐시는 build-push-action의 cache-from/to 활용
|
8. Day 4 체크리스트
- lint/unit-test를 병렬로 실행하고 빌드·시뮬레이션은 순차로 구성했다.
- Docker 레이어 캐시(
cache-from: type=gha)로 빌드 시간을 단축했다. - Xvfb로 헤드리스 Gazebo를 CI 환경에서 실행했다.
- 내비게이션 성공 여부를 액션 클라이언트로 자동 검증하는 스크립트를 작성했다.
- 태그 생성 시 SSH로 현장 로봇에 OTA 업데이트를 트리거했다.
다음 글 예고
Day 5에서는 AMR 시스템 아키텍처 설계를 다룬다. 물류 창고 자율이동로봇 시스템 전체를 ROS2로 설계할 때 고려해야 할 패키지 구조, 통신 패턴, 안전 설계를 정리한다.