Skip to content

Merge branch 'main' into tensor-bridge-749 #3364

Merge branch 'main' into tensor-bridge-749

Merge branch 'main' into tensor-bridge-749 #3364

Workflow file for this run

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
# Note: This name is referred to in the test job, so make sure any changes are sync'd up!
# Further this is referencing a run in the backport branch to fetch old bindings.
name: "CI"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: true
on:
push:
branches:
- "pull-request/[0-9]+"
- "main"
tags:
# Build release artifacts from tag refs so setuptools-scm resolves exact
# release versions instead of .dev+local variants.
- "v*"
- "cuda-core-v*"
- "cuda-pathfinder-v*"
schedule:
# every 24 hours at midnight UTC
- cron: "0 0 * * *"
jobs:
ci-vars:
runs-on: ubuntu-latest
outputs:
CUDA_BUILD_VER: ${{ steps.get-vars.outputs.cuda_build_ver }}
CUDA_PREV_BUILD_VER: ${{ steps.get-vars.outputs.cuda_prev_build_ver }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Get CUDA build versions
id: get-vars
run: |
cuda_build_ver=$(yq '.cuda.build.version' ci/versions.yml)
echo "cuda_build_ver=$cuda_build_ver" >> $GITHUB_OUTPUT
cuda_prev_build_ver=$(yq '.cuda.prev_build.version' ci/versions.yml)
echo "cuda_prev_build_ver=$cuda_prev_build_ver" >> $GITHUB_OUTPUT
should-skip:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.get-should-skip.outputs.skip }}
doc-only: ${{ steps.get-should-skip.outputs.doc_only }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Compute whether to skip builds and tests
id: get-should-skip
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euxo pipefail
if ${{ startsWith(github.ref_name, 'pull-request/') }}; then
pr_number="$(grep -Po '(\d+)$' <<< '${{ github.ref_name }}')"
pr_title="$(gh pr view "${pr_number}" --json title --jq '.title')"
skip="$(echo "${pr_title}" | grep -q '\[no-ci\]' && echo true || echo false)"
doc_only="$(echo "${pr_title}" | grep -q '\[doc-only\]' && echo true || echo false)"
else
skip=false
doc_only=false
fi
echo "skip=${skip}" >> "$GITHUB_OUTPUT"
echo "doc_only=${doc_only}" >> "$GITHUB_OUTPUT"
# Detect which top-level modules were touched by the PR so downstream build
# and test jobs can avoid rebuilding/retesting modules unaffected by the
# change. See issue #299.
#
# Dependency graph (verified in pyproject.toml files):
# cuda_pathfinder -> (no internal deps)
# cuda_bindings -> cuda_pathfinder
# cuda_core -> cuda_pathfinder, cuda_bindings
# cuda_python -> cuda_bindings (meta package)
#
# A change to cuda_pathfinder (or shared infra) forces a rebuild of every
# downstream module. A change to cuda_bindings forces rebuild of cuda_core.
# A change to cuda_core alone skips rebuilding/retesting cuda_bindings.
# On push to main, tag refs, schedule, or workflow_dispatch events we
# unconditionally run everything because there is no meaningful "changed
# paths" baseline for those events.
detect-changes:
runs-on: ubuntu-latest
outputs:
bindings: ${{ steps.compose.outputs.bindings }}
core: ${{ steps.compose.outputs.core }}
pathfinder: ${{ steps.compose.outputs.pathfinder }}
python_meta: ${{ steps.compose.outputs.python_meta }}
test_helpers: ${{ steps.compose.outputs.test_helpers }}
shared: ${{ steps.compose.outputs.shared }}
build_bindings: ${{ steps.compose.outputs.build_bindings }}
build_core: ${{ steps.compose.outputs.build_core }}
build_pathfinder: ${{ steps.compose.outputs.build_pathfinder }}
test_bindings: ${{ steps.compose.outputs.test_bindings }}
test_core: ${{ steps.compose.outputs.test_core }}
test_pathfinder: ${{ steps.compose.outputs.test_pathfinder }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
# copy-pr-bot pushes every PR (whether it targets main or a backport
# branch such as 12.9.x) to pull-request/<N>, so the base branch
# cannot be inferred from github.ref_name. Look it up via the
# upstream PR metadata so the diff below is rooted at the right place.
- name: Resolve PR base branch
id: pr-info
if: ${{ startsWith(github.ref_name, 'pull-request/') }}
uses: nv-gha-runners/get-pr-info@main
- name: Detect changed paths
id: filter
if: ${{ startsWith(github.ref_name, 'pull-request/') }}
env:
# GitHub Actions evaluates step-level `env:` expressions eagerly —
# the step's `if:` gate does NOT short-circuit them. On non-PR
# events (push/tag/schedule), `pr-info` is skipped and its outputs
# are empty strings, so `fromJSON('')` would raise a template error
# and fail the step despite `if:` being false. Guard the
# `fromJSON` call with a short-circuit so the expression resolves
# to an empty string on non-PR events; the step is still gated
# off by `if:`, so `BASE_REF` is never consumed there.
BASE_REF: ${{ steps.pr-info.outputs.pr-info && fromJSON(steps.pr-info.outputs.pr-info).base.ref || '' }}
run: |
# Diff against the merge base with the PR's actual target branch.
# Uses merge-base so diverged branches only show files changed on
# the PR side, not upstream commits.
if [[ -z "${BASE_REF}" ]]; then
echo "Could not resolve PR base branch from get-pr-info output" >&2
exit 1
fi
base=$(git merge-base HEAD "origin/${BASE_REF}")
changed=$(git diff --name-only "$base"...HEAD)
has_match() {
grep -qE "$1" <<< "$changed" && echo true || echo false
}
{
echo "bindings=$(has_match '^cuda_bindings/')"
echo "core=$(has_match '^cuda_core/')"
echo "pathfinder=$(has_match '^cuda_pathfinder/')"
echo "python_meta=$(has_match '^cuda_python/')"
echo "test_helpers=$(has_match '^cuda_python_test_helpers/')"
echo "shared=$(has_match '^(\.github/|ci/|scripts/|toolshed/|conftest\.py$|pyproject\.toml$|pixi\.(toml|lock)$|pytest\.ini$|ruff\.toml$)')"
} >> "$GITHUB_OUTPUT"
- name: Compose gating outputs
id: compose
env:
IS_PR: ${{ startsWith(github.ref_name, 'pull-request/') }}
BINDINGS: ${{ steps.filter.outputs.bindings || 'false' }}
CORE: ${{ steps.filter.outputs.core || 'false' }}
PATHFINDER: ${{ steps.filter.outputs.pathfinder || 'false' }}
PYTHON_META: ${{ steps.filter.outputs.python_meta || 'false' }}
TEST_HELPERS: ${{ steps.filter.outputs.test_helpers || 'false' }}
SHARED: ${{ steps.filter.outputs.shared || 'false' }}
run: |
set -euxo pipefail
# Non-PR events (push to main, tag push, schedule, workflow_dispatch)
# always exercise the full pipeline because there is no baseline for
# a meaningful diff.
if [[ "${IS_PR}" != "true" ]]; then
bindings=true
core=true
pathfinder=true
python_meta=true
test_helpers=true
shared=true
else
bindings="${BINDINGS}"
core="${CORE}"
pathfinder="${PATHFINDER}"
python_meta="${PYTHON_META}"
test_helpers="${TEST_HELPERS}"
shared="${SHARED}"
fi
or_flag() {
for v in "$@"; do
if [[ "${v}" == "true" ]]; then
echo "true"
return
fi
done
echo "false"
}
# Build gating: pathfinder change forces rebuild of bindings and
# core; bindings change forces rebuild of core. shared changes force
# a full rebuild.
build_pathfinder="$(or_flag "${shared}" "${pathfinder}")"
build_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}")"
build_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}")"
# Test gating: tests for a module must run whenever that module, any
# of its runtime dependencies, the shared test helper package, or
# shared infra changes. pathfinder tests are cheap and always run.
test_pathfinder=true
test_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${test_helpers}")"
test_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}" "${test_helpers}")"
{
echo "bindings=${bindings}"
echo "core=${core}"
echo "pathfinder=${pathfinder}"
echo "python_meta=${python_meta}"
echo "test_helpers=${test_helpers}"
echo "shared=${shared}"
echo "build_bindings=${build_bindings}"
echo "build_core=${build_core}"
echo "build_pathfinder=${build_pathfinder}"
echo "test_bindings=${test_bindings}"
echo "test_core=${test_core}"
echo "test_pathfinder=${test_pathfinder}"
} >> "$GITHUB_OUTPUT"
# NOTE: Build jobs are intentionally split by platform rather than using a single
# matrix. This allows each test job to depend only on its corresponding build,
# so faster platforms can proceed through build & test without waiting for slower
# ones. Keep these job definitions textually identical except for:
# - host-platform value
# - if: condition (build-linux-64 omits doc-only check since it's needed for docs)
build-linux-64:
needs:
- ci-vars
- should-skip
strategy:
fail-fast: false
matrix:
host-platform:
- linux-64
name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) }}
secrets: inherit
uses: ./.github/workflows/build-wheel.yml
with:
host-platform: ${{ matrix.host-platform }}
cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }}
# See build-linux-64 for why build jobs are split by platform.
build-linux-aarch64:
needs:
- ci-vars
- should-skip
strategy:
fail-fast: false
matrix:
host-platform:
- linux-aarch64
name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }}
secrets: inherit
uses: ./.github/workflows/build-wheel.yml
with:
host-platform: ${{ matrix.host-platform }}
cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }}
# See build-linux-64 for why build jobs are split by platform.
build-windows:
needs:
- ci-vars
- should-skip
strategy:
fail-fast: false
matrix:
host-platform:
- win-64
name: Build ${{ matrix.host-platform }}, CUDA ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }}
secrets: inherit
uses: ./.github/workflows/build-wheel.yml
with:
host-platform: ${{ matrix.host-platform }}
cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
prev-cuda-version: ${{ needs.ci-vars.outputs.CUDA_PREV_BUILD_VER }}
# NOTE: test-sdist jobs are split by platform (mirroring build-* and test-wheel-*)
# so platform-specific sources (e.g. cuda_bindings/*_windows.pyx selected by
# build_hooks.py) are exercised on their target OS. Keep these job definitions
# textually identical except for:
# - host-platform value
# - uses: (test-sdist-linux.yml vs test-sdist-windows.yml)
test-sdist-linux:
needs:
- ci-vars
- should-skip
name: Test sdist linux-64
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }}
secrets: inherit
uses: ./.github/workflows/test-sdist-linux.yml
with:
host-platform: linux-64
cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
# See test-sdist-linux for why sdist test jobs are split by platform.
test-sdist-windows:
needs:
- ci-vars
- should-skip
name: Test sdist win-64
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.skip) && !fromJSON(needs.should-skip.outputs.doc-only) }}
secrets: inherit
uses: ./.github/workflows/test-sdist-windows.yml
with:
host-platform: win-64
cuda-version: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
# NOTE: Test jobs are split by platform for the same reason as build jobs (see
# build-linux-64). Keep these job definitions textually identical except for:
# - host-platform value
# - build job under needs:
# - uses: (test-wheel-linux.yml vs test-wheel-windows.yml)
test-linux-64:
strategy:
fail-fast: false
matrix:
host-platform:
- linux-64
name: Test ${{ matrix.host-platform }}
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }}
permissions:
contents: read # This is required for actions/checkout
needs:
- ci-vars
- should-skip
- detect-changes
- build-linux-64
secrets: inherit
uses: ./.github/workflows/test-wheel-linux.yml
with:
build-type: pull-request
host-platform: ${{ matrix.host-platform }}
build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
nruns: ${{ (github.event_name == 'schedule' && 100) || 1}}
skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }}
# See test-linux-64 for why test jobs are split by platform.
test-linux-aarch64:
strategy:
fail-fast: false
matrix:
host-platform:
- linux-aarch64
name: Test ${{ matrix.host-platform }}
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }}
permissions:
contents: read # This is required for actions/checkout
needs:
- ci-vars
- should-skip
- detect-changes
- build-linux-aarch64
secrets: inherit
uses: ./.github/workflows/test-wheel-linux.yml
with:
build-type: pull-request
host-platform: ${{ matrix.host-platform }}
build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
nruns: ${{ (github.event_name == 'schedule' && 100) || 1}}
skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }}
# See test-linux-64 for why test jobs are split by platform.
test-windows:
strategy:
fail-fast: false
matrix:
host-platform:
- win-64
name: Test ${{ matrix.host-platform }}
if: ${{ github.repository_owner == 'nvidia' && !fromJSON(needs.should-skip.outputs.doc-only) }}
permissions:
contents: read # This is required for actions/checkout
needs:
- ci-vars
- should-skip
- detect-changes
- build-windows
secrets: inherit
uses: ./.github/workflows/test-wheel-windows.yml
with:
build-type: pull-request
host-platform: ${{ matrix.host-platform }}
build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
nruns: ${{ (github.event_name == 'schedule' && 100) || 1}}
skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }}
doc:
name: Docs
if: ${{ github.repository_owner == 'nvidia' }}
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
id-token: write
contents: write
pull-requests: write
needs:
- ci-vars
- build-linux-64
secrets: inherit
uses: ./.github/workflows/build-docs.yml
with:
is-release: ${{ github.ref_type == 'tag' }}
checks:
name: Check job status
if: always()
runs-on: ubuntu-latest
needs:
- should-skip
- detect-changes
- test-sdist-linux
- test-sdist-windows
- test-linux-64
- test-linux-aarch64
- test-windows
- doc
steps:
- name: Exit
run: |
# if any dependencies were cancelled or failed, that's a failure
#
# see https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#always
# and https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks#handling-skipped-but-required-checks
# for why this cannot be encoded in the job-level `if:` field
#
# TL; DR: `$REASONS`
#
# The intersection of skipped-as-success and required status checks
# creates a scenario where if you DON'T `always()` run this job, the
# status check UI will block merging and if you DO `always()` run and
# a dependency is _cancelled_ (due to a critical failure, which is
# somehow not considered a failure ¯\_(ツ)_/¯) then the critically
# failing job(s) will timeout causing a cancellation here and the
# build to succeed which we don't want (originally this was just
# 'exit 0')
#
# Note: When [doc-only] is in PR title, test jobs are intentionally
# skipped and should not cause failure.
#
# detect-changes gates whether heavy test matrices run at all; if it
# does not succeed, downstream test jobs are skipped rather than
# failed, which would otherwise go unnoticed here. Require its
# success explicitly so a broken gating step cannot masquerade as a
# green CI run.
doc_only=${{ needs.should-skip.outputs.doc-only }}
if ${{ needs.detect-changes.result != 'success' }}; then
exit 1
fi
if ${{ needs.doc.result == 'cancelled' || needs.doc.result == 'failure' }}; then
exit 1
fi
if ${{ needs.test-sdist-linux.result == 'cancelled' ||
needs.test-sdist-linux.result == 'failure' ||
needs.test-sdist-windows.result == 'cancelled' ||
needs.test-sdist-windows.result == 'failure' }}; then
exit 1
fi
if [[ "${doc_only}" != "true" ]]; then
if ${{ needs.test-linux-64.result == 'cancelled' ||
needs.test-linux-64.result == 'failure' ||
needs.test-linux-aarch64.result == 'cancelled' ||
needs.test-linux-aarch64.result == 'failure' ||
needs.test-windows.result == 'cancelled' ||
needs.test-windows.result == 'failure' }}; then
exit 1
fi
fi
exit 0