Skip to content

Commit da7ebee

Browse files
committed
ci: use paths-filter for selective CI planning
1 parent 9cf09a9 commit da7ebee

2 files changed

Lines changed: 156 additions & 298 deletions

File tree

.github/workflows/ci.yml

Lines changed: 156 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ jobs:
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

Comments
 (0)