8686 # downstream module. A change to cuda_bindings forces rebuild of cuda_core.
8787 # A change to cuda_core alone skips rebuilding/retesting cuda_bindings and
8888 # cuda_pathfinder, but still retests the downstream cuda-python metapackage.
89- # CI/planner changes are shared by design, so this implementation runs the
89+ # CI/filter changes are shared by design, so this implementation runs the
9090 # full pipeline; exercise selective cases in follow-up package-only PRs.
9191 # On push to main, tag refs, schedule, or workflow_dispatch events we
9292 # unconditionally run everything because there is no meaningful "changed
@@ -98,43 +98,98 @@ jobs:
9898 contents : read
9999 pull-requests : read
100100 outputs :
101- bindings : ${{ steps.compose.outputs.bindings }}
102- core : ${{ steps.compose.outputs.core }}
103- pathfinder : ${{ steps.compose.outputs.pathfinder }}
104- python_meta : ${{ steps.compose.outputs.python_meta }}
105- test_helpers : ${{ steps.compose.outputs.test_helpers }}
106- shared : ${{ steps.compose.outputs.shared }}
107- build_bindings : ${{ steps.compose.outputs.build_bindings }}
108- build_core : ${{ steps.compose.outputs.build_core }}
109- build_pathfinder : ${{ steps.compose.outputs.build_pathfinder }}
110- build_python : ${{ steps.compose.outputs.build_python }}
111- test_bindings : ${{ steps.compose.outputs.test_bindings }}
112- test_core : ${{ steps.compose.outputs.test_core }}
113- test_pathfinder : ${{ steps.compose.outputs.test_pathfinder }}
114- pr_merge_base : ${{ steps.filter.outputs.merge_base }}
115- test_python : ${{ steps.compose.outputs.test_python }}
116- baseline_run_id : ${{ steps.compose.outputs.baseline_run_id }}
117- baseline_sha : ${{ steps.compose.outputs.baseline_sha }}
101+ # Missing base artifacts or a skipped path filter fail open to the full pipeline.
102+ core : >-
103+ ${{ steps.baseline.outputs.available != 'true' ||
104+ steps.filter.outputs.changes == '' ||
105+ steps.filter.outputs.core_source == 'true' }}
106+ build_pathfinder : >-
107+ ${{ steps.baseline.outputs.available != 'true' ||
108+ steps.filter.outputs.changes == '' ||
109+ steps.filter.outputs.force_all == 'true' ||
110+ steps.filter.outputs.pathfinder_source == 'true' }}
111+ build_bindings : >-
112+ ${{ steps.baseline.outputs.available != 'true' ||
113+ steps.filter.outputs.changes == '' ||
114+ steps.filter.outputs.force_all == 'true' ||
115+ steps.filter.outputs.pathfinder_source == 'true' ||
116+ steps.filter.outputs.bindings_source == 'true' ||
117+ steps.filter.outputs.python_source == 'true' }}
118+ build_core : >-
119+ ${{ steps.baseline.outputs.available != 'true' ||
120+ steps.filter.outputs.changes == '' ||
121+ steps.filter.outputs.force_all == 'true' ||
122+ steps.filter.outputs.pathfinder_source == 'true' ||
123+ steps.filter.outputs.bindings_source == 'true' ||
124+ steps.filter.outputs.core_source == 'true' }}
125+ build_python : >-
126+ ${{ steps.baseline.outputs.available != 'true' ||
127+ steps.filter.outputs.changes == '' ||
128+ steps.filter.outputs.force_all == 'true' ||
129+ steps.filter.outputs.pathfinder_source == 'true' ||
130+ steps.filter.outputs.bindings_source == 'true' ||
131+ steps.filter.outputs.python_source == 'true' }}
132+ test_pathfinder : >-
133+ ${{ steps.baseline.outputs.available != 'true' ||
134+ steps.filter.outputs.changes == '' ||
135+ steps.filter.outputs.force_all == 'true' ||
136+ steps.filter.outputs.pathfinder_source == 'true' ||
137+ steps.filter.outputs.pathfinder_tests == 'true' }}
138+ test_bindings : >-
139+ ${{ steps.baseline.outputs.available != 'true' ||
140+ steps.filter.outputs.changes == '' ||
141+ steps.filter.outputs.force_all == 'true' ||
142+ steps.filter.outputs.pathfinder_source == 'true' ||
143+ steps.filter.outputs.bindings_source == 'true' ||
144+ steps.filter.outputs.bindings_tests == 'true' ||
145+ steps.filter.outputs.test_helpers == 'true' }}
146+ test_core : >-
147+ ${{ steps.baseline.outputs.available != 'true' ||
148+ steps.filter.outputs.changes == '' ||
149+ steps.filter.outputs.force_all == 'true' ||
150+ steps.filter.outputs.pathfinder_source == 'true' ||
151+ steps.filter.outputs.bindings_source == 'true' ||
152+ steps.filter.outputs.core_source == 'true' ||
153+ steps.filter.outputs.core_tests == 'true' ||
154+ steps.filter.outputs.test_helpers == 'true' }}
155+ pr_merge_base : ${{ steps.merge-base.outputs.sha }}
156+ test_python : >-
157+ ${{ steps.baseline.outputs.available != 'true' ||
158+ steps.filter.outputs.changes == '' ||
159+ steps.filter.outputs.force_all == 'true' ||
160+ steps.filter.outputs.pathfinder_source == 'true' ||
161+ steps.filter.outputs.bindings_source == 'true' ||
162+ steps.filter.outputs.core_source == 'true' ||
163+ steps.filter.outputs.python_source == 'true' ||
164+ steps.filter.outputs.python_tests == 'true' }}
165+ baseline_run_id : >-
166+ ${{ steps.filter.outputs.changes != '' &&
167+ steps.baseline.outputs.available == 'true' &&
168+ steps.baseline.outputs.run_id || '' }}
169+ baseline_sha : >-
170+ ${{ steps.filter.outputs.changes != '' &&
171+ steps.baseline.outputs.available == 'true' &&
172+ steps.baseline.outputs.sha || '' }}
118173 steps :
119174 - name : Checkout repository
120175 uses : actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
121176 with :
122- # Treeless clone: commit graph is needed for `git merge-base` and
123- # `git diff --name-only` below , but historical blobs aren't.
177+ # Treeless clone: the commit graph is needed to resolve the PR merge
178+ # base and classify its changed paths , but historical blobs aren't.
124179 fetch-depth : 0
125180 filter : blob:none
126181
127182 # copy-pr-bot pushes every PR (whether it targets main or a backport
128183 # branch such as 12.9.x) to pull-request/<N>, so the base branch
129184 # cannot be inferred from github.ref_name. Look it up via the
130- # upstream PR metadata so the diff below is rooted at the right place.
185+ # upstream PR metadata so change detection is rooted at the right place.
131186 - name : Resolve PR base branch
132187 id : pr-info
133188 if : ${{ startsWith(github.ref_name, 'pull-request/') }}
134189 uses : nv-gha-runners/get-pr-info@main
135190
136- - name : Detect changed paths
137- id : filter
191+ - name : Resolve PR merge base
192+ id : merge-base
138193 if : ${{ startsWith(github.ref_name, 'pull-request/') }}
139194 env :
140195 # GitHub Actions evaluates step-level `env:` expressions eagerly —
@@ -153,25 +208,83 @@ jobs:
153208 exit 1
154209 fi
155210
156- # Diff against the merge base with the PR's actual target branch.
157- # Disabling rename detection reports both sides of a cross-package
158- # move, which prevents the source package from being skipped.
159211 base=$(git merge-base HEAD "origin/${BASE_REF}")
160- git diff --no-renames --name-only -z "$base"...HEAD > changed-paths
161- python ci/tools/compute_ci_plan.py changed-paths >> "$GITHUB_OUTPUT"
162- echo "merge_base=${base}" >> "$GITHUB_OUTPUT"
212+ echo "sha=${base}" >> "$GITHUB_OUTPUT"
163213
164- {
165- echo "### Selective CI changed paths"
166- echo
167- tr '\0' '\n' < changed-paths | sed 's/^/- `/' | sed 's/$/`/'
168- } >> "$GITHUB_STEP_SUMMARY"
214+ - name : Classify changed paths
215+ id : filter
216+ if : ${{ startsWith(github.ref_name, 'pull-request/') }}
217+ uses : dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
218+ with :
219+ # The workflow runs on copy-pr-bot push branches, so compare the
220+ # checked-out PR head against the base resolved from PR metadata.
221+ base : ${{ steps.merge-base.outputs.sha }}
222+ ref : ${{ github.sha }}
223+ token : ' '
224+ predicate-quantifier : some-with-excludes
225+ filters : |
226+ pathfinder_source:
227+ - 'cuda_pathfinder/**'
228+ - '!cuda_pathfinder/docs/**'
229+ - '!cuda_pathfinder/tests/**'
230+ - '!cuda_pathfinder/examples/**'
231+ bindings_source:
232+ - 'cuda_bindings/**'
233+ - '!cuda_bindings/docs/**'
234+ - '!cuda_bindings/tests/**'
235+ - '!cuda_bindings/examples/**'
236+ core_source:
237+ - 'cuda_core/**'
238+ - '!cuda_core/docs/**'
239+ - '!cuda_core/tests/**'
240+ - '!cuda_core/examples/**'
241+ python_source:
242+ - 'cuda_python/**'
243+ - '!cuda_python/docs/**'
244+ - '!cuda_python/tests/**'
245+ - '!cuda_python/examples/**'
246+ pathfinder_tests:
247+ - 'cuda_pathfinder/tests/**'
248+ - 'cuda_pathfinder/examples/**'
249+ bindings_tests:
250+ - 'cuda_bindings/tests/**'
251+ - 'cuda_bindings/examples/**'
252+ - 'benchmarks/cuda_bindings/**'
253+ core_tests:
254+ - 'cuda_core/tests/**'
255+ - 'cuda_core/examples/**'
256+ python_tests:
257+ - 'cuda_python/tests/**'
258+ - 'cuda_python/examples/**'
259+ test_helpers:
260+ - 'cuda_python_test_helpers/**'
261+ # Shared infrastructure and unknown paths run the full pipeline.
262+ # Exclude only paths whose narrower behavior is defined above or
263+ # repository policy/prose files known not to affect artifacts.
264+ force_all:
265+ - '**'
266+ - '!cuda_pathfinder/**'
267+ - '!cuda_bindings/**'
268+ - '!cuda_core/**'
269+ - '!cuda_python/**'
270+ - '!cuda_python_test_helpers/**'
271+ - '!benchmarks/cuda_bindings/**'
272+ - '!.git-blame-ignore-revs'
273+ - '!.gitignore'
274+ - '!AGENTS.md'
275+ - '!CHANGELOG.md'
276+ - '!CODE_OF_CONDUCT.md'
277+ - '!CONTRIBUTING.md'
278+ - '!LICENSE'
279+ - '!README.md'
280+ - '!SECURITY.md'
169281
170282 - name : Resolve reusable base artifacts
171283 id : baseline
172284 if : ${{ startsWith(github.ref_name, 'pull-request/') }}
173285 env :
174286 BASE_REF : ${{ steps.pr-info.outputs.pr-info && fromJSON(steps.pr-info.outputs.pr-info).base.ref || '' }}
287+ MERGE_BASE : ${{ steps.merge-base.outputs.sha }}
175288 GH_TOKEN : ${{ github.token }}
176289 run : |
177290 set -uo pipefail
@@ -186,7 +299,10 @@ jobs:
186299 unavailable
187300 fi
188301
189- merge_base=$(git merge-base HEAD "origin/${BASE_REF}")
302+ merge_base="${MERGE_BASE}"
303+ if [[ -z "${merge_base}" ]]; then
304+ unavailable
305+ fi
190306 if ! runs=$(gh run list \
191307 --repo "${{ github.repository }}" \
192308 --branch "${BASE_REF}" \
@@ -256,118 +372,6 @@ jobs:
256372 echo "Reusable artifacts: run \`${run_id}\` at \`${baseline_sha}\` on \`${BASE_REF}\`."
257373 } >> "$GITHUB_STEP_SUMMARY"
258374
259- - name : Compose gating outputs
260- id : compose
261- env :
262- IS_PR : ${{ startsWith(github.ref_name, 'pull-request/') }}
263- BASELINE_AVAILABLE : ${{ steps.baseline.outputs.available || 'false' }}
264- BASELINE_RUN_ID : ${{ steps.baseline.outputs.run_id }}
265- BASELINE_SHA : ${{ steps.baseline.outputs.sha }}
266- BINDINGS : ${{ steps.filter.outputs.bindings_source }}
267- CORE : ${{ steps.filter.outputs.core_source }}
268- PATHFINDER : ${{ steps.filter.outputs.pathfinder_source }}
269- PYTHON_META : ${{ steps.filter.outputs.python_source }}
270- TEST_HELPERS : ${{ steps.filter.outputs.test_helpers }}
271- SHARED : ${{ steps.filter.outputs.shared }}
272- BUILD_BINDINGS : ${{ steps.filter.outputs.build_bindings }}
273- BUILD_CORE : ${{ steps.filter.outputs.build_core }}
274- BUILD_PATHFINDER : ${{ steps.filter.outputs.build_pathfinder }}
275- BUILD_PYTHON : ${{ steps.filter.outputs.build_python }}
276- TEST_BINDINGS : ${{ steps.filter.outputs.test_bindings }}
277- TEST_CORE : ${{ steps.filter.outputs.test_core }}
278- TEST_PATHFINDER : ${{ steps.filter.outputs.test_pathfinder }}
279- TEST_PYTHON : ${{ steps.filter.outputs.test_python }}
280- run : |
281- set -euxo pipefail
282- planner_valid=true
283- if [[ "${IS_PR}" == "true" ]]; then
284- for value in \
285- "${BINDINGS}" "${CORE}" "${PATHFINDER}" "${PYTHON_META}" \
286- "${TEST_HELPERS}" "${SHARED}" \
287- "${BUILD_BINDINGS}" "${BUILD_CORE}" "${BUILD_PATHFINDER}" "${BUILD_PYTHON}" \
288- "${TEST_BINDINGS}" "${TEST_CORE}" "${TEST_PATHFINDER}" "${TEST_PYTHON}"; do
289- if [[ "${value}" != "true" && "${value}" != "false" ]]; then
290- planner_valid=false
291- fi
292- done
293- if [[ "${BASELINE_AVAILABLE}" == "true" &&
294- ( -z "${BASELINE_RUN_ID}" || -z "${BASELINE_SHA}" ) ]]; then
295- planner_valid=false
296- fi
297- fi
298-
299- # Non-PR events produce the complete trusted artifact set. PRs also
300- # run everything when the trusted base artifact inventory is absent
301- # or the planner did not emit a complete boolean result.
302- if [[ "${IS_PR}" != "true" ||
303- "${BASELINE_AVAILABLE}" != "true" ||
304- "${planner_valid}" != "true" ]]; then
305- bindings=true
306- core=true
307- pathfinder=true
308- python_meta=true
309- test_helpers=true
310- shared=true
311- build_bindings=true
312- build_core=true
313- build_pathfinder=true
314- build_python=true
315- test_bindings=true
316- test_core=true
317- test_pathfinder=true
318- test_python=true
319- baseline_run_id=""
320- baseline_sha=""
321- else
322- bindings="${BINDINGS}"
323- core="${CORE}"
324- pathfinder="${PATHFINDER}"
325- python_meta="${PYTHON_META}"
326- test_helpers="${TEST_HELPERS}"
327- shared="${SHARED}"
328- build_bindings="${BUILD_BINDINGS}"
329- build_core="${BUILD_CORE}"
330- build_pathfinder="${BUILD_PATHFINDER}"
331- build_python="${BUILD_PYTHON}"
332- test_bindings="${TEST_BINDINGS}"
333- test_core="${TEST_CORE}"
334- test_pathfinder="${TEST_PATHFINDER}"
335- test_python="${TEST_PYTHON}"
336- baseline_run_id="${BASELINE_RUN_ID}"
337- baseline_sha="${BASELINE_SHA}"
338- fi
339-
340- {
341- echo "bindings=${bindings}"
342- echo "core=${core}"
343- echo "pathfinder=${pathfinder}"
344- echo "python_meta=${python_meta}"
345- echo "test_helpers=${test_helpers}"
346- echo "shared=${shared}"
347- echo "build_bindings=${build_bindings}"
348- echo "build_core=${build_core}"
349- echo "build_pathfinder=${build_pathfinder}"
350- echo "build_python=${build_python}"
351- echo "test_bindings=${test_bindings}"
352- echo "test_core=${test_core}"
353- echo "test_pathfinder=${test_pathfinder}"
354- echo "test_python=${test_python}"
355- echo "baseline_run_id=${baseline_run_id}"
356- echo "baseline_sha=${baseline_sha}"
357- } >> "$GITHUB_OUTPUT"
358-
359- {
360- echo
361- echo "### Effective package plan"
362- echo
363- echo "| Package | Build | Test |"
364- echo "| --- | --- | --- |"
365- echo "| cuda-pathfinder | ${build_pathfinder} | ${test_pathfinder} |"
366- echo "| cuda-bindings | ${build_bindings} | ${test_bindings} |"
367- echo "| cuda-core | ${build_core} | ${test_core} |"
368- echo "| cuda-python | ${build_python} | ${test_python} |"
369- } >> "$GITHUB_STEP_SUMMARY"
370-
371375 api-check-core-vs-release :
372376 name : API check (cuda_core vs. latest release)
373377 if : >-
@@ -834,7 +838,7 @@ jobs:
834838 needs.detect-changes.outputs.test_bindings == 'true' ||
835839 needs.detect-changes.outputs.test_core == 'true' ||
836840 needs.detect-changes.outputs.test_python == 'true' }}"
837- core_changed ="${{ needs.detect-changes.outputs.core == 'true' }}"
841+ run_core_api_check ="${{ needs.detect-changes.outputs.core == 'true' }}"
838842 is_pr="${{ startsWith(github.ref_name, 'pull-request/') }}"
839843 status="success"
840844 check_result() {
@@ -880,13 +884,14 @@ jobs:
880884 check_result "test-linux-aarch64" "$expected" "${{ needs.test-linux-aarch64.result }}"
881885 check_result "test-windows" "$expected" "${{ needs.test-windows.result }}"
882886
883- # API compatibility checks only run for cuda_core source changes.
887+ # API compatibility checks run for cuda_core source changes and for
888+ # conservative full runs when reusable base artifacts are unavailable.
884889 expected="skipped"
885- if [[ "$core_changed " == "true" ]]; then expected="success"; fi
890+ if [[ "$run_core_api_check " == "true" ]]; then expected="success"; fi
886891 check_result "api-check-core-vs-release" "$expected" "${{ needs.api-check-core-vs-release.result }}"
887892
888893 expected="skipped"
889- if [[ "$is_pr" == "true" && "$core_changed " == "true" ]]; then expected="success"; fi
894+ if [[ "$is_pr" == "true" && "$run_core_api_check " == "true" ]]; then expected="success"; fi
890895 check_result "api-check-core-vs-base" "$expected" "${{ needs.api-check-core-vs-base.result }}"
891896
892897 [[ "$status" == "success" ]]
0 commit comments