Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions .github/workflows/build-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,37 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

jobs:
detect-changes:
# Lightweight gating job: emits a `web` output consumed by build-webapp
# and test-cloudxr-web-e2e so PRs that don't touch the web stack skip
# the GPU-runner E2E pipeline. On push/tag events (main, release/*,
# v*) we always set web=true so protected branches retain full coverage.
runs-on: ubuntu-22.04
outputs:
web: ${{ steps.filter.outputs.web || 'true' }}
steps:
- name: Filter web paths
# paths-filter@v3 uses the GitHub API on pull_request events and
# does not need a prior actions/checkout. Skipped on non-PR events
# so the `|| 'true'` fallback above takes effect.
if: github.event_name == 'pull_request'
id: filter
uses: dorny/paths-filter@v3
with:
filters: |
web:
- 'deps/cloudxr/webxr_client/**'
- 'deps/cloudxr/docker-compose.test-e2e.yaml'
- 'deps/cloudxr/Dockerfile.runtime'
- 'tests/web_e2e/**'
- 'scripts/run_test_e2e_with_web.sh'
- '.github/workflows/build-ubuntu.yml'
- '.github/actions/setup-cloudxr-sdk/**'

build-ubuntu:
# Pin to 22.04 so the wheel is built with GCC 11's libstdc++. The resulting wheel
# then runs on both Ubuntu 22.04 and 24.04 (24.04's libstdc++ is backward compatible).
Expand Down Expand Up @@ -631,6 +661,133 @@ jobs:
fi
done

build-webapp:
# GitHub-hosted runner is sufficient: the webxr_client build is pure
# Node/Webpack and produces an artifact consumed by test-cloudxr-web-e2e.
# Skipped on PRs that don't touch the web stack — see detect-changes.
runs-on: ubuntu-22.04
needs: detect-changes
if: needs.detect-changes.outputs.web == 'true'

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup CloudXR SDK
uses: ./.github/actions/setup-cloudxr-sdk
with:
ngc-cli-api-key: ${{ secrets.NGC_TELEOP_CORE_GITHUB_SERVICE_KEY }}

- name: Set up Node
uses: actions/setup-node@v5
with:
node-version: '24'

- name: Install web client dependencies
working-directory: deps/cloudxr/webxr_client
# `npm install` (not `npm ci`) because package-lock.json is gitignored;
# --ignore-scripts skips the @nvidia/cloudxr postinstall (not needed for
# a webpack production build) and avoids any host-specific native steps.
run: npm install --ignore-scripts

- name: Build web client
working-directory: deps/cloudxr/webxr_client
run: npm run build

- name: Upload web client artifact
uses: actions/upload-artifact@v6
with:
name: webxr-client-build
path: deps/cloudxr/webxr_client/build
if-no-files-found: error
retention-days: 1

test-cloudxr-web-e2e:
Comment thread
yanziz-nvidia marked this conversation as resolved.
# GPU runner required: the CloudXR runtime needs an NVIDIA GPU even when
# only one E2E spec is running. Single matrix entry — the web client
# behavior is independent of the matrix dims used by other test jobs.
# Pinned to x64 because we install the x64-py3.11 wheel artifact below;
# an arm64 runner would make pip reject every wheel on platform tag and
# fail with "No matching distribution found".
# Skipped on PRs that don't touch the web stack — see detect-changes.
runs-on: [self-hosted, linux, gpu, x64]
needs:
- detect-changes
- build-ubuntu
- build-webapp
if: needs.detect-changes.outputs.web == 'true'

steps:
- name: Checkout code
uses: actions/checkout@v6
with:
submodules: recursive

- name: Download install artifacts
uses: actions/download-artifact@v7
with:
name: isaacteleop-install-release-x64-py3.11

- name: Extract tarball to preserve permissions
run: |
mkdir -p install
tar -xvf isaacteleop-install.tar -C install

