diff --git a/.github/actions/build-cloudxr-web-client/action.yml b/.github/actions/build-cloudxr-web-client/action.yml index 7dbf607d4..d7274dd06 100644 --- a/.github/actions/build-cloudxr-web-client/action.yml +++ b/.github/actions/build-cloudxr-web-client/action.yml @@ -53,6 +53,8 @@ runs: working-directory: deps/cloudxr/webxr_client env: SDK_VERSION: ${{ steps.sdk-version.outputs.version }} + CLIENT_GIT_REF: ${{ github.ref_name }} + CLIENT_GIT_SHA: ${{ github.sha }} run: | npm install "../nvidia-cloudxr-${SDK_VERSION}.tgz" USE_LOCAL_WEBXR_ASSETS=0 npm run build diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 9b7874d0f..7d2e312bb 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -2,15 +2,17 @@ # SPDX-License-Identifier: Apache-2.0 # # Build docs and Teleop web app, then deploy to GitHub Pages. -# - Push to main/release (canonical repo): deploy docs at site root, web app at /client/. -# - Pull request from a same-repo branch: auto-deploy preview to preview/pr-/. -# - Pull request from a fork: build only (skip deploy). A maintainer can deploy -# the preview manually via the `/preview-docs` slash command — see -# docs-preview-on-demand.yaml. +# - Push to main/release (canonical repo): deploy docs at site root, web app at /client//. +# - Same-repo PR: auto-deploy preview to preview/pr-/. +# - Fork PR: build only; a maintainer can deploy via `/preview-docs` (see +# docs-preview-on-demand.yaml). # -# PR: docs always built; web client (npm) built with public SDK for all, or private SDK only for allowlisted authors. -# Main/release (push): key used → private NGC for SDK. -# Allowlist for CXR.js SDK on PRs: Repo Settings → Variables → ALLOWED_CLOUDXR_JS_PR_AUTHORS (comma-separated GitHub logins). +# Builds are gated on changed paths by the `changes` job; gh-pages +# `keep_files: true` preserves whichever artifact was skipped. +# +# Web client uses the private CloudXR SDK for pushes and for PRs whose +# author is in ALLOWED_CLOUDXR_JS_PR_AUTHORS (Repo Settings → Variables); +# all other PRs fall back to the public SDK. name: Build & deploy docs @@ -24,6 +26,7 @@ on: paths: - "docs/**" - ".github/workflows/docs.yaml" + - ".github/actions/build-cloudxr-web-client/**" - "deps/cloudxr/webxr_client/**" - "deps/cloudxr/.env.default" @@ -64,10 +67,54 @@ jobs: esac fi + # gh-pages keep_files preserves whichever artifact isn't rebuilt this run. + changes: + name: Detect changed paths + runs-on: ubuntu-latest + permissions: {} + outputs: + docs: ${{ steps.filter.outputs.docs }} + webapp: ${{ steps.filter.outputs.webapp }} + steps: + - name: Checkout (with history) + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - id: filter + env: + EVENT: ${{ github.event_name }} + BEFORE: ${{ github.event.before }} + BASE_REF: ${{ github.base_ref }} + run: | + set -euo pipefail + if [ "$EVENT" = 'pull_request' ]; then + git fetch --no-tags --depth=1 origin "$BASE_REF" + base="$(git merge-base FETCH_HEAD HEAD || true)" + elif [ -n "$BEFORE" ] && [ "$BEFORE" != '0000000000000000000000000000000000000000' ]; then + base="$BEFORE" + else + base="" # new branch / force push: treat everything as changed + fi + files="$([ -n "$base" ] && git diff --name-only "$base" HEAD || git ls-tree -r --name-only HEAD)" + docs=false; webapp=false + while IFS= read -r f; do + case "$f" in + docs/*|.github/workflows/docs.yaml) docs=true ;; + esac + case "$f" in + deps/cloudxr/webxr_client/*|deps/cloudxr/.env.default|.github/actions/build-cloudxr-web-client/*|.github/workflows/docs.yaml) + webapp=true ;; + esac + done <<< "$files" + echo "docs=$docs" >> "$GITHUB_OUTPUT" + echo "webapp=$webapp" >> "$GITHUB_OUTPUT" + echo "::notice::changes: docs=$docs webapp=$webapp (base=${base:-})" + build-docs: name: Build Docs runs-on: ubuntu-latest - needs: [check-repo] + needs: [check-repo, changes] + if: needs.changes.outputs.docs == 'true' steps: - name: Checkout code uses: actions/checkout@v6 @@ -101,7 +148,8 @@ jobs: build-app: name: Build Teleop Web App runs-on: ubuntu-latest - needs: [check-repo] + needs: [check-repo, changes] + if: needs.changes.outputs.webapp == 'true' steps: - name: Checkout code uses: actions/checkout@v6 @@ -121,32 +169,66 @@ jobs: deploy-docs: name: Deploy docs runs-on: ubuntu-latest - needs: [check-repo, build-docs, build-app] - if: needs.check-repo.outputs.is-canonical-repo == 'true' && (needs.check-repo.outputs.is-deploy-branch == 'true' || github.event_name == 'pull_request') + needs: [check-repo, changes, build-docs, build-app] + if: >- + !cancelled() + && needs.check-repo.outputs.is-canonical-repo == 'true' + && (needs.check-repo.outputs.is-deploy-branch == 'true' || github.event_name == 'pull_request') + && needs.build-docs.result != 'failure' + && needs.build-app.result != 'failure' + && (needs.build-docs.result == 'success' || needs.build-app.result == 'success') permissions: contents: write # push to gh-pages steps: - name: Download docs artifact + if: needs.build-docs.result == 'success' uses: actions/download-artifact@v7 with: name: docs-html path: ./docs/build - name: Download web app artifact + if: needs.build-app.result == 'success' uses: actions/download-artifact@v7 with: name: webapp path: ./webapp - - name: Place web app under subpath + # Each push lands at client// so a release-branch deploy can no + # longer trample the canonical /client/, which is just a redirect to + # ./main/. + - name: Place web app under versioned subpath + if: needs.build-app.result == 'success' run: | + set -euo pipefail if [ "${{ github.event_name }}" = 'pull_request' ]; then + # PR previews live under preview/pr-/ and are pinned to the PR, + # so they don't need the multi-version layout. CLIENT_DIR=./docs/build/current/client + mkdir -p "$CLIENT_DIR" + cp -r ./webapp/. "$CLIENT_DIR/" else - CLIENT_DIR=./docs/build/client + slug="${GITHUB_REF_NAME//\//-}" + CLIENT_BASE=./docs/build/client + mkdir -p "$CLIENT_BASE/$slug" + cp -r ./webapp/. "$CLIENT_BASE/$slug/" + cat > "$CLIENT_BASE/index.html" <<'HTML' + + + + + + Redirecting to the latest Isaac Teleop web client + + + + +

