Skip to content

Commit 7ab4682

Browse files
cpcloudclaude
andauthored
CI: Detect changed paths and gate cuda.bindings tests (#1908)
* ci: detect changed paths and gate cuda.bindings tests Add a detect-changes job to ci.yml that classifies which top-level modules were touched by a PR (cuda_bindings, cuda_core, cuda_pathfinder, cuda_python, cuda_python_test_helpers, shared infra) using dorny/paths-filter. The job emits composed gating outputs that account for the dependency graph (pathfinder -> bindings -> core). Thread a new skip-bindings-test input through the reusable test-wheel workflows so cuda.bindings tests (and their Cython counterparts) are skipped when the detect-changes output for test_bindings is false. For PRs that only touch cuda_core this skips the expensive bindings suite while still running cuda.core and cuda.pathfinder tests against the wheel built in the current CI run. Split the existing SKIP_CUDA_BINDINGS_TEST env var in ci/tools/env-vars into two orthogonal flags: USE_BACKPORT_BINDINGS drives the backport branch download path (CTK major mismatch), while SKIP_CUDA_BINDINGS_TEST remains the test-time gate. This lets path-filter-based skips reuse the existing SKIP_CUDA_BINDINGS_TEST plumbing without triggering a cross-branch artifact fetch. Non-PR events (push to main, tag, schedule, workflow_dispatch) still exercise the full pipeline. Refs #299 * fix: set explicit dorny base and decouple cython-test skip Two fixes from code review: 1. Set `base: main` on dorny/paths-filter so backport PRs targeting non-default branches still diff against main for path detection. 2. Decouple SKIP_CYTHON_TEST from SKIP_BINDINGS_TEST_OVERRIDE. The path-filter override only skips bindings tests; cuda.core Cython tests should still run on core-only PRs. Cython skip is now driven solely by CTK minor-version mismatch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove explicit dorny base, rely on v3 default dorny/paths-filter v3 already diffs against the repo default branch for non-default-branch pushes. Explicit base: main was redundant and would produce wrong baselines for backport PRs targeting release branches (all files seen as changed vs main). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: replace dorny/paths-filter with native git diff Remove third-party dorny/paths-filter action dependency. Use git merge-base + git diff --name-only + grep instead. Same behavior, zero supply-chain risk, full control over base ref resolution. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style: use herestring instead of echo pipe in has_match Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * ci: resolve PR base branch at runtime for detect-changes diff copy-pr-bot mirrors every PR to a pull-request/<N> branch regardless of whether the upstream PR targets main or a backport branch such as 12.9.x. Diffing against origin/main unconditionally therefore misclassifies changed paths on backport PRs and silently suppresses the cuda.bindings test matrix. Look up the real base ref via nv-gha-runners/get-pr-info (already used elsewhere in this repo) and pass it to git merge-base so the changed- paths classification matches the PR's actual target. * ci: require detect-changes success in final checks aggregation The checks job used if:always() plus shell-level result inspection of doc and the three test jobs, treating skipped as non-fatal to preserve intentional [doc-only] skips. detect-changes is a needs prerequisite of every test job but was absent from checks.needs, so a failure in the gating step silently cascaded into test-job skips and a green final status. Add detect-changes to checks.needs and require its result to be success. The legitimate doc-only skip path is unaffected because that leaves detect-changes itself successful. --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 67be918 commit 7ab4682

4 files changed

Lines changed: 201 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,151 @@ jobs:
7171
echo "skip=${skip}" >> "$GITHUB_OUTPUT"
7272
echo "doc_only=${doc_only}" >> "$GITHUB_OUTPUT"
7373
74+
# Detect which top-level modules were touched by the PR so downstream build
75+
# and test jobs can avoid rebuilding/retesting modules unaffected by the
76+
# change. See issue #299.
77+
#
78+
# Dependency graph (verified in pyproject.toml files):
79+
# cuda_pathfinder -> (no internal deps)
80+
# cuda_bindings -> cuda_pathfinder
81+
# cuda_core -> cuda_pathfinder, cuda_bindings
82+
# cuda_python -> cuda_bindings (meta package)
83+
#
84+
# A change to cuda_pathfinder (or shared infra) forces a rebuild of every
85+
# downstream module. A change to cuda_bindings forces rebuild of cuda_core.
86+
# A change to cuda_core alone skips rebuilding/retesting cuda_bindings.
87+
# On push to main, tag refs, schedule, or workflow_dispatch events we
88+
# unconditionally run everything because there is no meaningful "changed
89+
# paths" baseline for those events.
90+
detect-changes:
91+
runs-on: ubuntu-latest
92+
outputs:
93+
bindings: ${{ steps.compose.outputs.bindings }}
94+
core: ${{ steps.compose.outputs.core }}
95+
pathfinder: ${{ steps.compose.outputs.pathfinder }}
96+
python_meta: ${{ steps.compose.outputs.python_meta }}
97+
test_helpers: ${{ steps.compose.outputs.test_helpers }}
98+
shared: ${{ steps.compose.outputs.shared }}
99+
build_bindings: ${{ steps.compose.outputs.build_bindings }}
100+
build_core: ${{ steps.compose.outputs.build_core }}
101+
build_pathfinder: ${{ steps.compose.outputs.build_pathfinder }}
102+
test_bindings: ${{ steps.compose.outputs.test_bindings }}
103+
test_core: ${{ steps.compose.outputs.test_core }}
104+
test_pathfinder: ${{ steps.compose.outputs.test_pathfinder }}
105+
steps:
106+
- name: Checkout repository
107+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
108+
with:
109+
fetch-depth: 0
110+
111+
# copy-pr-bot pushes every PR (whether it targets main or a backport
112+
# branch such as 12.9.x) to pull-request/<N>, so the base branch
113+
# cannot be inferred from github.ref_name. Look it up via the
114+
# upstream PR metadata so the diff below is rooted at the right place.
115+
- name: Resolve PR base branch
116+
id: pr-info
117+
if: ${{ startsWith(github.ref_name, 'pull-request/') }}
118+
uses: nv-gha-runners/get-pr-info@main
119+
120+
- name: Detect changed paths
121+
id: filter
122+
if: ${{ startsWith(github.ref_name, 'pull-request/') }}
123+
env:
124+
BASE_REF: ${{ fromJSON(steps.pr-info.outputs.pr-info).base.ref }}
125+
run: |
126+
# Diff against the merge base with the PR's actual target branch.
127+
# Uses merge-base so diverged branches only show files changed on
128+
# the PR side, not upstream commits.
129+
if [[ -z "${BASE_REF}" ]]; then
130+
echo "Could not resolve PR base branch from get-pr-info output" >&2
131+
exit 1
132+
fi
133+
base=$(git merge-base HEAD "origin/${BASE_REF}")
134+
changed=$(git diff --name-only "$base"...HEAD)
135+
136+
has_match() {
137+
grep -qE "$1" <<< "$changed" && echo true || echo false
138+
}
139+
140+
{
141+
echo "bindings=$(has_match '^cuda_bindings/')"
142+
echo "core=$(has_match '^cuda_core/')"
143+
echo "pathfinder=$(has_match '^cuda_pathfinder/')"
144+
echo "python_meta=$(has_match '^cuda_python/')"
145+
echo "test_helpers=$(has_match '^cuda_python_test_helpers/')"
146+
echo "shared=$(has_match '^(\.github/|ci/|scripts/|toolshed/|conftest\.py$|pyproject\.toml$|pixi\.(toml|lock)$|pytest\.ini$|ruff\.toml$)')"
147+
} >> "$GITHUB_OUTPUT"
148+
149+
- name: Compose gating outputs
150+
id: compose
151+
env:
152+
IS_PR: ${{ startsWith(github.ref_name, 'pull-request/') }}
153+
BINDINGS: ${{ steps.filter.outputs.bindings || 'false' }}
154+
CORE: ${{ steps.filter.outputs.core || 'false' }}
155+
PATHFINDER: ${{ steps.filter.outputs.pathfinder || 'false' }}
156+
PYTHON_META: ${{ steps.filter.outputs.python_meta || 'false' }}
157+
TEST_HELPERS: ${{ steps.filter.outputs.test_helpers || 'false' }}
158+
SHARED: ${{ steps.filter.outputs.shared || 'false' }}
159+
run: |
160+
set -euxo pipefail
161+
# Non-PR events (push to main, tag push, schedule, workflow_dispatch)
162+
# always exercise the full pipeline because there is no baseline for
163+
# a meaningful diff.
164+
if [[ "${IS_PR}" != "true" ]]; then
165+
bindings=true
166+
core=true
167+
pathfinder=true
168+
python_meta=true
169+
test_helpers=true
170+
shared=true
171+
else
172+
bindings="${BINDINGS}"
173+
core="${CORE}"
174+
pathfinder="${PATHFINDER}"
175+
python_meta="${PYTHON_META}"
176+
test_helpers="${TEST_HELPERS}"
177+
shared="${SHARED}"
178+
fi
179+
180+
or_flag() {
181+
for v in "$@"; do
182+
if [[ "${v}" == "true" ]]; then
183+
echo "true"
184+
return
185+
fi
186+
done
187+
echo "false"
188+
}
189+
190+
# Build gating: pathfinder change forces rebuild of bindings and
191+
# core; bindings change forces rebuild of core. shared changes force
192+
# a full rebuild.
193+
build_pathfinder="$(or_flag "${shared}" "${pathfinder}")"
194+
build_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}")"
195+
build_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}")"
196+
197+
# Test gating: tests for a module must run whenever that module, any
198+
# of its runtime dependencies, the shared test helper package, or
199+
# shared infra changes. pathfinder tests are cheap and always run.
200+
test_pathfinder=true
201+
test_bindings="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${test_helpers}")"
202+
test_core="$(or_flag "${shared}" "${pathfinder}" "${bindings}" "${core}" "${test_helpers}")"
203+
204+
{
205+
echo "bindings=${bindings}"
206+
echo "core=${core}"
207+
echo "pathfinder=${pathfinder}"
208+
echo "python_meta=${python_meta}"
209+
echo "test_helpers=${test_helpers}"
210+
echo "shared=${shared}"
211+
echo "build_bindings=${build_bindings}"
212+
echo "build_core=${build_core}"
213+
echo "build_pathfinder=${build_pathfinder}"
214+
echo "test_bindings=${test_bindings}"
215+
echo "test_core=${test_core}"
216+
echo "test_pathfinder=${test_pathfinder}"
217+
} >> "$GITHUB_OUTPUT"
218+
74219
# NOTE: Build jobs are intentionally split by platform rather than using a single
75220
# matrix. This allows each test job to depend only on its corresponding build,
76221
# so faster platforms can proceed through build & test without waiting for slower
@@ -151,6 +296,7 @@ jobs:
151296
needs:
152297
- ci-vars
153298
- should-skip
299+
- detect-changes
154300
- build-linux-64
155301
secrets: inherit
156302
uses: ./.github/workflows/test-wheel-linux.yml
@@ -159,6 +305,7 @@ jobs:
159305
host-platform: ${{ matrix.host-platform }}
160306
build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
161307
nruns: ${{ (github.event_name == 'schedule' && 100) || 1}}
308+
skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }}
162309