- name: Sanity-check wheel artifact
# Surfaces "wrong-arch / missing wheel" up front. Without this, the
# Dockerfile.runtime pip step fails with a noisy "No matching
# distribution found" that masks the real cause (platform-tag
# mismatch when the runner arch doesn't match the wheel artifact).
run: |
ls -la install/wheels/
test -n "$(ls install/wheels/isaacteleop-*.whl 2>/dev/null)" || {
echo "::error::No isaacteleop wheel found in install/wheels/"
exit 1
}

- name: Download web client artifact
uses: actions/download-artifact@v7
with:
name: webxr-client-build
path: webapp

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Run web E2E tests
env:
CI: true
ACCEPT_CLOUDXR_EULA: Y
CXR_BUILD_CONTEXT: ${{ github.workspace }}
CXR_WEBAPP_DIR: ${{ github.workspace }}/webapp
PYTHON_VERSION: '3.11'
ISAACTELEOP_PIP_FIND_LINKS: /workspace/install/wheels
ISAACTELEOP_PIP_SPEC: 'isaacteleop[cloudxr]'
ISAACTELEOP_PIP_DEBUG: '0'
run: ./scripts/run_test_e2e_with_web.sh

- name: Upload web E2E artifacts
# always() so green runs also ship streaming.png + video.webm as
# visual proof that the runtime served frames; failure runs
# additionally pick up test-failed-N.png and trace.zip.
if: always()
uses: actions/upload-artifact@v6
with:
name: web-e2e-results
# Compose/runtime logs + Playwright JSON summary + screenshots +
# video recording (every run) + trace zip (failure runs only,
# per playwright.config.ts).
path: |
e2e-logs.txt
tests/web_e2e/test-results/results.json
tests/web_e2e/test-results/**/*.png
tests/web_e2e/test-results/**/*.webm
tests/web_e2e/test-results/**/trace.zip
if-no-files-found: ignore
retention-days: 7

# These are the actual dependency jobs for publish-wheel. publish-wheel
# depends only on wheel-gate so the dependency list lives in one place, while
# this gate still runs for fork PRs where publish-wheel is intentionally skipped.
Expand All @@ -643,6 +800,7 @@ jobs:
- test-viz-sanitizers
- test-cloudxr
- test-teleop-ros2
- test-cloudxr-web-e2e
if: ${{ always() }}

steps:
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
node_modules/
package-lock.json

# Playwright artifacts (test-results/, traces, HTML report)
test-results/
**/test-results/
playwright-report/
**/playwright-report/

# Build directories
build/
**/build/
Expand Down
125 changes: 125 additions & 0 deletions deps/cloudxr/docker-compose.test-e2e.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Self-contained Docker Compose stack used only by web E2E tests
# (scripts/run_test_e2e_with_web.sh and the test-cloudxr-web-e2e CI job).
# Defines:
# - cloudxr-runtime: the runtime container reusing Dockerfile.runtime, with
# a small entrypoint shim that backgrounds python -m http.server to serve
# the prebuilt webxr_client at port 8080.
# - playwright: the official Playwright image driving the web client.
#
# Both services join the project default user-defined bridge network so the
# playwright container can reach the runtime by service name (cloudxr-runtime).
#
# Use:
# docker compose -p <unique> \
# -f deps/cloudxr/docker-compose.test-e2e.yaml up
#
# This file intentionally duplicates a few lines from docker-compose.runtime.yaml
# (build args) to avoid extending it with !reset tags for the inherited
# network_mode/runtime values that conflict with E2E requirements.

services:
cloudxr-runtime:
build:
context: ${CXR_BUILD_CONTEXT:?CXR_BUILD_CONTEXT must point to repository root}
dockerfile: deps/cloudxr/Dockerfile.runtime
args:
PYTHON_VERSION:
ISAACTELEOP_PIP_SPEC:
ISAACTELEOP_PIP_FIND_LINKS:
ISAACTELEOP_PIP_DEBUG:
container_name: cloudxr-runtime-${COMPOSE_PROJECT_NAME:-isaacteleop-web-e2e}