Redirecting to the latest Isaac Teleop web client.

+ + + HTML + echo "Deployed web client to /client/$slug/" fi - mkdir -p "$CLIENT_DIR" - cp -r ./webapp/. "$CLIENT_DIR/" - name: Deploy to gh-pages (main branch) if: github.event_name == 'push' && needs.check-repo.outputs.is-deploy-branch == 'true' diff --git a/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx b/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx new file mode 100644 index 000000000..77a78706d --- /dev/null +++ b/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +const BUILD_INFO = { + teleopVersion: process.env.CLIENT_TELEOP_VERSION, + sdkVersion: process.env.CLIENT_SDK_VERSION, + gitRef: process.env.CLIENT_GIT_REF, + gitSha: process.env.CLIENT_GIT_SHA, + buildTime: process.env.CLIENT_BUILD_TIME, +}; + +console.info( + `[Isaac Teleop Web Client] teleop=${BUILD_INFO.teleopVersion} sdk=${BUILD_INFO.sdkVersion} ` + + `ref=${BUILD_INFO.gitRef}@${BUILD_INFO.gitSha} built=${BUILD_INFO.buildTime}` +); + +export function mountBuildInfoOverlayIfRequested(): void { + if (new URLSearchParams(window.location.search).get('showVersion') !== '1') return; + const el = document.createElement('div'); + el.id = 'teleop-build-info-overlay'; + el.style.cssText = + 'position:fixed;left:8px;bottom:8px;z-index:99999;padding:8px 10px;' + + 'font:12px/1.4 ui-monospace,Menlo,Consolas,monospace;color:#fff;' + + 'background:rgba(0,0,0,0.78);border:1px solid #76b900;border-radius:4px;' + + 'pointer-events:none;white-space:pre'; + el.textContent = + `Teleop ${BUILD_INFO.teleopVersion} · SDK ${BUILD_INFO.sdkVersion}\n` + + `${BUILD_INFO.gitRef}@${BUILD_INFO.gitSha}\n` + + `built ${BUILD_INFO.buildTime}`; + document.body.appendChild(el); +} diff --git a/deps/cloudxr/webxr_client/src/index.tsx b/deps/cloudxr/webxr_client/src/index.tsx index ceab27bc0..6072847ef 100644 --- a/deps/cloudxr/webxr_client/src/index.tsx +++ b/deps/cloudxr/webxr_client/src/index.tsx @@ -19,6 +19,7 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App'; +import { mountBuildInfoOverlayIfRequested } from './BuildInfoOverlay'; // Start the React app immediately in the 3d-ui container function startApp() { @@ -34,6 +35,8 @@ function startApp() { } else { console.error('3d-ui container not found'); } + + mountBuildInfoOverlayIfRequested(); } // Initialize the app when DOM is ready diff --git a/deps/cloudxr/webxr_client/webpack.common.js b/deps/cloudxr/webxr_client/webpack.common.js index e41ec662c..a23bc901d 100644 --- a/deps/cloudxr/webxr_client/webpack.common.js +++ b/deps/cloudxr/webxr_client/webpack.common.js @@ -16,10 +16,31 @@ */ const path = require('path'); +const fs = require('fs'); +const { execSync } = require('child_process'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const webpack = require('webpack'); +function git(cmd) { + try { + return execSync(`git ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); + } catch { + return ''; + } +} +let TELEOP_VERSION = ''; +try { + TELEOP_VERSION = fs.readFileSync(path.resolve(__dirname, '../../../VERSION'), 'utf8').trim(); +} catch {} +// Source of truth for the CloudXR SDK is deps/cloudxr/.env.default's +// CXR_WEB_SDK_VERSION (also controls which tarball `npm install` +// consumes); package.json.version is just a local-dev fallback. +const CLIENT_SDK_VERSION = process.env.SDK_VERSION || require('./package.json').version; +const CLIENT_GIT_REF = process.env.CLIENT_GIT_REF || git('rev-parse --abbrev-ref HEAD') || 'unknown'; +const CLIENT_GIT_SHA = (process.env.CLIENT_GIT_SHA || git('rev-parse HEAD') || 'unknown').slice(0, 12); +const CLIENT_BUILD_TIME = new Date().toISOString(); + // WebXR input profile assets are used by default when @webxr-input-profiles/assets is installed. // Set USE_LOCAL_WEBXR_ASSETS=0 to skip bundling local assets (build needs internet at runtime to load assets). const useLocalWebxrAssets = process.env.USE_LOCAL_WEBXR_ASSETS !== '0'; @@ -95,6 +116,11 @@ module.exports = { // Inject environment variables new webpack.DefinePlugin({ 'process.env.WEBXR_ASSETS_VERSION': JSON.stringify(WEBXR_ASSETS_VERSION), + 'process.env.CLIENT_TELEOP_VERSION': JSON.stringify(TELEOP_VERSION), + 'process.env.CLIENT_SDK_VERSION': JSON.stringify(CLIENT_SDK_VERSION), + 'process.env.CLIENT_GIT_REF': JSON.stringify(CLIENT_GIT_REF), + 'process.env.CLIENT_GIT_SHA': JSON.stringify(CLIENT_GIT_SHA), + 'process.env.CLIENT_BUILD_TIME': JSON.stringify(CLIENT_BUILD_TIME), }), // Copies WebXR input profile assets when available; always copies public and favicon