163310
# See test-linux-64 for why test jobs are split by platform.
164311
test-linux-aarch64:
@@ -174,6 +321,7 @@ jobs:
174321
needs:
175322
- ci-vars
176323
- should-skip
324+
- detect-changes
177325
- build-linux-aarch64
178326
secrets: inherit
179327
uses: ./.github/workflows/test-wheel-linux.yml
@@ -182,6 +330,7 @@ jobs:
182330
host-platform: ${{ matrix.host-platform }}
183331
build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
184332
nruns: ${{ (github.event_name == 'schedule' && 100) || 1}}
333+
skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }}
185334

186335
# See test-linux-64 for why test jobs are split by platform.
187336
test-windows:
@@ -197,6 +346,7 @@ jobs:
197346
needs:
198347
- ci-vars
199348
- should-skip
349+
- detect-changes
200350
- build-windows
201351
secrets: inherit
202352
uses: ./.github/workflows/test-wheel-windows.yml
@@ -205,6 +355,7 @@ jobs:
205355
host-platform: ${{ matrix.host-platform }}
206356
build-ctk-ver: ${{ needs.ci-vars.outputs.CUDA_BUILD_VER }}
207357
nruns: ${{ (github.event_name == 'schedule' && 100) || 1}}
358+
skip-bindings-test: ${{ !fromJSON(needs.detect-changes.outputs.test_bindings) }}
208359

