From 778a3267e2186bc4339eade7c3a4c87689aaf328 Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Sat, 2 May 2026 17:54:57 -0700 Subject: [PATCH 1/5] ci(webclient): publish to versioned /client// + add ?showVersion=1 overlay The docs workflow runs on every push to main and release/* and was unconditionally writing the freshly-built web client to gh-pages /client/. Whichever branch deployed last won, so a release-branch backport could silently overwrite the canonical /client/ URL with an older build. Mirror the docs sphinx-multiversion layout: each ref lands at /client// (slashes -> dashes), and /client/index.html is a thin redirect to ./main/ so existing bookmarks keep working. Bundle now embeds teleop version, CloudXR SDK version, git ref, sha, and build time via webpack DefinePlugin; the values are logged on startup and exposed by an opt-in overlay (append ?showVersion=1 to the URL). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../build-cloudxr-web-client/action.yml | 5 ++ .github/workflows/docs.yaml | 36 ++++++++-- .../webxr_client/src/BuildInfoOverlay.tsx | 70 +++++++++++++++++++ deps/cloudxr/webxr_client/src/index.tsx | 4 ++ deps/cloudxr/webxr_client/webpack.common.js | 30 ++++++++ 5 files changed, 141 insertions(+), 4 deletions(-) create mode 100644 deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx diff --git a/.github/actions/build-cloudxr-web-client/action.yml b/.github/actions/build-cloudxr-web-client/action.yml index 7dbf607d4..760364b7e 100644 --- a/.github/actions/build-cloudxr-web-client/action.yml +++ b/.github/actions/build-cloudxr-web-client/action.yml @@ -53,6 +53,11 @@ runs: working-directory: deps/cloudxr/webxr_client env: SDK_VERSION: ${{ steps.sdk-version.outputs.version }} + # Plumbed into the bundle via webpack DefinePlugin and surfaced by the + # in-app build-info overlay (append ?showVersion=1 to the URL) so we + # can tell at a glance which deployed version a user is running. + 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..e6c2ce509 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -138,15 +138,43 @@ jobs: name: webapp path: ./webapp - - name: Place web app under subpath + # Mirrors the docs multi-version layout: each ref lands at + # client// (slashes in the ref are replaced with "-"). We never + # write directly to `client/` so a push to a release branch can no + # longer overwrite the canonical /client/ URL with an older build. + # `client/index.html` is rewritten as a thin redirect to ./main/ so + # bookmarks of `/client/` keep working. + - name: Place web app under versioned subpath 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..e57fa858b --- /dev/null +++ b/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + */ + +// Build identity injected by webpack DefinePlugin (see webpack.common.js). +// Always logged on startup so devtools surfaces the running version even +// when the overlay is hidden. +export const BUILD_INFO = { + teleopVersion: process.env.CLIENT_TELEOP_VERSION || 'dev', + sdkVersion: process.env.CLIENT_SDK_VERSION || 'unknown', + gitRef: process.env.CLIENT_GIT_REF || 'unknown', + gitSha: process.env.CLIENT_GIT_SHA || 'unknown', + buildTime: process.env.CLIENT_BUILD_TIME || 'unknown', +} as const; + +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}` +); + +function isOverlayRequested(): boolean { + if (typeof window === 'undefined') return false; + const v = new URLSearchParams(window.location.search).get('showVersion'); + return v === '1' || v?.toLowerCase() === 'true'; +} + +/** + * Small fixed-corner overlay that prints the deployed build identity. + * Appended to directly so it stays visible even while the React + * tree is mounting / unmounting and is unaffected by xr-mode CSS that + * hides the 2D UI. + */ +export function mountBuildInfoOverlayIfRequested(): void { + if (typeof document === 'undefined' || !isOverlayRequested()) return; + if (document.getElementById('teleop-build-info-overlay')) return; + + const el = document.createElement('div'); + el.id = 'teleop-build-info-overlay'; + el.setAttribute('role', 'status'); + el.style.cssText = [ + 'position:fixed', + 'left:8px', + 'bottom:8px', + 'z-index:99999', + 'padding:8px 10px', + 'font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace', + 'color:#fff', + 'background:rgba(0,0,0,0.78)', + 'border:1px solid #76b900', + 'border-radius:4px', + 'pointer-events:auto', + 'max-width:360px', + 'word-break:break-all', + ].join(';'); + el.textContent = + `Teleop ${BUILD_INFO.teleopVersion} · SDK ${BUILD_INFO.sdkVersion}\n` + + `${BUILD_INFO.gitRef}@${BUILD_INFO.gitSha}\n` + + `built ${BUILD_INFO.buildTime}`; + el.style.whiteSpace = 'pre'; + el.title = 'Click to dismiss'; + el.addEventListener('click', () => el.remove()); + document.body.appendChild(el); +} diff --git a/deps/cloudxr/webxr_client/src/index.tsx b/deps/cloudxr/webxr_client/src/index.tsx index ceab27bc0..604fe568f 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,9 @@ function startApp() { } else { console.error('3d-ui container not found'); } + + // Independent of the React tree so it stays visible across XR mode toggles. + 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..2a295d684 100644 --- a/deps/cloudxr/webxr_client/webpack.common.js +++ b/deps/cloudxr/webxr_client/webpack.common.js @@ -16,10 +16,35 @@ */ 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'); +// Build identity: surfaced by the in-app overlay (open with ?showVersion=1) +// so we can tell which build a user is actually running on gh-pages. +const PKG_VERSION = require('./package.json').version; +const TELEOP_VERSION = (() => { + try { + return fs.readFileSync(path.resolve(__dirname, '../../../VERSION'), 'utf8').trim(); + } catch { + return ''; + } +})(); +function gitOrEmpty(args) { + try { + return execSync(`git ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] }) + .toString() + .trim(); + } catch { + return ''; + } +} +const CLIENT_GIT_REF = process.env.CLIENT_GIT_REF || gitOrEmpty('rev-parse --abbrev-ref HEAD') || 'unknown'; +const CLIENT_GIT_SHA = (process.env.CLIENT_GIT_SHA || gitOrEmpty('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 +120,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(PKG_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 From fd1a03a409d5d56b495a72c1d8718befca7d843d Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Sat, 2 May 2026 18:53:36 -0700 Subject: [PATCH 2/5] fix(webclient): keep build-info overlay sticky when ?showVersion=1 The overlay had a click-to-dismiss handler, so a single tap (deliberate or stray) made it disappear. With ?showVersion=1 being an explicit opt-in, the overlay should stay until the URL changes. Drop the click handler and switch the element to pointer-events:none so it never intercepts clicks on the underlying UI either. Co-Authored-By: Claude Opus 4.7 (1M context) --- deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx b/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx index e57fa858b..355c55784 100644 --- a/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx +++ b/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx @@ -55,7 +55,10 @@ export function mountBuildInfoOverlayIfRequested(): void { 'background:rgba(0,0,0,0.78)', 'border:1px solid #76b900', 'border-radius:4px', - 'pointer-events:auto', + // Click-through so the overlay never blocks UI interactions in the + // 2D page or the underlying canvas. + 'pointer-events:none', + 'user-select:text', 'max-width:360px', 'word-break:break-all', ].join(';'); @@ -64,7 +67,5 @@ export function mountBuildInfoOverlayIfRequested(): void { `${BUILD_INFO.gitRef}@${BUILD_INFO.gitSha}\n` + `built ${BUILD_INFO.buildTime}`; el.style.whiteSpace = 'pre'; - el.title = 'Click to dismiss'; - el.addEventListener('click', () => el.remove()); document.body.appendChild(el); } From 03c776e3131bb734b882afb164de0da58501f56f Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Sat, 2 May 2026 19:05:22 -0700 Subject: [PATCH 3/5] ci(docs): gate docs/webapp builds on changed paths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `changes` job that diffs the push/PR ref against its base and exports two booleans (`docs`, `webapp`). `build-docs` and `build-app` are now conditional on the relevant flag, and `deploy-docs` tolerates either upstream being skipped — gh-pages `keep_files: true` keeps the previously deployed copy of whichever artifact wasn't rebuilt. Cuts ~minutes off PRs that touch only docs or only the web client. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/docs.yaml | 90 ++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index e6c2ce509..783d3f1d2 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -8,8 +8,14 @@ # the preview manually via the `/preview-docs` slash command — 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. +# Builds are gated on changed paths via the `changes` job: docs are only +# rebuilt when docs/** changes, the web client is only rebuilt when +# webxr_client/.env.default/build action changes. Touching this workflow +# rebuilds both. gh-pages `keep_files: true` preserves the previously +# deployed copy of whichever artifact is skipped. +# Web client (npm) builds with public SDK for all PRs by default, or private +# SDK if the PR author is on ALLOWED_CLOUDXR_JS_PR_AUTHORS. Main/release +# (push) always uses the private NGC SDK key. # Allowlist for CXR.js SDK on PRs: Repo Settings → Variables → ALLOWED_CLOUDXR_JS_PR_AUTHORS (comma-separated GitHub logins). name: Build & deploy docs @@ -24,6 +30,7 @@ on: paths: - "docs/**" - ".github/workflows/docs.yaml" + - ".github/actions/build-cloudxr-web-client/**" - "deps/cloudxr/webxr_client/**" - "deps/cloudxr/.env.default" @@ -64,10 +71,68 @@ jobs: esac fi + # Skip the heavy npm/Sphinx builds when the relevant inputs didn't change. + # gh-pages keeps the previously deployed artifacts (`keep_files: true`), so + # not rebuilding here just means the existing version stays live. + 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 + # Resolve the base commit to diff against. PRs compare to the + # merge-base with the target branch; pushes compare to the previous + # tip. New branches / force pushes have no usable base, so we treat + # everything as changed. + if [ "$EVENT" = 'pull_request' ]; then + git fetch --no-tags --depth=1 origin "$BASE_REF" + base="$(git merge-base FETCH_HEAD HEAD || true)" + else + base="$BEFORE" + if [ -z "$base" ] || [ "$base" = '0000000000000000000000000000000000000000' ]; then + base="" + fi + fi + if [ -n "$base" ]; then + files="$(git diff --name-only "$base" HEAD)" + else + files="$(git ls-tree -r --name-only HEAD)" + fi + docs=false + webapp=false + while IFS= read -r f; do + [ -z "$f" ] && continue + 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 +166,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,18 +187,29 @@ 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] + # Run as long as at least one upstream build succeeded (we tolerate + # skips, since a skipped build means "reuse what's already on + # gh-pages") and neither failed. + if: >- + always() + && 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-docs.result != 'cancelled' + && needs.build-app.result != 'failure' && needs.build-app.result != 'cancelled' + && (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 @@ -145,6 +222,7 @@ jobs: # `client/index.html` is rewritten as a thin redirect to ./main/ so # bookmarks of `/client/` keep working. - name: Place web app under versioned subpath + if: needs.build-app.result == 'success' run: | set -euo pipefail if [ "${{ github.event_name }}" = 'pull_request' ]; then From dba7095a177b3e891cc966d7b7b3c61dd070b78a Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Sat, 2 May 2026 20:24:59 -0700 Subject: [PATCH 4/5] refactor(webclient/ci): trim explanatory comments and dead defensive code Drop SSR guards and idempotency checks from BuildInfoOverlay (browser bundle, called once), inline the TELEOP_VERSION read in webpack, replace the multi-line comment blocks with one-liners, and condense the deploy-docs `if` clause via !cancelled(). No behavior change; bundle still ships the same metadata and overlay, verified headlessly. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../build-cloudxr-web-client/action.yml | 3 - .github/workflows/docs.yaml | 70 ++++++------------- .../webxr_client/src/BuildInfoOverlay.tsx | 64 ++++------------- deps/cloudxr/webxr_client/src/index.tsx | 1 - deps/cloudxr/webxr_client/webpack.common.js | 25 +++---- 5 files changed, 45 insertions(+), 118 deletions(-) diff --git a/.github/actions/build-cloudxr-web-client/action.yml b/.github/actions/build-cloudxr-web-client/action.yml index 760364b7e..d7274dd06 100644 --- a/.github/actions/build-cloudxr-web-client/action.yml +++ b/.github/actions/build-cloudxr-web-client/action.yml @@ -53,9 +53,6 @@ runs: working-directory: deps/cloudxr/webxr_client env: SDK_VERSION: ${{ steps.sdk-version.outputs.version }} - # Plumbed into the bundle via webpack DefinePlugin and surfaced by the - # in-app build-info overlay (append ?showVersion=1 to the URL) so we - # can tell at a glance which deployed version a user is running. CLIENT_GIT_REF: ${{ github.ref_name }} CLIENT_GIT_SHA: ${{ github.sha }} run: | diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 783d3f1d2..7d2e312bb 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -2,21 +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). # -# Builds are gated on changed paths via the `changes` job: docs are only -# rebuilt when docs/** changes, the web client is only rebuilt when -# webxr_client/.env.default/build action changes. Touching this workflow -# rebuilds both. gh-pages `keep_files: true` preserves the previously -# deployed copy of whichever artifact is skipped. -# Web client (npm) builds with public SDK for all PRs by default, or private -# SDK if the PR author is on ALLOWED_CLOUDXR_JS_PR_AUTHORS. Main/release -# (push) always uses the private NGC SDK key. -# 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 @@ -71,9 +67,7 @@ jobs: esac fi - # Skip the heavy npm/Sphinx builds when the relevant inputs didn't change. - # gh-pages keeps the previously deployed artifacts (`keep_files: true`), so - # not rebuilding here just means the existing version stays live. + # gh-pages keep_files preserves whichever artifact isn't rebuilt this run. changes: name: Detect changed paths runs-on: ubuntu-latest @@ -93,38 +87,26 @@ jobs: BASE_REF: ${{ github.base_ref }} run: | set -euo pipefail - # Resolve the base commit to diff against. PRs compare to the - # merge-base with the target branch; pushes compare to the previous - # tip. New branches / force pushes have no usable base, so we treat - # everything as changed. if [ "$EVENT" = 'pull_request' ]; then git fetch --no-tags --depth=1 origin "$BASE_REF" base="$(git merge-base FETCH_HEAD HEAD || true)" - else + elif [ -n "$BEFORE" ] && [ "$BEFORE" != '0000000000000000000000000000000000000000' ]; then base="$BEFORE" - if [ -z "$base" ] || [ "$base" = '0000000000000000000000000000000000000000' ]; then - base="" - fi - fi - if [ -n "$base" ]; then - files="$(git diff --name-only "$base" HEAD)" else - files="$(git ls-tree -r --name-only HEAD)" + base="" # new branch / force push: treat everything as changed fi - docs=false - webapp=false + 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 - [ -z "$f" ] && continue case "$f" in - docs/*|.github/workflows/docs.yaml) - docs=true ;; + 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 "docs=$docs" >> "$GITHUB_OUTPUT" echo "webapp=$webapp" >> "$GITHUB_OUTPUT" echo "::notice::changes: docs=$docs webapp=$webapp (base=${base:-})" @@ -188,15 +170,12 @@ jobs: name: Deploy docs runs-on: ubuntu-latest needs: [check-repo, changes, build-docs, build-app] - # Run as long as at least one upstream build succeeded (we tolerate - # skips, since a skipped build means "reuse what's already on - # gh-pages") and neither failed. if: >- - always() + !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-docs.result != 'cancelled' - && needs.build-app.result != 'failure' && needs.build-app.result != 'cancelled' + && 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 @@ -215,12 +194,9 @@ jobs: name: webapp path: ./webapp - # Mirrors the docs multi-version layout: each ref lands at - # client// (slashes in the ref are replaced with "-"). We never - # write directly to `client/` so a push to a release branch can no - # longer overwrite the canonical /client/ URL with an older build. - # `client/index.html` is rewritten as a thin redirect to ./main/ so - # bookmarks of `/client/` keep working. + # 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: | diff --git a/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx b/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx index 355c55784..77a78706d 100644 --- a/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx +++ b/deps/cloudxr/webxr_client/src/BuildInfoOverlay.tsx @@ -1,71 +1,33 @@ /* * SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: Apache-2.0 - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 */ -// Build identity injected by webpack DefinePlugin (see webpack.common.js). -// Always logged on startup so devtools surfaces the running version even -// when the overlay is hidden. -export const BUILD_INFO = { - teleopVersion: process.env.CLIENT_TELEOP_VERSION || 'dev', - sdkVersion: process.env.CLIENT_SDK_VERSION || 'unknown', - gitRef: process.env.CLIENT_GIT_REF || 'unknown', - gitSha: process.env.CLIENT_GIT_SHA || 'unknown', - buildTime: process.env.CLIENT_BUILD_TIME || 'unknown', -} as const; +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}` ); -function isOverlayRequested(): boolean { - if (typeof window === 'undefined') return false; - const v = new URLSearchParams(window.location.search).get('showVersion'); - return v === '1' || v?.toLowerCase() === 'true'; -} - -/** - * Small fixed-corner overlay that prints the deployed build identity. - * Appended to directly so it stays visible even while the React - * tree is mounting / unmounting and is unaffected by xr-mode CSS that - * hides the 2D UI. - */ export function mountBuildInfoOverlayIfRequested(): void { - if (typeof document === 'undefined' || !isOverlayRequested()) return; - if (document.getElementById('teleop-build-info-overlay')) return; - + if (new URLSearchParams(window.location.search).get('showVersion') !== '1') return; const el = document.createElement('div'); el.id = 'teleop-build-info-overlay'; - el.setAttribute('role', 'status'); - el.style.cssText = [ - 'position:fixed', - 'left:8px', - 'bottom:8px', - 'z-index:99999', - 'padding:8px 10px', - 'font:12px/1.4 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace', - 'color:#fff', - 'background:rgba(0,0,0,0.78)', - 'border:1px solid #76b900', - 'border-radius:4px', - // Click-through so the overlay never blocks UI interactions in the - // 2D page or the underlying canvas. - 'pointer-events:none', - 'user-select:text', - 'max-width:360px', - 'word-break:break-all', - ].join(';'); + 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}`; - el.style.whiteSpace = 'pre'; document.body.appendChild(el); } diff --git a/deps/cloudxr/webxr_client/src/index.tsx b/deps/cloudxr/webxr_client/src/index.tsx index 604fe568f..6072847ef 100644 --- a/deps/cloudxr/webxr_client/src/index.tsx +++ b/deps/cloudxr/webxr_client/src/index.tsx @@ -36,7 +36,6 @@ function startApp() { console.error('3d-ui container not found'); } - // Independent of the React tree so it stays visible across XR mode toggles. mountBuildInfoOverlayIfRequested(); } diff --git a/deps/cloudxr/webxr_client/webpack.common.js b/deps/cloudxr/webxr_client/webpack.common.js index 2a295d684..9ac412910 100644 --- a/deps/cloudxr/webxr_client/webpack.common.js +++ b/deps/cloudxr/webxr_client/webpack.common.js @@ -22,27 +22,20 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); const webpack = require('webpack'); -// Build identity: surfaced by the in-app overlay (open with ?showVersion=1) -// so we can tell which build a user is actually running on gh-pages. -const PKG_VERSION = require('./package.json').version; -const TELEOP_VERSION = (() => { - try { - return fs.readFileSync(path.resolve(__dirname, '../../../VERSION'), 'utf8').trim(); - } catch { - return ''; - } -})(); -function gitOrEmpty(args) { +function git(cmd) { try { - return execSync(`git ${args}`, { stdio: ['ignore', 'pipe', 'ignore'] }) - .toString() - .trim(); + return execSync(`git ${cmd}`, { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch { return ''; } } -const CLIENT_GIT_REF = process.env.CLIENT_GIT_REF || gitOrEmpty('rev-parse --abbrev-ref HEAD') || 'unknown'; -const CLIENT_GIT_SHA = (process.env.CLIENT_GIT_SHA || gitOrEmpty('rev-parse HEAD') || 'unknown').slice(0, 12); +let TELEOP_VERSION = ''; +try { + TELEOP_VERSION = fs.readFileSync(path.resolve(__dirname, '../../../VERSION'), 'utf8').trim(); +} catch {} +const PKG_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. From 9a0554b9f0e17c30b4773a553873d7ab75256344 Mon Sep 17 00:00:00 2001 From: yanziz-nv Date: Sat, 2 May 2026 20:29:33 -0700 Subject: [PATCH 5/5] fix(webclient): source CLIENT_SDK_VERSION from build env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overlay was reading CLIENT_SDK_VERSION from package.json.version, which is the web-client app's own version and incidental to the actual CloudXR SDK that gets linked. Use the build-provided SDK_VERSION instead — same value the action derives from .env.default and that `npm install ../nvidia-cloudxr-${SDK_VERSION}.tgz` consumes — so the overlay always reflects what's really shipped, with package.json as a local-dev fallback. Co-Authored-By: Claude Opus 4.7 (1M context) --- deps/cloudxr/webxr_client/webpack.common.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/deps/cloudxr/webxr_client/webpack.common.js b/deps/cloudxr/webxr_client/webpack.common.js index 9ac412910..a23bc901d 100644 --- a/deps/cloudxr/webxr_client/webpack.common.js +++ b/deps/cloudxr/webxr_client/webpack.common.js @@ -33,7 +33,10 @@ let TELEOP_VERSION = ''; try { TELEOP_VERSION = fs.readFileSync(path.resolve(__dirname, '../../../VERSION'), 'utf8').trim(); } catch {} -const PKG_VERSION = require('./package.json').version; +// 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(); @@ -114,7 +117,7 @@ module.exports = { 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(PKG_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),