Repository navigation
Add file-level import graph for per-PR CI job selection - #6713
mataylor-nvidia wants to merge 4 commits into
Conversation
Adds tools/package_impact_graph.py which walks every .py file under source/, parses runtime imports (excluding TYPE_CHECKING guards), and builds a reverse-dependency graph from that. Given a set of changed files it computes which CI test jobs are transitively affected and emits a manifest showing the import chain for each triggered job. Integrates this into build.yaml via a new compute-affected-packages job that runs on ubuntu-latest in parallel with the Docker build. Each test job gains a contains(fromJSON(...)) guard so it is skipped when its package is not in the affected set. Non-Python file changes and any failure in the compute job both fail open (all jobs run).
Greptile SummaryThis PR adds
Confidence Score: 3/5Merging as-is means a crash in the compute step silently cancels all GPU test jobs instead of running them all — the opposite of the stated design. The core logic of the import graph and the CI guard expressions is sound, but the fail-open contract is inverted: when compute-affected-packages fails (script crash, git error, JSON parse failure), GitHub Actions cancels every dependent test job without ever evaluating the if conditions that were supposed to re-enable them. .github/workflows/build.yaml — all test-job if blocks need always() && prepended to honour the fail-open contract. Important Files Changed
Reviews (1): Last reviewed commit: "Add file-level import graph for per-PR C..." | Re-trigger Greptile |
| if: >- | ||
| github.event_name != 'push' && | ||
| needs.build.result == 'success' | ||
| needs.build.result == 'success' && | ||
| (needs.compute-affected-packages.result != 'success' || | ||
| contains(fromJSON(needs.compute-affected-packages.outputs.affected_jobs), 'test-isaaclab-tasks')) |
There was a problem hiding this comment.
Fail-open broken when
compute-affected-packages fails
In GitHub Actions, when a job listed in needs fails (not skipped — actually errors), GitHub automatically cancels dependent jobs before evaluating their if condition. The condition needs.compute-affected-packages.result != 'success' is never reached because the test job is pre-emptively skipped. The PR description promises fail-open behavior ("any failure in the compute job fails open"), but the current implementation delivers fail-closed: if the script crashes or git errors out, every test job is silently cancelled.
To honour the fail-open contract each test job's if needs to begin with always() && so GitHub evaluates the expression instead of auto-cancelling. The same fix applies to all other test jobs that share the same if pattern.
| def _changed_files_since(ref: str) -> list[str]: | ||
| result = subprocess.run( | ||
| ["git", "diff", "--name-only", ref], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ) |
There was a problem hiding this comment.
git diff --name-only <ref> compares the working tree (including staged and unstaged changes) against ref, not two commits. In CI this produces the right result only because there are no local modifications; locally, any uncommitted edits are silently included, causing the impact graph to diverge from what was actually pushed. Using the two-argument form ref HEAD is explicit about comparing two commits.
| def _changed_files_since(ref: str) -> list[str]: | |
| result = subprocess.run( | |
| ["git", "diff", "--name-only", ref], | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ) | |
| def _changed_files_since(ref: str) -> list[str]: | |
| result = subprocess.run( | |
| ["git", "diff", "--name-only", ref, "HEAD"], | |
| capture_output=True, | |
| text=True, | |
| check=True, | |
| ) |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| import re | ||
| import subprocess | ||
| import sys | ||
| from collections import defaultdict | ||
| from pathlib import Path | ||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Constants | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| _PREFIX = "isaaclab" | ||
| _DEP_NAME_RE = re.compile(r"^([A-Za-z0-9_-]+)") |
There was a problem hiding this comment.
_DEP_NAME_RE is defined but never referenced anywhere in the file, dragging import re along as an unused import.
| import re | |
| import subprocess | |
| import sys | |
| from collections import defaultdict | |
| from pathlib import Path | |
| # --------------------------------------------------------------------------- | |
| # Constants | |
| # --------------------------------------------------------------------------- | |
| _PREFIX = "isaaclab" | |
| _DEP_NAME_RE = re.compile(r"^([A-Za-z0-9_-]+)") | |
| import subprocess | |
| import sys | |
| from collections import defaultdict | |
| from pathlib import Path | |
| # --------------------------------------------------------------------------- | |
| # Constants | |
| # --------------------------------------------------------------------------- | |
| _PREFIX = "isaaclab" |
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| tc_ids: set[int] = set() | ||
| for node in ast.walk(tree): | ||
| if isinstance(node, ast.If): | ||
| t = node.test | ||
| if (isinstance(t, ast.Name) and t.id == "TYPE_CHECKING") or ( | ||
| isinstance(t, ast.Attribute) and t.attr == "TYPE_CHECKING" | ||
| ): | ||
| for child in ast.walk(node): | ||
| tc_ids.add(id(child)) |
There was a problem hiding this comment.
else branch of if TYPE_CHECKING incorrectly excluded
ast.walk(node) starting from the ast.If node traverses the entire subtree — both the body and the orelse (the else branch). Any imports in an else clause of if TYPE_CHECKING: ... else: <runtime imports> are added to tc_ids and filtered out, so those runtime imports are silently missed. This pattern is uncommon but valid Python; the fix is to walk only node.body instead of the full If node.
Replaces the static _PACKAGE_TO_CI_JOB dict with _derive_pkg_to_job_map() which parses .github/workflows/build.yaml at runtime. For each job using run-package-tests, it extracts the filter-pattern and maps every source package whose name contains that pattern to the corresponding canonical job label (shortest shard ID for sharded jobs). A package can now map to multiple labels, so all matching CI jobs are triggered when it changes.
The compute step now pip-installs pyyaml (needed for the dynamic job map) and derives the fail-open "all jobs" list for push events from the graph rather than a hardcoded string, so new test jobs added to build.yaml are picked up automatically.
|
🤖 AI-assisted review (batch triage of open CI/infra PRs) Closing as stale: no activity since creation a month ago, and develop's workflows have restructured underneath it — the Feel free to reopen if this was closed in error. |
Description
Adds tools/package_impact_graph.py which walks every .py file under source/, parses runtime imports (excluding TYPE_CHECKING guards), and builds a reverse-dependency graph from that. Given a set of changed files it computes which CI test jobs are transitively affected and emits a manifest showing the import chain for each triggered job.
Integrates this into build.yaml via a new compute-affected-packages job that runs on ubuntu-latest in parallel with the Docker build. Each test job gains a contains(fromJSON(...)) guard so it is skipped when its package is not in the affected set. Non-Python file changes and any failure in the compute job both fail open (all jobs run).
6 seconds to scan 963 files across all packages - well within CI budget. The script stays as a tool, never committed as a JSON artifact. CI generates it fresh each run with python3 tools/package_impact_graph.py.
Fixes # (issue)
Type of change
Screenshots
Please attach before and after screenshots of the change if applicable.
Checklist
pre-commitchecks with./isaaclab.sh --formatconfig/extension.tomlfileCONTRIBUTORS.mdor my name already exists there