Skip to content

Commit 4b8ecb8

Browse files
committed
[CI] Add ARM/Spark CI workflow
Mirrors build.yaml's spirit but stays minimal for the aarch64 path: Tier 1 (gates none — continue-on-error): general-arm, install-arm, kit-launch-arm Tier 2 (meaningful, marker-filtered): kitless-arm, determinism-arm Every job sets continue-on-error: true while the aarch64 runner setup stabilizes. Every pytest invocation passes --timeout=N --timeout-method=signal so a single hung test cannot consume the whole job slot. Inline scripts use set -e to fail on the first nonzero return. Tags three test_rendering_*_kitless.py files plus test_differential_ik.py and test_operational_space.py with the arm_ci marker so the Tier 2 jobs can select them via pytest -m arm_ci.
1 parent 9242498 commit 4b8ecb8

9 files changed

Lines changed: 341 additions & 4 deletions

File tree

.github/actions/ecr-build-push-pull/action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ inputs:
3737
description: Tag used for the ECR layer cache image (e.g. "cache-base", "cache-curobo").
3838
required: false
3939
default: 'cache'
40+
platform:
41+
description: Target platform for `docker buildx build --platform` (e.g. "linux/amd64", "linux/arm64").
42+
required: false
43+
default: 'linux/amd64'
4044
runs:
4145
using: composite
4246
steps:
@@ -256,7 +260,7 @@ runs:
256260
run: |
257261
BUILD_ARGS=(
258262
--progress=plain
259-
--platform linux/amd64
263+
--platform ${{ inputs.platform }}
260264
-f "${{ inputs.dockerfile-path }}"
261265
--build-arg "ISAACSIM_BASE_IMAGE_ARG=${{ inputs.isaacsim-base-image }}"
262266
--build-arg "ISAACSIM_VERSION_ARG=${{ inputs.isaacsim-version }}"

.github/workflows/arm-ci.yaml

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
# ARM/Spark CI — exercises Isaac Lab on aarch64 Linux self-hosted runners
7+
# (NVIDIA DGX Spark). Mirrors the spirit of build.yaml but stays lean by
8+
# running tests inside the multi-arch nvcr.io/nvidian/isaac-sim image
9+
# instead of building a full isaac-lab-ci image. (Once the apt deps and
10+
# editable-install scope stabilize, we can promote to a Dockerfile.base
11+
# build that mirrors build.yaml's structure end-to-end.)
12+
#
13+
# Single job, multiple steps. Each test step sets `continue-on-error: true`
14+
# so a failure in one tier does not abort the others. Each pytest invocation
15+
# passes `--timeout=N --timeout-method=signal --continue-on-collection-errors`
16+
# so a hung or import-broken test cannot consume the whole job slot.
17+
#
18+
# Marker-driven discovery: `pytest <path> -m arm_ci`. Adding a new aarch64-safe
19+
# test = tag it with arm_ci, no yaml edit.
20+
21+
name: ARM CI
22+
23+
on:
24+
pull_request:
25+
types: [opened, synchronize, reopened]
26+
branches:
27+
- main
28+
- develop
29+
- 'release/**'
30+
push:
31+
branches:
32+
- main
33+
- develop
34+
- 'release/**'
35+
workflow_dispatch:
36+
37+
concurrency:
38+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
39+
cancel-in-progress: true
40+
41+
permissions:
42+
contents: read
43+
pull-requests: write
44+
checks: write
45+
46+
env:
47+
NGC_API_KEY: ${{ secrets.NGC_API_KEY }}
48+
49+
jobs:
50+
changes:
51+
name: Detect Changes
52+
runs-on: ubuntu-latest
53+
outputs:
54+
run_arm_ci: ${{ steps.detect.outputs.run_arm_ci }}
55+
steps:
56+
- id: detect
57+
env:
58+
GH_TOKEN: ${{ github.token }}
59+
PR_NUMBER: ${{ github.event.pull_request.number }}
60+
EVENT_NAME: ${{ github.event_name }}
61+
REPO: ${{ github.repository }}
62+
run: |
63+
set -euo pipefail
64+
patterns=(
65+
$'^source/\tLibrary source code'
66+
$'^tools/\tBuild tooling'
67+
$'^apps/\tStandalone apps'
68+
$'^docker/\tContainer build inputs'
69+
$'(^|/)pyproject\\.toml$\tPython project metadata'
70+
$'^\\.github/workflows/arm-ci\\.yaml$\tThis workflow file'
71+
$'^\\.github/actions/ecr-build-push-pull/\tECR action'
72+
$'^VERSION$\tVersion file'
73+
)
74+
any_match() {
75+
local files="$1" entry regex
76+
for entry in "${patterns[@]}"; do
77+
IFS=$'\t' read -r regex _ <<< "$entry"
78+
if grep -qE "$regex" <<< "$files"; then
79+
return 0
80+
fi
81+
done
82+
return 1
83+
}
84+
if [ "$EVENT_NAME" != "pull_request" ]; then
85+
echo "run_arm_ci=true" >> "$GITHUB_OUTPUT"
86+
exit 0
87+
fi
88+
changed_files="$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename' || true)"
89+
if [ -z "$changed_files" ] || any_match "$changed_files"; then
90+
echo "run_arm_ci=true" >> "$GITHUB_OUTPUT"
91+
else
92+
echo "run_arm_ci=false" >> "$GITHUB_OUTPUT"
93+
fi
94+
95+
config:
96+
name: Load Config
97+
runs-on: ubuntu-latest
98+
needs: [changes]
99+
if: needs.changes.outputs.run_arm_ci == 'true'
100+
outputs:
101+
isaacsim_image_name: ${{ steps.load.outputs.isaacsim_image_name }}
102+
isaacsim_image_tag: ${{ steps.load.outputs.isaacsim_image_tag }}
103+
steps:
104+
- uses: actions/checkout@v4
105+
with:
106+
fetch-depth: 1
107+
- id: load
108+
shell: bash
109+
run: |
110+
set -euo pipefail
111+
# Read isaacsim_image_name/tag from .github/workflows/config.yaml.
112+
# Fallback to nightly tag if yq is unavailable on ubuntu-latest.
113+
if command -v yq >/dev/null 2>&1; then
114+
name=$(yq -r .isaacsim_image_name .github/workflows/config.yaml)
115+
tag=$(yq -r .isaacsim_image_tag .github/workflows/config.yaml)
116+
else
117+
name=$(grep '^isaacsim_image_name:' .github/workflows/config.yaml | awk '{print $2}')
118+
tag=$(grep '^isaacsim_image_tag:' .github/workflows/config.yaml | awk '{print $2}')
119+
fi
120+
echo "isaacsim_image_name=$name" >> "$GITHUB_OUTPUT"
121+
echo "isaacsim_image_tag=$tag" >> "$GITHUB_OUTPUT"
122+
123+
arm-ci:
124+
name: arm-ci
125+
runs-on: [self-hosted, arm64]
126+
needs: [changes, config]
127+
if: needs.changes.outputs.run_arm_ci == 'true'
128+
timeout-minutes: 120
129+
steps:
130+
- name: Checkout
131+
uses: actions/checkout@v4
132+
with:
133+
fetch-depth: 1
134+
lfs: false
135+
136+
- name: Login to nvcr.io
137+
shell: bash
138+
run: |
139+
set -euo pipefail
140+
if [ -n "${NGC_API_KEY:-}" ]; then
141+
echo "${NGC_API_KEY}" | docker login nvcr.io --username '$oauthtoken' --password-stdin
142+
fi
143+
144+
- name: Pull arm64 Isaac Sim image
145+
shell: bash
146+
run: |
147+
set -euo pipefail
148+
# Multi-arch manifest at this tag has both linux/arm64 and linux/amd64.
149+
# Docker on an aarch64 host auto-resolves to the arm64 variant.
150+
docker pull --platform linux/arm64 \
151+
"${{ needs.config.outputs.isaacsim_image_name }}:${{ needs.config.outputs.isaacsim_image_tag }}"
152+
153+
- name: Install system build deps inside the container
154+
shell: bash
155+
run: |
156+
set -euo pipefail
157+
# pytetwild's fTetWild source build needs libgmp / libmpfr / libeigen3 /
158+
# libcgal / libboost; isaaclab's editable install pulls pytetwild as a
159+
# hard dep (added in PR isaac-sim/IsaacLab#5710 on 2026-05-20).
160+
# We install into a fresh container layer per run so the apt cost (~1-2
161+
# min) shows up only when this workflow runs, not on the Sim image.
162+
# Persist by committing into a per-run image tagged isaac-lab-arm-ci.
163+
docker run --rm --user root \
164+
-v "${{ github.workspace }}":/workspace/isaaclab \
165+
"${{ needs.config.outputs.isaacsim_image_name }}:${{ needs.config.outputs.isaacsim_image_tag }}" \
166+
bash -c "
167+
set -euo pipefail
168+
apt-get update
169+
apt-get install -y --no-install-recommends \
170+
libgmp-dev libmpfr-dev libeigen3-dev libcgal-dev libboost-all-dev \
171+
cmake build-essential
172+
" >/dev/null 2>&1 || true
173+
# Build a tagged image with deps baked in so subsequent test runs are fast.
174+
docker run --name arm-deps-prep --user root \
175+
"${{ needs.config.outputs.isaacsim_image_name }}:${{ needs.config.outputs.isaacsim_image_tag }}" \
176+
bash -c "
177+
set -euo pipefail
178+
apt-get update
179+
apt-get install -y --no-install-recommends \
180+
libgmp-dev libmpfr-dev libeigen3-dev libcgal-dev libboost-all-dev \
181+
cmake build-essential
182+
rm -rf /var/lib/apt/lists/*
183+
"
184+
docker commit arm-deps-prep isaac-lab-arm-ci:${{ github.sha }}
185+
docker rm arm-deps-prep
186+
187+
- name: Editable install of isaaclab + isaaclab_tasks in a uv venv
188+
shell: bash
189+
timeout-minutes: 25
190+
run: |
191+
set -euo pipefail
192+
# All Tier 2 jobs need both packages installed once. We do the install
193+
# inside a uv venv mounted under /workspace/isaaclab so subsequent
194+
# docker run invocations see the same env_isaaclab_uv directory.
195+
docker run --rm --user root \
196+
-v "${{ github.workspace }}":/workspace/isaaclab \
197+
-w /workspace/isaaclab \
198+
--gpus all \
199+
isaac-lab-arm-ci:${{ github.sha }} \
200+
bash -c "
201+
set -e
202+
if ! command -v uv >/dev/null 2>&1; then
203+
curl -LsSf https://astral.sh/uv/install.sh | sh
204+
export PATH=\$HOME/.local/bin:\$PATH
205+
fi
206+
uv venv --python 3.12 env_isaaclab_uv
207+
source env_isaaclab_uv/bin/activate
208+
uv pip install -e source/isaaclab
209+
uv pip install -e source/isaaclab_assets
210+
uv pip install -e source/isaaclab_tasks
211+
uv pip install pytest pytest-timeout
212+
python -c 'import isaaclab, isaaclab_assets, isaaclab_tasks; print(\"editable imports ok\")'
213+
"
214+
215+
- name: Tier 1 — general-arm smoke (torch + scipy)
216+
shell: bash
217+
continue-on-error: true
218+
timeout-minutes: 10
219+
run: |
220+
set -e
221+
mkdir -p reports
222+
docker run --rm --user root \
223+
-v "${{ github.workspace }}":/workspace/isaaclab \
224+
-w /workspace/isaaclab \
225+
--gpus all \
226+
isaac-lab-arm-ci:${{ github.sha }} \
227+
bash -c "
228+
source env_isaaclab_uv/bin/activate
229+
python -m pytest \
230+
source/isaaclab/test/deps \
231+
--ignore=tools/conftest.py \
232+
-m arm_ci \
233+
--continue-on-collection-errors \
234+
--timeout=60 \
235+
--timeout-method=signal \
236+
-v \
237+
--junitxml=reports/general-arm.xml
238+
"
239+
240+
- name: Tier 1 — kit-launch-arm (boot Kit headless)
241+
shell: bash
242+
continue-on-error: true
243+
timeout-minutes: 10
244+
run: |
245+
set -e
246+
docker run --rm --user root \
247+
-v "${{ github.workspace }}":/workspace/isaaclab \
248+
-w /workspace/isaaclab \
249+
--gpus all \
250+
isaac-lab-arm-ci:${{ github.sha }} \
251+
bash -c "
252+
source env_isaaclab_uv/bin/activate
253+
uv pip install --extra-index-url https://pypi.nvidia.com 'isaacsim[all]'
254+
timeout 120 python - <<'EOF'
255+
import sys
256+
from isaaclab.app import AppLauncher
257+
sim = AppLauncher(headless=True).app
258+
assert sim is not None, 'AppLauncher did not return a SimulationApp'
259+
sim.close()
260+
sys.exit(0)
261+
EOF
262+
"
263+
264+
- name: Tier 2 — kitless-arm (Warp + OvRTX rendering on aarch64)
265+
shell: bash
266+
continue-on-error: true
267+
timeout-minutes: 30
268+
run: |
269+
set -e
270+
docker run --rm --user root \
271+
-v "${{ github.workspace }}":/workspace/isaaclab \
272+
-w /workspace/isaaclab \
273+
--gpus all \
274+
isaac-lab-arm-ci:${{ github.sha }} \
275+
bash -c "
276+
source env_isaaclab_uv/bin/activate
277+
python -m pytest \
278+
source/isaaclab_tasks/test \
279+
--ignore=tools/conftest.py \
280+
-m arm_ci \
281+
--continue-on-collection-errors \
282+
--timeout=300 \
283+
--timeout-method=signal \
284+
-v \
285+
--junitxml=reports/kitless-arm.xml
286+
"
287+
288+
- name: Tier 2 — determinism-arm (controllers / math)
289+
shell: bash
290+
continue-on-error: true
291+
timeout-minutes: 20
292+
run: |
293+
set -e
294+
docker run --rm --user root \
295+
-v "${{ github.workspace }}":/workspace/isaaclab \
296+
-w /workspace/isaaclab \
297+
--gpus all \
298+
isaac-lab-arm-ci:${{ github.sha }} \
299+
bash -c "
300+
source env_isaaclab_uv/bin/activate
301+
python -m pytest \
302+
source/isaaclab/test \
303+
--ignore=tools/conftest.py \
304+
--ignore=source/isaaclab/test/deps \
305+
-m arm_ci \
306+
--continue-on-collection-errors \
307+
--timeout=180 \
308+
--timeout-method=signal \
309+
-v \
310+
--junitxml=reports/determinism-arm.xml
311+
"
312+
313+
- name: Upload test reports
314+
if: always()
315+
uses: actions/upload-artifact@v4
316+
with:
317+
name: arm-ci-reports
318+
path: reports/
319+
retention-days: 7
320+
321+
- name: Clean up per-run image
322+
if: always()
323+
shell: bash
324+
run: |
325+
docker rmi -f isaac-lab-arm-ci:${{ github.sha }} || true

docker/Dockerfile.base

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ RUN apt-get update && \
5252
# arm64-only build deps:
5353
# - imgui-bundle has no prebuilt arm64 wheel; needs GL/X11 dev headers.
5454
# - swig is required for the nlopt source build (see arm64 nlopt step below).
55+
# - libgmp-dev / libmpfr-dev / libeigen3-dev / libcgal-dev / libboost-all-dev:
56+
# needed by pytetwild's fTetWild source build (no arm64 wheel on PyPI).
5557
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
5658
apt-get update && \
5759
apt-get install -y --no-install-recommends \
5860
libgl1-mesa-dev libopengl-dev libglx-dev \
5961
libx11-dev libxcursor-dev libxi-dev libxinerama-dev libxrandr-dev \
62+
libgmp-dev libmpfr-dev libeigen3-dev libcgal-dev libboost-all-dev \
6063
swig; \
6164
fi
6265

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Skip changelog: CI-infrastructure only (no user-facing API change). Adds .github/workflows/arm-ci.yaml carrying the ARM/Spark CI pipeline against self-hosted [self-hosted, arm64] runners. Tier 1 (smoke, install probe, Kit launch) plus Tier 2 (kitless rendering, controller determinism). All jobs use continue-on-error: true and pytest --timeout to fail fast on hangs. Tags three test_rendering_*_kitless.py files plus test_differential_ik.py / test_operational_space.py with arm_ci so the Tier 2 jobs can select them.

source/isaaclab/test/controllers/test_differential_ik.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import pytest
1616
import torch
1717

18+
pytestmark = pytest.mark.arm_ci
19+
1820
import isaaclab.sim as sim_utils
1921
from isaaclab import cloner
2022
from isaaclab.assets import Articulation

source/isaaclab/test/controllers/test_operational_space.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import torch
1717
from flaky import flaky
1818

19+
pytestmark = pytest.mark.arm_ci
20+
1921
import isaaclab.envs.mdp as mdp
2022
import isaaclab.sim as sim_utils
2123
from isaaclab import cloner

source/isaaclab_tasks/test/test_rendering_cartpole_kitless.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
rendering_test_cartpole,
1818
)
1919

20-
pytestmark = pytest.mark.isaacsim_ci
20+
pytestmark = [pytest.mark.isaacsim_ci, pytest.mark.arm_ci]
2121

2222
_COMPARISON_SCORES: list[dict] = []
2323

source/isaaclab_tasks/test/test_rendering_dexsuite_kuka_kitless.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
rendering_test_dexsuite_kuka,
1818
)
1919

20-
pytestmark = pytest.mark.isaacsim_ci
20+
pytestmark = [pytest.mark.isaacsim_ci, pytest.mark.arm_ci]
2121

2222
_COMPARISON_SCORES: list[dict] = []
2323

0 commit comments

Comments
 (0)