Skip to content

Commit 3e904c3

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 3e904c3

9 files changed

Lines changed: 351 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: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
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+
# Build a tagged image with deps baked in so subsequent test runs are fast.
164+
# Override the Sim image's entrypoint (Kit launcher) with bash so we can
165+
# run plain shell commands. EULA env vars set per build.yaml conventions.
166+
docker run --name arm-deps-prep --user root \
167+
--entrypoint bash \
168+
-e OMNI_KIT_ACCEPT_EULA=yes -e ACCEPT_EULA=Y -e ISAAC_SIM_HEADLESS=1 \
169+
-e PRIVACY_CONSENT=Y \
170+
"${{ needs.config.outputs.isaacsim_image_name }}:${{ needs.config.outputs.isaacsim_image_tag }}" \
171+
-c "
172+
set -euo pipefail
173+
apt-get update
174+
apt-get install -y --no-install-recommends \
175+
libgmp-dev libmpfr-dev libeigen3-dev libcgal-dev libboost-all-dev \
176+
cmake build-essential
177+
rm -rf /var/lib/apt/lists/*
178+
"
179+
docker commit arm-deps-prep isaac-lab-arm-ci:${{ github.sha }}
180+
docker rm arm-deps-prep
181+
182+
- name: Editable install of isaaclab + isaaclab_tasks in a uv venv
183+
shell: bash
184+
timeout-minutes: 25
185+
run: |
186+
set -euo pipefail
187+
# All Tier 2 jobs need both packages installed once. We do the install
188+
# inside a uv venv mounted under /workspace/isaaclab so subsequent
189+
# docker run invocations see the same env_isaaclab_uv directory.
190+
docker run --rm --user root \
191+
--entrypoint bash \
192+
-e OMNI_KIT_ACCEPT_EULA=yes -e ACCEPT_EULA=Y -e ISAAC_SIM_HEADLESS=1 \
193+
-e PRIVACY_CONSENT=Y \
194+
-v "${{ github.workspace }}":/workspace/isaaclab \
195+
-w /workspace/isaaclab \
196+
--gpus all \
197+
isaac-lab-arm-ci:${{ github.sha }} \
198+
-c "
199+
set -e
200+
if ! command -v uv >/dev/null 2>&1; then
201+
curl -LsSf https://astral.sh/uv/install.sh | sh
202+
export PATH=\$HOME/.local/bin:\$PATH
203+
fi
204+
uv venv --python 3.12 env_isaaclab_uv
205+
source env_isaaclab_uv/bin/activate
206+
uv pip install -e source/isaaclab
207+
uv pip install -e source/isaaclab_assets
208+
uv pip install -e source/isaaclab_tasks
209+
uv pip install pytest pytest-timeout
210+
python -c 'import isaaclab, isaaclab_assets, isaaclab_tasks; print(\"editable imports ok\")'
211+
"
212+
213+
- name: Tier 1 — general-arm smoke (torch + scipy)
214+
shell: bash
215+
continue-on-error: true
216+
timeout-minutes: 10
217+
run: |
218+
set -e
219+
mkdir -p reports
220+
docker run --rm --user root \
221+
--entrypoint bash \
222+
-e OMNI_KIT_ACCEPT_EULA=yes -e ACCEPT_EULA=Y -e ISAAC_SIM_HEADLESS=1 \
223+
-e PRIVACY_CONSENT=Y \
224+
-v "${{ github.workspace }}":/workspace/isaaclab \
225+
-w /workspace/isaaclab \
226+
--gpus all \
227+
isaac-lab-arm-ci:${{ github.sha }} \
228+
-c "
229+
source env_isaaclab_uv/bin/activate
230+
python -m pytest \
231+
source/isaaclab/test/deps \
232+
--ignore=tools/conftest.py \
233+
-m arm_ci \
234+
--continue-on-collection-errors \
235+
--timeout=60 \
236+
--timeout-method=signal \
237+
-v \
238+
--junitxml=reports/general-arm.xml
239+
"
240+
241+
- name: Tier 1 — kit-launch-arm (boot Kit headless)
242+
shell: bash
243+
continue-on-error: true
244+
timeout-minutes: 10
245+
run: |
246+
set -e
247+
docker run --rm --user root \
248+
--entrypoint bash \
249+
-e OMNI_KIT_ACCEPT_EULA=yes -e ACCEPT_EULA=Y -e ISAAC_SIM_HEADLESS=1 \
250+
-e PRIVACY_CONSENT=Y \
251+
-v "${{ github.workspace }}":/workspace/isaaclab \
252+
-w /workspace/isaaclab \
253+
--gpus all \
254+
isaac-lab-arm-ci:${{ github.sha }} \
255+
-c "
256+
source env_isaaclab_uv/bin/activate
257+
uv pip install --extra-index-url https://pypi.nvidia.com 'isaacsim[all]'
258+
timeout 120 python - <<'EOF'
259+
import sys
260+
from isaaclab.app import AppLauncher
261+
sim = AppLauncher(headless=True).app
262+
assert sim is not None, 'AppLauncher did not return a SimulationApp'
263+
sim.close()
264+
sys.exit(0)
265+
EOF
266+
"
267+
268+
- name: Tier 2 — kitless-arm (Warp + OvRTX rendering on aarch64)
269+
shell: bash
270+
continue-on-error: true
271+
timeout-minutes: 30
272+
run: |
273+
set -e
274+
docker run --rm --user root \
275+
--entrypoint bash \
276+
-e OMNI_KIT_ACCEPT_EULA=yes -e ACCEPT_EULA=Y -e ISAAC_SIM_HEADLESS=1 \
277+
-e PRIVACY_CONSENT=Y \
278+
-v "${{ github.workspace }}":/workspace/isaaclab \
279+
-w /workspace/isaaclab \
280+
--gpus all \
281+
isaac-lab-arm-ci:${{ github.sha }} \
282+
-c "
283+
source env_isaaclab_uv/bin/activate
284+
python -m pytest \
285+
source/isaaclab_tasks/test \
286+
--ignore=tools/conftest.py \
287+
-m arm_ci \
288+
--continue-on-collection-errors \
289+
--timeout=300 \
290+
--timeout-method=signal \
291+
-v \
292+
--junitxml=reports/kitless-arm.xml
293+
"
294+
295+
- name: Tier 2 — determinism-arm (controllers / math)
296+
shell: bash
297+
continue-on-error: true
298+
timeout-minutes: 20
299+
run: |
300+
set -e
301+
docker run --rm --user root \
302+
--entrypoint bash \
303+
-e OMNI_KIT_ACCEPT_EULA=yes -e ACCEPT_EULA=Y -e ISAAC_SIM_HEADLESS=1 \
304+
-e PRIVACY_CONSENT=Y \
305+
-v "${{ github.workspace }}":/workspace/isaaclab \
306+
-w /workspace/isaaclab \
307+
--gpus all \
308+
isaac-lab-arm-ci:${{ github.sha }} \
309+
-c "
310+
source env_isaaclab_uv/bin/activate
311+
python -m pytest \
312+
source/isaaclab/test \
313+
--ignore=tools/conftest.py \
314+
--ignore=source/isaaclab/test/deps \
315+
-m arm_ci \
316+
--continue-on-collection-errors \
317+
--timeout=180 \
318+
--timeout-method=signal \
319+
-v \
320+
--junitxml=reports/determinism-arm.xml
321+
"
322+
323+
- name: Upload test reports
324+
if: always()
325+
uses: actions/upload-artifact@v4
326+
with:
327+
name: arm-ci-reports
328+
path: reports/
329+
retention-days: 7
330+
331+
- name: Clean up per-run image
332+
if: always()
333+
shell: bash
334+
run: |
335+
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

0 commit comments

Comments
 (0)