# No `user:` override: the runtime container is fully ephemeral here (no
# host bind-writes, no shared OpenXR volume with another consumer) so it
# runs as the image-default root. That keeps /openxr/ writable without
# having to seed/chown a named volume to match a runner uid/gid.

# CI hook-based GPU injection: runc + deploy.resources, mirroring the
# pattern in docker-compose.test.yaml. Avoids `runtime: nvidia` which is
# not registered as a Docker runtime on most CI hosts.
runtime: runc
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]

environment:
- ACCEPT_EULA=${ACCEPT_CLOUDXR_EULA:-Y}
- NV_DEVICE_PROFILE=${NV_DEVICE_PROFILE:-Quest3}
- NV_GPU_INDEX=${NV_GPU_INDEX:-0}

volumes:
# /openxr/ stays in the container's own writable layer — Playwright
# never reads it, so there's no need for a shared named volume here
# (and a Docker-managed named volume seeded from the image's
# root-owned /openxr/ would break a non-root container user).
#
# Bind-mount the prebuilt web client so python -m http.server can serve
# it. CI sets CXR_WEBAPP_DIR to the downloaded webxr-client-build artifact
# directory; the default works for local runs after `npm run build`.
- ${CXR_WEBAPP_DIR:-./webxr_client/build}:/app/webapp:ro
Comment thread
yanziz-nvidia marked this conversation as resolved.

healthcheck:
# Verify both the CloudXR runtime ready marker and the static HTTP
# server's TCP port before tests start.
test:
- CMD-SHELL
- >-
test -f /openxr/.cloudxr/run/runtime_started &&
bash -c 'exec 3<>/dev/tcp/localhost/8080'
interval: 2s
timeout: 2s
retries: 30
start_period: 10s

entrypoint: ["/bin/bash", "-lc"]
command:
- |
python -m http.server 8080 --bind 0.0.0.0 --directory /app/webapp >/tmp/http.log 2>&1 &
exec /eula.sh /entrypoint.sh

playwright:
# Tag must match @playwright/test version in tests/web_e2e/package.json;
# mismatched tags break Playwright's bundled-browser path resolution.
image: mcr.microsoft.com/playwright:v1.59.1-noble
# Run as the host user so writes through the /tests bind mount
# (test-results/, package-lock.json, node_modules/) land on the host with
# the runner's ownership. The Playwright image otherwise starts as root.
user: "${PLAYWRIGHT_UID:-1000}:${PLAYWRIGHT_GID:-1000}"
depends_on:
cloudxr-runtime:
condition: service_healthy
working_dir: /tests
volumes:
# Compose paths are relative to this file's directory (deps/cloudxr/);
# ../../tests/web_e2e resolves to <repo-root>/tests/web_e2e/.
- ../../tests/web_e2e:/tests
environment:
- WEB_URL=http://cloudxr-runtime:8080
- CXR_HOST=cloudxr-runtime
# Direct CloudXR signaling port over plain ws:// (matches HTTP page
# protocol), so no TLS/certificate handling is required anywhere in the
# test path. Tests that exercise the wss-proxy would use 48322 with
# WEB_URL switched to https://.
- CXR_PORT=49100
- CI=true
# Image default HOME=/root is not writable by the non-root user we run
# as; redirect HOME so `npm install`'s cache (~/.npm) and any tool
# configs land in a writable tmpfs-style path. Playwright browsers are
# at PLAYWRIGHT_BROWSERS_PATH=/ms-playwright in the image and don't
# depend on HOME.
- HOME=/tmp
entrypoint: ["/bin/bash", "-lc"]
command:
# `npm install` (not `npm ci`) because package-lock.json is gitignored;
# @playwright/test is exact-pinned in package.json and the image tag is
# pinned above, so the install is still effectively reproducible.
- |
npm install --no-audit --no-fund
exec npx playwright test --reporter=list
Loading
Loading