ci: activate dependency-aware package builds and tests - #2467
Conversation
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
edfbf97 to
66445a4
Compare
a06ad8b to
adf15bd
Compare
adf15bd to
ba702ca
Compare
ba702ca to
85f173e
Compare
85f173e to
9cf09a9
Compare
|
mdboom
left a comment
There was a problem hiding this comment.
Sorry this review will be long, and will negate some of the earlier decisions in the stack. I should have evaluated the whole stack at once to see where this is going.
I think this is fundamentally a maintenance nightmare and way more verbose than it needs to be. Rather than passing 9 new parameters between jobs, there should be a single, structured JSON object that is passed around. I am thinking something like (using YAML because it's easier to type, but it would be JSON).
modules:
test_helpers:
needs_build: false
needs_test: false
pathfinder:
needs_build: false
needs_test: false
build_depends: []
test_depends: ["test_helpers"]
bindings:
needs_build: false
needs_test: false
build_depends: ["pathfinder"]
test_depends: ["test_helpers"]
core:
needs_build: false
needs_test: false
build_depends: ["bindings"]
test_depends: ["test_helpers"]
baseline_run_id: 123456789
baseline_sha: c0ffee
This makes verbose things that are sensitive to modules coming and going like:
${{ needs.detect-changes.outputs.build_pathfinder == 'true' ||
needs.detect-changes.outputs.build_bindings == 'true' ||
needs.detect-changes.outputs.build_core == 'true' ||
needs.detect-changes.outputs.build_python == 'true' }}
into:
Object.values(data.modules).some(m => m.needs_test)
Rather than this:
test_bindings: >-
${{ steps.baseline.outputs.available != 'true' ||
steps.filter.outputs.changes == '' ||
steps.filter.outputs.force_all == 'true' ||
steps.filter.outputs.all_tests == 'true' ||
steps.filter.outputs.pathfinder_source == 'true' ||
steps.filter.outputs.bindings_source == 'true' ||
steps.filter.outputs.bindings_tests == 'true' ||
steps.filter.outputs.test_helpers == 'true' }}
You can do:
(name, modules, seen = new Set()) => {
if (seen.has(name)) return false;
seen.add(name);
const m = modules[name];
return m.needs_test || (m.depends || []).some(dep => moduleNeedsTest(dep, modules, seen));
};
rather than hardcoding the dependencies into a bunch of large boolean expressions.
We can pre-compute values for downstream consumers so this kind of logic is only written in one place.
data["needs_build"] = Object.values(data.modules).some(m => m.needs_build)
# or Python
data["needs_build"] = any(mod["needs_build"] for mod in data["modules"])
This object would get passed between jobs as "workplan" (or some better name).
Given my other comment that dorny/paths-filter seems like the wrong tool for the job, I would propose:
A standalone Python script that does the analysis of changes and builds this workplan object, including as many pre-computed dependendent information as we can to make downstream consumers of this information concise and prevent long complicated expressions being repeated everywhere.
This script has the side benefit of forming the basis of a future local helper to test only what is needed (which all of this GHA work doesn't move us toward).
(I confirmed that outputs can have a size maximum of 1MB which should be more than enough for this.)
|
|
||
| has_match() { | ||
| grep -qE "$1" <<< "$changed" && echo true || echo false | ||
| - name: Classify changed paths |
There was a problem hiding this comment.
The hardcoded path lists here is going to be a maintenance nightmare.
Each subsection follows a pretty similar pattern, though. If there were a better way of specifying things to reduce duplication, I think that would be fine. (For example, if we could globally exclude all AGENTS.md).
As it stands, if this is the best we can do with dorny/paths-filter, I think we should reach for a different tool or write our own in Python. Upside of that, too, is we could build a "test everything we need to" local script on top of it, whereas this is stuck in the GHA silo.
There was a problem hiding this comment.
Originally my agent had implemented a Python script to encapsulate the logic of what paths / files impact which builds / tests and I found that script even more difficult to understand and maintain than maintaining a list of paths like this that an agent could easily update (and we should include in the AGENTS.md in hindsight). Let me see what I can do to simplify things here, because I agree this is going to be an ongoing annoyance.
For what it's worth, I think this overall work is going to be a stopgap as opposed to the end solution, where moving to something like moon (https://moonrepo.dev/moon) or bazel is probably a better longer term solution that makes things more "target" oriented as opposed to path / file oriented.
| # cuda_python/README.md is a symlink to this packaging input. | ||
| - 'README.md' |
There was a problem hiding this comment.
Not sure I understand the comment.
There was a problem hiding this comment.
cuda_python/README.md symlinks the root README.md and it gets included in the Python package that we produce, so changes to the top level README need to trigger a rebuild of the metapackage.
mdboom
left a comment
There was a problem hiding this comment.
Some suggestions to simplify the logic.
I think the agent here is trying to use a scalpel, and thereby creating real pitfalls if assumptions that hold true today don't hold true in the future.
I think instead, we take an approach to be as simple as possible, err on the side of doing too much work, rather than missing things that legitimately should be tested.
Maybe some description similar to the above would cause the agent to come up with something similar/safer? If not, I think my concrete suggestions should also help.
| "pixi.lock", | ||
| "pixi.toml", |
There was a problem hiding this comment.
We can probably safely put these in IGNORED_BASENAMES
| ".gitignore", | ||
| ".pre-commit-config.yaml", | ||
| ".spdx-ignore", | ||
| "CONTRIBUTING.md", |
There was a problem hiding this comment.
We can probably safely ignore all .md files, especially if we handle files downstream of a directory called test specially, which it looks like we already do below.
| "benchmarks/cuda_bindings/pixi.lock", | ||
| "benchmarks/cuda_bindings/pixi.toml", | ||
| "ci/.ci-pipeline-regen.md", | ||
| "ci/ci-pipeline.svg", |
There was a problem hiding this comment.
We can probably safely ignore all .svg files.
| "ci/.ci-pipeline-regen.md", | ||
| "ci/ci-pipeline.svg", | ||
| "ci/cleanup-pr-previews", | ||
| "ci/tools/check_mempool_hygiene.py", | ||
| "ci/tools/check_pixi_cuda_version.py", | ||
| "ci/tools/check_release_notes.py", | ||
| "ci/tools/download-wheels", | ||
| "ci/tools/run_pytest_with_stack.py", | ||
| "ci/tools/validate-release-wheels", |
There was a problem hiding this comment.
I think never ignoring anything in ci would be a safer (and simpler) choice.
| "greptile.json", | ||
| "pixi.lock", | ||
| "pixi.toml", | ||
| "pytest.ini", |
There was a problem hiding this comment.
This should not be on the ignore list -- this could have an appreciable impact on tests in the whole repo.
| if not path_parts or path in IGNORED_PATHS or path_parts[-1] in IGNORED_BASENAMES: | ||
| continue | ||
|
|
||
| if path == "README.md": |
There was a problem hiding this comment.
Can we just track symlinks generically (using Path.resolve or similar)?
| continue | ||
|
|
||
| if path.startswith("cuda_python_test_helpers/cuda_python_test_helpers/"): | ||
| test_changes.update(("bindings", "core")) |
There was a problem hiding this comment.
It doesn't today, but it wouldn't surprise me if cuda_pathfinder depended on test helpers someday. Probably safer to do this:
| test_changes.update(("bindings", "core")) | |
| test_changes.update(MODULES) |
| ALL_TEST_PATHS = { | ||
| ".github/workflows/test-wheel-linux.yml", | ||
| ".github/workflows/test-wheel-windows.yml", | ||
| "ci/test-matrix.yml", | ||
| "ci/tools/configure_driver_mode.ps1", | ||
| "ci/tools/guess_latest.sh", | ||
| "ci/tools/install_gpu_driver.ps1", | ||
| "ci/tools/install_gpu_driver.sh", | ||
| "ci/tools/run-tests", | ||
| "ci/tools/setup-sanitizer", | ||
| } |
There was a problem hiding this comment.
I think it's safer to trigger full tests if anything in ci or .github/{workflow|actions} changes.
| "ci/tools/setup-sanitizer", | ||
| } | ||
|
|
||
| INDEPENDENT_GITHUB_PATHS = { |
There was a problem hiding this comment.
As I said elsewhere, I think we can invert the logic and consider anything in workflows or actions to trigger a full test and everything else ignored.
| if path.startswith("cuda_python_test_helpers/cuda_python_test_helpers/"): | ||
| test_changes.update(("bindings", "core")) | ||
| elif path.startswith("benchmarks/cuda_bindings/"): | ||
| test_changes.add("bindings") |
There was a problem hiding this comment.
Ditto to above.
| test_changes.add("bindings") | |
| test_changes.add(MODULES) |
Important
Stack completion: PR 4 of 4. #2464, #2465, and #2466 have merged. This is the final PR in the stack.
Merge order: #2464 (merged) -> #2465 (merged) -> #2466 (merged) -> #2467 (this PR). The branch lives in the
kkraus14fork and targets upstreammain.What
merge-base...HEAD, applies the four-package build/test impact policy, and emits final module and job decisions as JSON.git diff --no-renames.Expected package behavior
cuda_pathfindersource: build and test all four packages.cuda_bindingssource: build and test bindings, core, and cuda-python.cuda_coresource: build core; test core and cuda-python; run cuda-core API checks.cuda_pythonsource: build bindings and cuda-python because development cuda-python wheels exactly pin bindings; test cuda-python.Validation
Implements the remaining dependency-aware build/test selection requested in #299.