209360
doc:
210361
name: Docs
@@ -228,6 +379,7 @@ jobs:
228379
runs-on: ubuntu-latest
229380
needs:
230381
- should-skip
382+
- detect-changes
231383
- test-linux-64
232384
- test-linux-aarch64
233385
- test-windows
@@ -254,7 +406,16 @@ jobs:
254406
#
255407
# Note: When [doc-only] is in PR title, test jobs are intentionally
256408
# skipped and should not cause failure.
409+
#
410+
# detect-changes gates whether heavy test matrices run at all; if it
411+
# does not succeed, downstream test jobs are skipped rather than
412+
# failed, which would otherwise go unnoticed here. Require its
413+
# success explicitly so a broken gating step cannot masquerade as a
414+
# green CI run.
257415
doc_only=${{ needs.should-skip.outputs.doc-only }}
416+
if ${{ needs.detect-changes.result != 'success' }}; then
417+
exit 1
418+
fi
258419
if ${{ needs.doc.result == 'cancelled' || needs.doc.result == 'failure' }}; then
259420
exit 1
260421
fi

.github/workflows/test-wheel-linux.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ on:
2222
nruns:
2323
type: number
2424
default: 1
25+
# When true, cuda.bindings tests (and the Cython tests that depend on
26+
# them) are skipped even when CTK majors match. Callers set this based
27+
# on the output of the detect-changes job in ci.yml so PRs that only
28+
# touch unrelated modules avoid the expensive bindings test suite.
29+
skip-bindings-test:
30+
type: boolean
31+
default: false
2532

