diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7e0c65224..525c210889 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,151 @@ jobs: 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/, 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: + BASE_REF: ${{ 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 @@ -151,6 +296,7 @@ jobs: needs: - ci-vars - should-skip + - detect-changes - build-linux-64 secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml @@ -159,6 +305,7 @@ jobs: 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: @@ -174,6 +321,7 @@ jobs: needs: - ci-vars - should-skip + - detect-changes - build-linux-aarch64 secrets: inherit uses: ./.github/workflows/test-wheel-linux.yml @@ -182,6 +330,7 @@ jobs: 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: @@ -197,6 +346,7 @@ jobs: needs: - ci-vars - should-skip + - detect-changes - build-windows secrets: inherit uses: ./.github/workflows/test-wheel-windows.yml @@ -205,6 +355,7 @@ jobs: 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 @@ -228,6 +379,7 @@ jobs: runs-on: ubuntu-latest needs: - should-skip + - detect-changes - test-linux-64 - test-linux-aarch64 - test-windows @@ -254,7 +406,16 @@ jobs: # # 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 diff --git a/.github/workflows/test-wheel-linux.yml b/.github/workflows/test-wheel-linux.yml index 270ed445a9..796040333b 100644 --- a/.github/workflows/test-wheel-linux.yml +++ b/.github/workflows/test-wheel-linux.yml @@ -22,6 +22,13 @@ on: nruns: type: number default: 1 + # When true, cuda.bindings tests (and the Cython tests that depend on + # them) are skipped even when CTK majors match. Callers set this based + # on the output of the detect-changes job in ci.yml so PRs that only + # touch unrelated modules avoid the expensive bindings test suite. + skip-bindings-test: + type: boolean + default: false defaults: run: @@ -113,6 +120,7 @@ jobs: LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ github.sha }} + SKIP_BINDINGS_TEST_OVERRIDE: ${{ inputs.skip-bindings-test && '1' || '0' }} run: ./ci/tools/env-vars test - name: Download cuda-pathfinder build artifacts @@ -122,21 +130,21 @@ jobs: path: ./cuda_pathfinder - name: Download cuda-python build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cuda-python-wheel path: . - name: Download cuda.bindings build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - name: Download cuda-python & cuda.bindings build artifacts from the prior branch - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '1'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '1' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/.github/workflows/test-wheel-windows.yml b/.github/workflows/test-wheel-windows.yml index 91f098b0e9..765823c6bf 100644 --- a/.github/workflows/test-wheel-windows.yml +++ b/.github/workflows/test-wheel-windows.yml @@ -22,6 +22,13 @@ on: nruns: type: number default: 1 + # When true, cuda.bindings tests (and the Cython tests that depend on + # them) are skipped even when CTK majors match. Callers set this based + # on the output of the detect-changes job in ci.yml so PRs that only + # touch unrelated modules avoid the expensive bindings test suite. + skip-bindings-test: + type: boolean + default: false jobs: compute-matrix: @@ -107,6 +114,7 @@ jobs: LOCAL_CTK: ${{ matrix.LOCAL_CTK }} PY_VER: ${{ matrix.PY_VER }} SHA: ${{ github.sha }} + SKIP_BINDINGS_TEST_OVERRIDE: ${{ inputs.skip-bindings-test && '1' || '0' }} shell: bash --noprofile --norc -xeuo pipefail {0} run: ./ci/tools/env-vars test @@ -117,21 +125,21 @@ jobs: path: ./cuda_pathfinder - name: Download cuda-python build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: cuda-python-wheel path: . - name: Download cuda.bindings build artifacts - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '0' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }} path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }} - name: Download cuda-python & cuda.bindings build artifacts from the prior branch - if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '1'}} + if: ${{ env.USE_BACKPORT_BINDINGS == '1' }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/ci/tools/env-vars b/ci/tools/env-vars index bbc3ed66c5..5d7382f500 100755 --- a/ci/tools/env-vars +++ b/ci/tools/env-vars @@ -52,11 +52,27 @@ elif [[ "${1}" == "test" ]]; then BUILD_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${BUILD_CUDA_VER})" TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})" CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BUILD_CUDA_VER}-${HOST_PLATFORM}" + # USE_BACKPORT_BINDINGS flags the CTK-major-mismatch case where the + # current-run bindings wheel was built for a different CTK major than the + # one under test, so we must pull the bindings wheel from the backport + # branch instead. This is independent of whether bindings tests run. + # SKIP_CUDA_BINDINGS_TEST is the test-time gate: it is set when the CTK + # majors differ OR when the caller tells us to skip for path-filter + # reasons via SKIP_BINDINGS_TEST_OVERRIDE. if [[ ${BUILD_CUDA_MAJOR} != ${TEST_CUDA_MAJOR} ]]; then + USE_BACKPORT_BINDINGS=1 SKIP_CUDA_BINDINGS_TEST=1 SKIP_CYTHON_TEST=1 else - SKIP_CUDA_BINDINGS_TEST=0 + USE_BACKPORT_BINDINGS=0 + # Path-filter override only skips bindings tests, NOT cython tests + # for other modules (e.g. cuda.core). Cython skip is driven solely + # by the build/test CTK minor-version mismatch. + if [[ "${SKIP_BINDINGS_TEST_OVERRIDE:-0}" == "1" ]]; then + SKIP_CUDA_BINDINGS_TEST=1 + else + SKIP_CUDA_BINDINGS_TEST=0 + fi BUILD_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_CUDA_VER})" TEST_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${CUDA_VER})" if [[ ${BUILD_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then @@ -80,6 +96,7 @@ elif [[ "${1}" == "test" ]]; then echo "SKIP_CUDA_BINDINGS_TEST=${SKIP_CUDA_BINDINGS_TEST}" echo "SKIP_CYTHON_TEST=${SKIP_CYTHON_TEST}" echo "TEST_CUDA_MAJOR=${TEST_CUDA_MAJOR}" + echo "USE_BACKPORT_BINDINGS=${USE_BACKPORT_BINDINGS}" } >> $GITHUB_ENV fi