name: PR Verify on: pull_request: types: [opened, synchronize, reopened, ready_for_review] concurrency: group: pr-${{ github.event.pull_request.number }} cancel-in-progress: true permissions: contents: read env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true EVAL_KPIS_ARTIFACT: eval-kpis-pr EVAL_REPORT_ARTIFACT: eval-html-report AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }} S3_DATASETS_BUCKET: ${{ vars.S3_DATASETS_BUCKET }} # PR eval runs on x86_64 with the build script's default CUDA/Ubuntu. EVAL_CONFIG: x86_64-cuda12.6.3-ubuntu24.04 jobs: lint: name: "Lint" runs-on: ubuntu-latest timeout-minutes: 15 steps: - uses: actions/checkout@v5 with: fetch-depth: 0 persist-credentials: false - name: Build CI tools image env: AWS_CLI_PUBLIC_KEY: ${{ vars.AWS_CLI_PUBLIC_KEY }} run: | keyfile="$RUNNER_TEMP/aws-cli-public-key.asc" if [ -z "${AWS_CLI_PUBLIC_KEY:-}" ]; then echo "AWS_CLI_PUBLIC_KEY repository variable is not configured" >&2 exit 1 fi printf '%s\n' "$AWS_CLI_PUBLIC_KEY" > "$keyfile" DOCKER_BUILDKIT=1 docker build -f scripts/Dockerfile.ci . --network host --tag cuvslam-ci:local \ --secret id=aws_cli_public_key,src="$keyfile" - name: Run pre-commit (changed files) run: | docker run --rm \ -e GITHUB_BASE_REF \ -v "$(pwd):/cuvslam" \ -w /cuvslam \ cuvslam-ci:local \ bash -c 'git config --global --add safe.directory /cuvslam && pre-commit run --from-ref "origin/$GITHUB_BASE_REF" --to-ref HEAD' - name: Enforce ruleset/CODEOWNERS isolation run: | docker run --rm \ -e GITHUB_BASE_REF \ -v "$(pwd):/cuvslam" \ -w /cuvslam \ cuvslam-ci:local \ bash -c 'git config --global --add safe.directory /cuvslam && pre-commit run isolated-ruleset-change-ci --hook-stage manual' build-test-x86: name: "Build + Test (x86_64)" if: github.event.pull_request.head.repo.full_name == github.repository runs-on: [gpu] timeout-minutes: 120 env: EXTRA_CMAKE_ARGS: "-DCMAKE_CUDA_ARCHITECTURES=native" steps: - name: Verify GPU run: nvidia-smi - uses: actions/checkout@v5 with: lfs: false - name: Configure Ubuntu Ports mirror if: runner.arch == 'ARM64' run: | python3 - <<'PY' from pathlib import Path dockerfile = Path("scripts/Dockerfile") marker = "ARG DEBIAN_FRONTEND=noninteractive\n" mirror_setup = r"""ARG DEBIAN_FRONTEND=noninteractive # NVIDIA ARM runner networks cannot reliably reach ports.ubuntu.com. RUN set -eux; \ find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ -exec sed -Ei 's#https?://ports\.ubuntu\.com/ubuntu-ports#http://mirrors.ocf.berkeley.edu/ubuntu-ports#g' {} +; \ printf '%s\n' \ 'Acquire::IndexTargets::deb::DEP-11::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons-small::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons-hidpi::DefaultEnabled "false";' \ > /etc/apt/apt.conf.d/99-nvidia-ubuntu-ports-mirror """ content = dockerfile.read_text() if marker not in content: raise SystemExit(f"{marker.strip()} not found in {dockerfile}") dockerfile.write_text(content.replace(marker, mirror_setup, 1)) PY - name: Pull LFS test data run: git lfs pull --include="test_data/" - name: Resolve runner storage root if: vars.RUNNER_STORAGE_ROOT != '' run: echo "RUNNER_STORAGE_ROOT=${{ vars.RUNNER_STORAGE_ROOT }}" >> "$GITHUB_ENV" - name: Resolve local datasets root run: echo "RUNNER_LOCAL_DATASETS_ROOT=${{ vars.RUNNER_LOCAL_DATASETS_ROOT || '$HOME/.cache/cuvslam' }}" >> "$GITHUB_ENV" - name: Build CI tools image env: AWS_CLI_PUBLIC_KEY: ${{ vars.AWS_CLI_PUBLIC_KEY }} run: | keyfile="$RUNNER_TEMP/aws-cli-public-key.asc" if [ -z "${AWS_CLI_PUBLIC_KEY:-}" ]; then echo "AWS_CLI_PUBLIC_KEY repository variable is not configured" >&2 exit 1 fi printf '%s\n' "$AWS_CLI_PUBLIC_KEY" > "$keyfile" DOCKER_BUILDKIT=1 docker build -f scripts/Dockerfile.ci . --network host --tag cuvslam-ci:local \ --secret id=aws_cli_public_key,src="$keyfile" # Runs before the build so a broken tool fails in seconds. The package is # copied out of the read-only mount before installing, as run_eval.sh does, # to keep pip build artifacts out of the runner workspace. - name: Run Python tools tests run: | docker run --rm --network host \ -v "$(pwd):/cuvslam:ro" \ cuvslam-ci:local \ bash -c 'set -euo pipefail src="$(mktemp -d)" cp -a /cuvslam/tools/python_tools/. "$src/" pip install --quiet "$src" PYTHONPATH="$src" python3 -m unittest discover -v -s "$src/cuvslam_tools/tests"' - name: Verify eval prerequisites env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_S3_RO_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_S3_RO_SECRET_ACCESS_KEY }} run: | docker run --rm --network host \ -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION -e S3_DATASETS_BUCKET \ -e RUNNER_STORAGE_ROOT -e RUNNER_LOCAL_DATASETS_ROOT \ -v "$(pwd):/cuvslam:ro" \ -v "$RUNNER_LOCAL_DATASETS_ROOT:$RUNNER_LOCAL_DATASETS_ROOT" \ -w /cuvslam \ cuvslam-ci:local ./scripts/check_eval_prerequisites.sh - name: Stage eval datasets env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_S3_RO_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_S3_RO_SECRET_ACCESS_KEY }} run: | mkdir -p "$RUNNER_LOCAL_DATASETS_ROOT" docker run --rm --network host \ -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION -e S3_DATASETS_BUCKET \ -e RUNNER_LOCAL_DATASETS_ROOT \ -v "$(pwd):/cuvslam:ro" \ -v "$RUNNER_LOCAL_DATASETS_ROOT:$RUNNER_LOCAL_DATASETS_ROOT" \ -w /cuvslam \ cuvslam-ci:local ./scripts/stage_eval_datasets.sh - name: Build run: ./scripts/build_cuvslam_in_docker.sh Release ./output - name: Run C++ tests run: ./scripts/test_cuvslam_in_docker.sh ./output - name: Run Python tests run: ./scripts/test_pycuvslam_in_docker.sh ./output - name: Run cuVSLAM evaluation env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_S3_RO_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_S3_RO_SECRET_ACCESS_KEY }} run: | RUN_ID="pr-${{ github.event.pull_request.number }}" \ KPI_HISTORY_DIR="${RUNNER_STORAGE_ROOT:?RUNNER_STORAGE_ROOT must be set}/cuvslam-ci/kpi-history/${EVAL_CONFIG}" \ ./scripts/eval_cuvslam_in_docker.sh ./output - name: Render KPI table run: | KPI_REPORT_JSON=$(find output/eval -name 'kpi_*.report.json' -print -quit) python3 scripts/cuvslam_kpi_report.py render \ --report_json "$KPI_REPORT_JSON" \ --config "$EVAL_CONFIG" \ --output output/eval/kpi-report.md - name: Build documentation run: ./scripts/build_docs_in_docker.sh ./output - name: Upload documentation if: always() uses: actions/upload-artifact@v7 with: name: docs-html path: output/docs/ retention-days: 30 if-no-files-found: warn - name: Generate test summary if: always() run: | python3 scripts/junit-to-markdown.py x86_64 C++ output/cpp-test-results.xml > output/test-summary.txt python3 scripts/junit-to-markdown.py x86_64 Python output/python-test-output.txt >> output/test-summary.txt - name: Upload test results if: always() uses: actions/upload-artifact@v7 with: name: test-results-x86_64 path: | output/cpp-test-results.xml output/test-summary.txt retention-days: 30 if-no-files-found: warn - name: Upload KPI results if: always() uses: actions/upload-artifact@v7 with: name: ${{ env.EVAL_KPIS_ARTIFACT }} path: | output/eval/kpi_*.json output/eval/kpi-report.md retention-days: 30 if-no-files-found: warn - name: Upload eval HTML report if: always() uses: actions/upload-artifact@v7 with: name: ${{ env.EVAL_REPORT_ARTIFACT }} path: output/eval/stats/ retention-days: 30 if-no-files-found: warn build-test-orin: name: "Build + Test (Orin)" if: github.event.pull_request.head.repo.full_name == github.repository runs-on: [jetson-orin] timeout-minutes: 120 # Orin uses BASE_IMAGE override; CUDA_VERSION is not used for the Docker base env: BASE_IMAGE: "nvcr.io/nvidia/12.6.11-devel:12.6.11-devel-aarch64-ubuntu22.04" UBUNTU_VERSION: "22.04" EXTRA_CMAKE_ARGS: "-DCMAKE_CUDA_ARCHITECTURES=87" steps: - name: System info run: | cat /etc/nv_tegra_release 2>/dev/null || echo "No tegra release file" dpkg-query -W nvidia-jetpack 2>/dev/null || echo "nvidia-jetpack not installed" uname -r - uses: actions/checkout@v5 with: lfs: false - name: Configure Ubuntu Ports mirror if: runner.arch == 'ARM64' run: | python3 - <<'PY' from pathlib import Path dockerfile = Path("scripts/Dockerfile") marker = "ARG DEBIAN_FRONTEND=noninteractive\n" mirror_setup = r"""ARG DEBIAN_FRONTEND=noninteractive # NVIDIA ARM runner networks cannot reliably reach ports.ubuntu.com. RUN set -eux; \ find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ -exec sed -Ei 's#https?://ports\.ubuntu\.com/ubuntu-ports#http://mirrors.ocf.berkeley.edu/ubuntu-ports#g' {} +; \ printf '%s\n' \ 'Acquire::IndexTargets::deb::DEP-11::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons-small::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons-hidpi::DefaultEnabled "false";' \ > /etc/apt/apt.conf.d/99-nvidia-ubuntu-ports-mirror """ content = dockerfile.read_text() if marker not in content: raise SystemExit(f"{marker.strip()} not found in {dockerfile}") dockerfile.write_text(content.replace(marker, mirror_setup, 1)) PY - name: Pull LFS test data run: git lfs pull --include="test_data/" - name: Build run: ./scripts/build_cuvslam_in_docker.sh Release ./output - name: Run C++ tests run: ./scripts/test_cuvslam_in_docker.sh ./output - name: Run Python tests run: ./scripts/test_pycuvslam_in_docker.sh ./output - name: Generate test summary if: always() run: | python3 scripts/junit-to-markdown.py Orin C++ output/cpp-test-results.xml > output/test-summary.txt python3 scripts/junit-to-markdown.py Orin Python output/python-test-output.txt >> output/test-summary.txt - name: Upload test results if: always() uses: actions/upload-artifact@v7 with: name: test-results-orin path: | output/cpp-test-results.xml output/test-summary.txt retention-days: 30 if-no-files-found: warn build-test-thor: name: "Build + Test (Thor)" if: github.event.pull_request.head.repo.full_name == github.repository runs-on: [jetson-thor] timeout-minutes: 180 env: CUDA_VERSION: "13.0.1" UBUNTU_VERSION: "24.04" EXTRA_CMAKE_ARGS: "-DCMAKE_CUDA_ARCHITECTURES=110" steps: - name: System info run: | cat /etc/nv_tegra_release 2>/dev/null || echo "No tegra release file" dpkg-query -W nvidia-jetpack 2>/dev/null || echo "nvidia-jetpack not installed" uname -r - uses: actions/checkout@v5 with: lfs: false - name: Configure Ubuntu Ports mirror if: runner.arch == 'ARM64' run: | python3 - <<'PY' from pathlib import Path dockerfile = Path("scripts/Dockerfile") marker = "ARG DEBIAN_FRONTEND=noninteractive\n" mirror_setup = r"""ARG DEBIAN_FRONTEND=noninteractive # NVIDIA ARM runner networks cannot reliably reach ports.ubuntu.com. RUN set -eux; \ find /etc/apt -type f \( -name '*.list' -o -name '*.sources' \) \ -exec sed -Ei 's#https?://ports\.ubuntu\.com/ubuntu-ports#http://mirrors.ocf.berkeley.edu/ubuntu-ports#g' {} +; \ printf '%s\n' \ 'Acquire::IndexTargets::deb::DEP-11::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons-small::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons::DefaultEnabled "false";' \ 'Acquire::IndexTargets::deb::DEP-11-icons-hidpi::DefaultEnabled "false";' \ > /etc/apt/apt.conf.d/99-nvidia-ubuntu-ports-mirror """ content = dockerfile.read_text() if marker not in content: raise SystemExit(f"{marker.strip()} not found in {dockerfile}") dockerfile.write_text(content.replace(marker, mirror_setup, 1)) PY - name: Pull LFS test data run: git lfs pull --include="test_data/" - name: Build uses: nick-fields/retry@v3 with: max_attempts: 3 timeout_minutes: 60 retry_wait_seconds: 30 command: ./scripts/build_cuvslam_in_docker.sh Release ./output - name: Run C++ tests uses: nick-fields/retry@v3 with: max_attempts: 3 timeout_minutes: 30 retry_wait_seconds: 30 command: ./scripts/test_cuvslam_in_docker.sh ./output - name: Run Python tests uses: nick-fields/retry@v3 with: max_attempts: 3 timeout_minutes: 30 retry_wait_seconds: 30 command: ./scripts/test_pycuvslam_in_docker.sh ./output - name: Generate test summary if: always() run: | python3 scripts/junit-to-markdown.py Thor C++ output/cpp-test-results.xml > output/test-summary.txt python3 scripts/junit-to-markdown.py Thor Python output/python-test-output.txt >> output/test-summary.txt - name: Upload test results if: always() uses: actions/upload-artifact@v7 with: name: test-results-thor path: | output/cpp-test-results.xml output/test-summary.txt retention-days: 30 if-no-files-found: warn # Smoke check for a non-default CMake configuration. Catches build/link and test # breakages the default (USE_RERUN=OFF) build never exercises. Builds and runs the # C++ unit tests; no wheels, docs, or evaluation (nightly runs the full USE_RERUN build). build-rerun-x86: name: "Build + Test (USE_RERUN, x86_64)" if: github.event.pull_request.head.repo.full_name == github.repository runs-on: [gpu] timeout-minutes: 120 steps: - name: Verify GPU run: nvidia-smi - uses: actions/checkout@v5 with: lfs: false - name: Pull LFS test data run: git lfs pull --include="test_data/" - name: Build with USE_RERUN env: # PR binaries are built and tested on the same GPU runner. EXTRA_CMAKE_ARGS: "-DUSE_RERUN=ON -DCMAKE_CUDA_ARCHITECTURES=native" run: ./scripts/build_cuvslam_in_docker.sh Release ./output-rerun - name: Run C++ tests run: ./scripts/test_cuvslam_in_docker.sh ./output-rerun test-report: name: "Post Test Report" if: always() && github.event.pull_request.head.repo.full_name == github.repository needs: [build-test-x86, build-test-orin, build-test-thor] runs-on: ubuntu-latest timeout-minutes: 5 permissions: pull-requests: write actions: read steps: - name: Download all test summaries uses: actions/download-artifact@v8 with: pattern: '{test-results-*,${{ env.EVAL_KPIS_ARTIFACT }}}' path: results - name: Build combined report run: | { echo "## Test Results" echo "" echo "| Status | Platform | Language | Total | Passed | Failed | Errors | Skipped |" echo "|:------:|----------|----------|------:|-------:|-------:|-------:|--------:|" for f in results/*/test-summary.txt; do [ -f "$f" ] || continue while IFS='|' read -r status platform lang total passed failed errors skipped details; do if [ "$status" = "pass" ]; then icon="✅"; elif [ "$status" = "fail" ]; then icon="❌"; else icon="⚠️"; fi echo "| $icon | $platform | $lang | $total | $passed | $failed | $errors | $skipped |" done < "$f" done echo "" has_failures=false for f in results/*/test-summary.txt; do [ -f "$f" ] || continue while IFS='|' read -r status platform lang total passed failed errors skipped details; do if [ -n "$details" ] && [ "$details" != "" ]; then has_failures=true fi done < "$f" done if [ "$has_failures" = "true" ]; then echo "
Failed tests" echo "" for f in results/*/test-summary.txt; do [ -f "$f" ] || continue while IFS='|' read -r status platform lang total passed failed errors skipped details; do if [ -n "$details" ] && [ "$details" != "" ]; then echo "**$platform / $lang:**" echo "$details" | python3 -c 'import sys,json;[print("- **%s**: %s"%(n,m))for n,m in json.loads(sys.stdin.read())]' echo "" fi done < "$f" done echo "
" fi } > report.md KPI_TABLE=$(find results -name 'kpi-report.md' -print -quit 2>/dev/null || true) if [ -f "$KPI_TABLE" ]; then { echo "" echo "## cuVSLAM Evaluation KPIs" echo "" cat "$KPI_TABLE" echo "" } >> report.md fi cat report.md - name: Post PR comment uses: actions/github-script@v7 with: script: | const fs = require('fs'); let body = fs.readFileSync('report.md', 'utf8'); const { data: { artifacts } } = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.runId, }); if (artifacts.length > 0) { const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; body += '\n### Artifacts\n\n'; for (const a of artifacts) { body += `- [${a.name}](${runUrl}/artifacts/${a.id})\n`; } } const marker = ''; const fullBody = `${marker}\n${body}`; const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, }); const existing = comments.find(c => c.body.includes(marker)); if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body: fullBody, }); } else { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, body: fullBody, }); } pr-status: name: "PR status" runs-on: ubuntu-latest if: always() needs: [lint, build-test-x86, build-test-orin, build-test-thor, build-rerun-x86] steps: - name: Check results env: LINT_RESULT: ${{ needs.lint.result }} X86_RESULT: ${{ needs.build-test-x86.result }} ORIN_RESULT: ${{ needs.build-test-orin.result }} THOR_RESULT: ${{ needs.build-test-thor.result }} RERUN_RESULT: ${{ needs.build-rerun-x86.result }} IS_SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository }} run: | set -euo pipefail echo "lint=$LINT_RESULT x86=$X86_RESULT orin=$ORIN_RESULT thor=$THOR_RESULT rerun=$RERUN_RESULT same_repo=$IS_SAME_REPO" if [ "$LINT_RESULT" != "success" ]; then echo "::error::Lint did not succeed (result: $LINT_RESULT)" exit 1 fi if [ "$IS_SAME_REPO" = "true" ]; then for pair in "x86_64:$X86_RESULT" "Orin:$ORIN_RESULT" "Thor:$THOR_RESULT" "Rerun:$RERUN_RESULT"; do name="${pair%%:*}" result="${pair##*:}" if [ "$result" != "success" ]; then echo "::error::Required GPU job '$name' did not succeed (result: $result)" exit 1 fi done else echo "Fork PR detected: GPU build/test is skipped by design. Lint-only gate satisfied." fi echo "PR status: all required checks passed."