2633
defaults:
2734
run:
@@ -113,6 +120,7 @@ jobs:
113120
LOCAL_CTK: ${{ matrix.LOCAL_CTK }}
114121
PY_VER: ${{ matrix.PY_VER }}
115122
SHA: ${{ github.sha }}
123+
SKIP_BINDINGS_TEST_OVERRIDE: ${{ inputs.skip-bindings-test && '1' || '0' }}
116124
run: ./ci/tools/env-vars test
117125

118126
- name: Download cuda-pathfinder build artifacts
@@ -122,21 +130,21 @@ jobs:
122130
path: ./cuda_pathfinder
123131

124132
- name: Download cuda-python build artifacts
125-
if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}}
133+
if: ${{ env.USE_BACKPORT_BINDINGS == '0' }}
126134
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
127135
with:
128136
name: cuda-python-wheel
129137
path: .
130138

131139
- name: Download cuda.bindings build artifacts
132-
if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}}
140+
if: ${{ env.USE_BACKPORT_BINDINGS == '0' }}
133141
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
134142
with:
135143
name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}
136144
path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}
137145

138146
- name: Download cuda-python & cuda.bindings build artifacts from the prior branch
139-
if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '1'}}
147+
if: ${{ env.USE_BACKPORT_BINDINGS == '1' }}
140148
env:
141149
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
142150
run: |

.github/workflows/test-wheel-windows.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ on:
2222
nruns:
2323
type: number
2424
default: 1
25+
# When true, cuda.bindings tests (and the Cython tests that depend on
26+
# them) are skipped even when CTK majors match. Callers set this based
27+
# on the output of the detect-changes job in ci.yml so PRs that only
28+
# touch unrelated modules avoid the expensive bindings test suite.
29+
skip-bindings-test:
30+
type: boolean
31+
default: false
2532

2633
jobs:
2734
compute-matrix:
@@ -107,6 +114,7 @@ jobs:
107114
LOCAL_CTK: ${{ matrix.LOCAL_CTK }}
108115
PY_VER: ${{ matrix.PY_VER }}
109116
SHA: ${{ github.sha }}
117+
SKIP_BINDINGS_TEST_OVERRIDE: ${{ inputs.skip-bindings-test && '1' || '0' }}
110118
shell: bash --noprofile --norc -xeuo pipefail {0}
111119
run: ./ci/tools/env-vars test
112120

@@ -117,21 +125,21 @@ jobs:
117125
path: ./cuda_pathfinder
118126

119127
- name: Download cuda-python build artifacts
120-
if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}}
128+
if: ${{ env.USE_BACKPORT_BINDINGS == '0' }}
121129
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
122130
with:
123131
name: cuda-python-wheel
124132
path: .
125133

126134
- name: Download cuda.bindings build artifacts
127-
if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '0'}}
135+
if: ${{ env.USE_BACKPORT_BINDINGS == '0' }}
128136
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
129137
with:
130138
name: ${{ env.CUDA_BINDINGS_ARTIFACT_NAME }}
131139
path: ${{ env.CUDA_BINDINGS_ARTIFACTS_DIR }}
132140

133141
- name: Download cuda-python & cuda.bindings build artifacts from the prior branch
134-
if: ${{ env.SKIP_CUDA_BINDINGS_TEST == '1'}}
142+
if: ${{ env.USE_BACKPORT_BINDINGS == '1' }}
135143
env:
136144
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
137145
run: |

ci/tools/env-vars

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,27 @@ elif [[ "${1}" == "test" ]]; then
5252
BUILD_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${BUILD_CUDA_VER})"
5353
TEST_CUDA_MAJOR="$(cut -d '.' -f 1 <<< ${CUDA_VER})"
5454
CUDA_BINDINGS_ARTIFACT_BASENAME="cuda-bindings-python${PYTHON_VERSION_FORMATTED}-cuda${BUILD_CUDA_VER}-${HOST_PLATFORM}"
55+
# USE_BACKPORT_BINDINGS flags the CTK-major-mismatch case where the
56+
# current-run bindings wheel was built for a different CTK major than the
57+
# one under test, so we must pull the bindings wheel from the backport
58+
# branch instead. This is independent of whether bindings tests run.
59+
# SKIP_CUDA_BINDINGS_TEST is the test-time gate: it is set when the CTK
60+
# majors differ OR when the caller tells us to skip for path-filter
61+
# reasons via SKIP_BINDINGS_TEST_OVERRIDE.
5562
if [[ ${BUILD_CUDA_MAJOR} != ${TEST_CUDA_MAJOR} ]]; then
63+
USE_BACKPORT_BINDINGS=1
5664
SKIP_CUDA_BINDINGS_TEST=1
5765
SKIP_CYTHON_TEST=1
5866
else
59-
SKIP_CUDA_BINDINGS_TEST=0
67+
USE_BACKPORT_BINDINGS=0
68+
# Path-filter override only skips bindings tests, NOT cython tests
69+
# for other modules (e.g. cuda.core). Cython skip is driven solely
70+
# by the build/test CTK minor-version mismatch.
71+
if [[ "${SKIP_BINDINGS_TEST_OVERRIDE:-0}" == "1" ]]; then
72+
SKIP_CUDA_BINDINGS_TEST=1
73+
else
74+
SKIP_CUDA_BINDINGS_TEST=0
75+
fi
6076
BUILD_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${BUILD_CUDA_VER})"
6177
TEST_CUDA_MINOR="$(cut -d '.' -f 2 <<< ${CUDA_VER})"
6278
if [[ ${BUILD_CUDA_MINOR} != ${TEST_CUDA_MINOR} ]]; then
@@ -80,6 +96,7 @@ elif [[ "${1}" == "test" ]]; then
8096
echo "SKIP_CUDA_BINDINGS_TEST=${SKIP_CUDA_BINDINGS_TEST}"
8197
echo "SKIP_CYTHON_TEST=${SKIP_CYTHON_TEST}"
8298
echo "TEST_CUDA_MAJOR=${TEST_CUDA_MAJOR}"
99+
echo "USE_BACKPORT_BINDINGS=${USE_BACKPORT_BINDINGS}"
83100
} >> $GITHUB_ENV
84101
fi
85102

0 commit comments

Comments
 (0)