From 54061bcf7c527bf5b37489fa2fcf3ab77d3b2337 Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Tue, 7 Jul 2026 15:17:17 -0400 Subject: [PATCH 1/3] affected ci test jobs --- .../actions/select-ci-jobs/select_ci_jobs.py | 183 +++++++++++ .../select-ci-jobs/test_select_ci_jobs.py | 146 +++++++++ .github/workflows/build.yaml | 286 ++++++++---------- .../ci-path-based-job-selection.skip | 2 + 4 files changed, 464 insertions(+), 153 deletions(-) create mode 100644 .github/actions/select-ci-jobs/select_ci_jobs.py create mode 100644 .github/actions/select-ci-jobs/test_select_ci_jobs.py create mode 100644 source/isaaclab/changelog.d/ci-path-based-job-selection.skip diff --git a/.github/actions/select-ci-jobs/select_ci_jobs.py b/.github/actions/select-ci-jobs/select_ci_jobs.py new file mode 100644 index 000000000000..dc6f3bf34ab5 --- /dev/null +++ b/.github/actions/select-ci-jobs/select_ci_jobs.py @@ -0,0 +1,183 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Select which CI test jobs to run for a change based on the affected files. + +The Docker test jobs in ``.github/workflows/build.yaml`` are expensive. Rather than run +every job on every pull request, this helper maps the set of changed files to a per-job +"run" flag using a conservative, dependency-aware path map derived from the inter-package +import graph. + +Design invariants: + +* **Conservative by construction.** Any change to a hub package (core, assets, tasks, + newton, physx), to shared low-level packages, to build tooling / Docker / CI, or to any + path not covered by an explicit rule falls back to running the *entire* suite. The + failure mode is always "runs too much", never "misses a break". +* **Leaf packages get a subset.** Packages that few or nothing depend on + (``isaaclab_mimic``, ``isaaclab_rl``, ``isaaclab_teleop``, ``isaaclab_visualizers``, + ``isaaclab_ov`` / ``isaaclab_ovphysx``, ``isaaclab_contrib``) map to just the jobs whose + tests can be affected. +* **Docs-only changes run nothing.** Markdown / reStructuredText / changelog fragments and + anything under a ``docs/`` directory contribute no jobs. + +The module exposes a pure :func:`select_ci_jobs` for unit testing and a ``__main__`` entry +point that reads newline-separated changed paths from stdin and writes ``run_=`` +lines to stdout (append to ``$GITHUB_OUTPUT``). +""" + +from __future__ import annotations + +import re +import sys + +# --- Job groups ------------------------------------------------------------------------- +# Each name corresponds to a ``run_`` output consumed by a job's ``if:`` in build.yaml. +# The three sharded tasks/core jobs share a single flag. +_BASE_JOBS: tuple[str, ...] = ( + "core", + "tasks", + "rl", + "mimic", + "contrib", + "teleop", + "visualizers", + "assets", + "newton", + "physx", + "ov", + "curobo", + "skillgen", + "environments_training", + "rendering", + "rendering_kitless", +) + +# The tasks package drives its own job plus the rendering / training / curobo / skillgen +# jobs, which all exercise ``source/isaaclab_tasks``. +_TASKS_FAMILY: frozenset[str] = frozenset( + {"tasks", "rendering", "rendering_kitless", "environments_training", "curobo", "skillgen"} +) + +# --- Path classification ---------------------------------------------------------------- +# Docs / changelog files never trigger tests. +_IGNORED_SUFFIXES = (".md", ".rst", ".skip") + +# Package prefixes that fan out to the whole suite because many packages import them +# (core is imported by every package; assets/newton/physx/tasks are deep hubs) or because +# they are shared low-level packages without a dedicated job. +_GLOBAL_PACKAGE_PREFIXES: tuple[str, ...] = ( + "source/isaaclab/", + "source/isaaclab_assets/", + "source/isaaclab_tasks/", + "source/isaaclab_newton/", + "source/isaaclab_physx/", + "source/isaaclab_experimental/", + "source/isaaclab_ppisp/", + "source/isaaclab_tasks_experimental/", +) + +# Non-source directories/files whose changes can affect any job. +_GLOBAL_DIR_PREFIXES: tuple[str, ...] = ("docker/", "tools/", "apps/", "scripts/") +_GLOBAL_CI_PREFIXES: tuple[str, ...] = (".github/actions/", ".github/test-subsets/") +_GLOBAL_CI_FILES: frozenset[str] = frozenset( + { + ".github/workflows/build.yaml", + ".github/workflows/config.yaml", + } +) + +# Repo-root config files (dependency pins, lock files, top-level tooling config). +_ROOT_CONFIG = re.compile(r"^(?:[^/]+\.(?:toml|yaml|yml|json|ini|cfg|conf|lock|sh|bat|ps1)|\.gitmodules)$") + +# Leaf packages -> the exact set of job groups whose tests they can affect. Derived from the +# import graph; see the module docstring. ``core`` is included where core lazily imports the +# package (video recorder / markers / teleop shim), so editing it can touch core runtime paths. +_TARGETED: dict[str, frozenset[str]] = { + "source/isaaclab_mimic/": frozenset({"mimic"}), + "source/isaaclab_rl/": frozenset({"rl"}) | _TASKS_FAMILY, + "source/isaaclab_teleop/": frozenset({"teleop", "core"}) | _TASKS_FAMILY, + "source/isaaclab_visualizers/": frozenset({"visualizers", "core"}), + "source/isaaclab_ov/": frozenset({"ov", "core"}) | _TASKS_FAMILY, + "source/isaaclab_ovphysx/": frozenset({"ov", "core"}) | _TASKS_FAMILY, + "source/isaaclab_contrib/": frozenset({"contrib", "core", "newton", "physx", "assets"}) | _TASKS_FAMILY, +} + + +def _is_ignored(path: str) -> bool: + """Return whether a docs/changelog path should contribute no test jobs.""" + return path.endswith(_IGNORED_SUFFIXES) or path.startswith("docs/") or "/docs/" in path + + +def _is_relevant(path: str) -> bool: + """Return whether a path can affect the Docker test jobs at all. + + Files that are neither relevant nor ignored (e.g. unrelated workflow files, license + metadata) contribute no jobs, mirroring the previous all-or-nothing gate. + """ + if path.startswith("source/"): + return True + if path.startswith(_GLOBAL_DIR_PREFIXES) or path.startswith(_GLOBAL_CI_PREFIXES): + return True + return path in _GLOBAL_CI_FILES or bool(_ROOT_CONFIG.match(path)) + + +def _is_global(path: str) -> bool: + """Return whether a relevant path forces the full suite to run.""" + if path.startswith(_GLOBAL_PACKAGE_PREFIXES): + return True + if path.startswith(_GLOBAL_DIR_PREFIXES) or path.startswith(_GLOBAL_CI_PREFIXES): + return True + return path in _GLOBAL_CI_FILES or bool(_ROOT_CONFIG.match(path)) + + +def _all_jobs(value: bool) -> dict[str, bool]: + """Return a flag dict with every base job set to ``value`` plus derived flags.""" + return _with_derived({job: value for job in _BASE_JOBS}) + + +def _with_derived(flags: dict[str, bool]) -> dict[str, bool]: + """Add the aggregate ``any`` (build gate) and ``curobo_image`` (build-curobo gate) flags.""" + flags = dict(flags) + flags["any"] = any(flags[job] for job in _BASE_JOBS) + flags["curobo_image"] = flags["curobo"] or flags["skillgen"] + return flags + + +def select_ci_jobs(paths: list[str]) -> dict[str, bool]: + """Map changed paths to per-job run flags. + + Args: + paths: Repo-relative changed file paths (e.g. from the pull request files API). + + Returns: + A mapping of job group -> whether that job should run, including the aggregate + ``any`` and ``curobo_image`` gates. An empty ``paths`` list returns all-true as a + fail-safe; a change touching only ignored docs returns all-false. + """ + if not paths: + return _all_jobs(True) + + relevant = [path for path in paths if _is_relevant(path) and not _is_ignored(path)] + if not relevant: + return _all_jobs(False) + + flags = {job: False for job in _BASE_JOBS} + for path in relevant: + if _is_global(path): + return _all_jobs(True) + jobs = next((groups for prefix, groups in _TARGETED.items() if path.startswith(prefix)), None) + if jobs is None: + # Relevant but not covered by a targeted rule (e.g. a new package): fail safe. + return _all_jobs(True) + for job in jobs: + flags[job] = True + return _with_derived(flags) + + +if __name__ == "__main__": + selected = select_ci_jobs([line.strip() for line in sys.stdin if line.strip()]) + for key in list(_BASE_JOBS) + ["any", "curobo_image"]: + print(f"run_{key}={'true' if selected[key] else 'false'}") diff --git a/.github/actions/select-ci-jobs/test_select_ci_jobs.py b/.github/actions/select-ci-jobs/test_select_ci_jobs.py new file mode 100644 index 000000000000..e39900d44917 --- /dev/null +++ b/.github/actions/select-ci-jobs/test_select_ci_jobs.py @@ -0,0 +1,146 @@ +# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md). +# All rights reserved. +# +# SPDX-License-Identifier: BSD-3-Clause + +"""Tests for the path-based CI job selector.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import pytest + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from select_ci_jobs import _BASE_JOBS, select_ci_jobs + +_TASKS_FAMILY = {"tasks", "rendering", "rendering_kitless", "environments_training", "curobo", "skillgen"} + + +def _true_jobs(paths: list[str]) -> set[str]: + """Return the set of base job groups selected to run for ``paths``.""" + flags = select_ci_jobs(paths) + return {job for job in _BASE_JOBS if flags[job]} + + +def test_empty_change_set_runs_everything_as_failsafe(): + assert _true_jobs([]) == set(_BASE_JOBS) + assert select_ci_jobs([])["any"] is True + + +def test_core_change_runs_everything(): + assert _true_jobs(["source/isaaclab/isaaclab/assets/articulation.py"]) == set(_BASE_JOBS) + + +@pytest.mark.parametrize( + "path", + [ + "source/isaaclab_assets/x.py", + "source/isaaclab_tasks/x.py", + "source/isaaclab_newton/x.py", + "source/isaaclab_physx/x.py", + "source/isaaclab_experimental/x.py", + "source/isaaclab_ppisp/x.py", + "source/isaaclab_tasks_experimental/x.py", + ], +) +def test_hub_and_shared_packages_run_everything(path): + assert _true_jobs([path]) == set(_BASE_JOBS) + + +@pytest.mark.parametrize( + "path", + [ + "docker/Dockerfile.base", + "tools/conftest.py", + "apps/foo.kit", + "scripts/train.py", + ".github/actions/run-tests/action.yml", + ".github/workflows/build.yaml", + ".github/workflows/config.yaml", + ".github/test-subsets/postmerge-rendering.toml", + "pyproject.toml", + "isaaclab.sh", + ".gitmodules", + ], +) +def test_tooling_and_ci_changes_run_everything(path): + assert _true_jobs([path]) == set(_BASE_JOBS) + + +def test_mimic_only_runs_mimic(): + assert _true_jobs(["source/isaaclab_mimic/isaaclab_mimic/foo.py"]) == {"mimic"} + + +def test_rl_only_runs_rl_and_tasks_family(): + assert _true_jobs(["source/isaaclab_rl/isaaclab_rl/foo.py"]) == {"rl"} | _TASKS_FAMILY + + +def test_teleop_only_runs_teleop_tasks_family_and_core(): + assert _true_jobs(["source/isaaclab_teleop/foo.py"]) == {"teleop", "core"} | _TASKS_FAMILY + + +def test_visualizers_only_runs_visualizers_and_core(): + assert _true_jobs(["source/isaaclab_visualizers/foo.py"]) == {"visualizers", "core"} + + +@pytest.mark.parametrize("pkg", ["isaaclab_ov", "isaaclab_ovphysx"]) +def test_ov_packages_run_ov_tasks_family_and_core(pkg): + assert _true_jobs([f"source/{pkg}/foo.py"]) == {"ov", "core"} | _TASKS_FAMILY + + +def test_contrib_only_runs_contrib_and_dependents(): + assert _true_jobs(["source/isaaclab_contrib/foo.py"]) == ( + {"contrib", "core", "newton", "physx", "assets"} | _TASKS_FAMILY + ) + + +@pytest.mark.parametrize( + "path", + [ + "README.md", + "docs/source/setup/installation/pip_installation.rst", + "source/isaaclab_mimic/docs/CHANGELOG.rst", + "source/isaaclab/changelog.d/foo.skip", + "source/isaaclab_tasks/docs/index.md", + ], +) +def test_docs_and_changelog_only_run_nothing(path): + assert _true_jobs([path]) == set() + assert select_ci_jobs([path])["any"] is False + + +def test_unrelated_files_run_nothing(): + # A change touching only files that cannot affect the docker tests (e.g. an unrelated + # workflow or license metadata) selects no jobs, matching the previous all-or-nothing gate. + assert _true_jobs([".github/workflows/labeler.yml", "LICENSE"]) == set() + + +def test_leaf_plus_hub_falls_back_to_everything(): + assert _true_jobs(["source/isaaclab_mimic/foo.py", "source/isaaclab/isaaclab/bar.py"]) == set(_BASE_JOBS) + + +def test_multiple_leaf_packages_union_their_jobs(): + assert _true_jobs(["source/isaaclab_mimic/foo.py", "source/isaaclab_visualizers/bar.py"]) == { + "mimic", + "visualizers", + "core", + } + + +def test_unknown_source_package_falls_back_to_everything(): + assert _true_jobs(["source/isaaclab_brandnew/foo.py"]) == set(_BASE_JOBS) + + +def test_derived_flags(): + # curobo_image tracks the curobo image jobs; any tracks build gating. + docs = select_ci_jobs(["README.md"]) + assert docs["curobo_image"] is False and docs["any"] is False + + rl = select_ci_jobs(["source/isaaclab_rl/foo.py"]) + assert rl["curobo_image"] is True # curobo + skillgen are in the tasks family + + mimic = select_ci_jobs(["source/isaaclab_mimic/foo.py"]) + assert mimic["curobo_image"] is False and mimic["any"] is True diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 39f9ffab1b2c..fdef038938c7 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -4,8 +4,13 @@ # SPDX-License-Identifier: BSD-3-Clause # region help -# Every test job runs on every PR and merge queue candidate. Pushes to protected branches run the -# post-merge integration tests. +# On pull requests, the `changes` job maps the changed files to per-job run flags via +# .github/actions/select-ci-jobs/select_ci_jobs.py: a test job runs only when files affecting +# its package (or a package it depends on) changed. Hub packages (core/assets/tasks/newton/ +# physx), build tooling, Docker, and CI changes still run the full suite; docs-only changes run +# nothing. Skipped jobs report green to branch protection. To force a full run, touch a +# global-trigger path or dispatch the workflow manually. Pushes to protected branches (main, +# develop, release/**) run the entire suite: all flags default to true on non-PR events. # # ============================================================================= # CI DEBUGGING TIPS @@ -79,8 +84,34 @@ jobs: name: Detect Changes runs-on: ubuntu-latest outputs: - run_docker_tests: ${{ steps.detect.outputs.run_docker_tests }} + # Per-job run flags computed by select_ci_jobs.py from the PR's changed files. + # A test job runs only when its flag is 'true'; skipped jobs report green to branch + # protection (that's why we gate via `if:` rather than a workflow-level `paths:` + # filter, which would leave a required check un-triggered and block the PR forever). + run_any: ${{ steps.detect.outputs.run_any }} + run_curobo_image: ${{ steps.detect.outputs.run_curobo_image }} + run_core: ${{ steps.detect.outputs.run_core }} + run_tasks: ${{ steps.detect.outputs.run_tasks }} + run_rl: ${{ steps.detect.outputs.run_rl }} + run_mimic: ${{ steps.detect.outputs.run_mimic }} + run_contrib: ${{ steps.detect.outputs.run_contrib }} + run_teleop: ${{ steps.detect.outputs.run_teleop }} + run_visualizers: ${{ steps.detect.outputs.run_visualizers }} + run_assets: ${{ steps.detect.outputs.run_assets }} + run_newton: ${{ steps.detect.outputs.run_newton }} + run_physx: ${{ steps.detect.outputs.run_physx }} + run_ov: ${{ steps.detect.outputs.run_ov }} + run_curobo: ${{ steps.detect.outputs.run_curobo }} + run_skillgen: ${{ steps.detect.outputs.run_skillgen }} + run_environments_training: ${{ steps.detect.outputs.run_environments_training }} + run_rendering: ${{ steps.detect.outputs.run_rendering }} + run_rendering_kitless: ${{ steps.detect.outputs.run_rendering_kitless }} steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 1 + sparse-checkout: .github/actions/select-ci-jobs + sparse-checkout-cone-mode: false - id: detect env: GH_TOKEN: ${{ github.token }} @@ -90,99 +121,45 @@ jobs: run: | set -euo pipefail - # Docker test jobs run only when paths in the patterns table change. - # Otherwise they skip via `if:` and report green to branch protection, - # which is why we don't use a workflow-level `paths:` filter (a - # not-triggered required check would block the PR forever). - # config.yaml is included because it controls the base image names and - # tags consumed by the Docker build jobs. - patterns=( - $'^source/\tLibrary source code' - $'^docker/\tContainer build inputs' - $'^tools/\tBuild tooling' - $'^apps/\tStandalone apps' - $'^scripts/\tStandalone scripts' - $'^\\.github/workflows/build\\.yaml$\tThis workflow file' - $'^\\.github/workflows/config\\.yaml$\tBase image config' - $'^\\.github/actions/\tCI actions' - $'^\\.github/test-subsets/\tCI test subset config' - ) - if [ "$EVENT_NAME" = "push" ]; then - triggered_jobs="Docker base build job + rendering-correctness + rendering-correctness-kitless" - else - triggered_jobs="Docker build jobs + all test-* matrix jobs" + # For pull_request events, select_ci_jobs.py maps the changed files to per-job run + # flags. For every other event (push to a protected branch, workflow_dispatch) we + # feed it an empty change set, which returns all flags true so the full suite runs + # -- the same fail-safe applied when the changed-files API is unavailable. + changed_files="" + reason="non-PR event ($EVENT_NAME): running the full suite" + if [ "$EVENT_NAME" = "pull_request" ]; then + if changed_files="$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')"; then + reason="selected from changed files" + printf '%s\n' "$changed_files" + else + echo "::warning::Could not list changed files; defaulting to the full suite" + changed_files="" + reason="fail-safe (could not list changed files): running the full suite" + fi fi - render_table() { - local files="$1" entry regex desc count sample shown - echo "| Pattern | What it covers | Matched files |" - echo "|---|---|---|" - for entry in "${patterns[@]}"; do - IFS=$'\t' read -r regex desc <<< "$entry" - # escape | so it doesn't end the markdown table cell mid-regex - shown="${regex//|/\\|}" - count=$(grep -cE "$regex" <<< "$files" || true) - if [ "$count" -gt 0 ]; then - sample=$(grep -m 3 -E "$regex" <<< "$files" | paste -sd ', ' -) - [ "$count" -gt 3 ] && sample="$sample (and $((count - 3)) more)" - echo "| \`$shown\` | $desc | $sample |" - else - echo "| \`$shown\` | $desc | - |" - fi - done - } - - any_match() { - local files="$1" entry regex - for entry in "${patterns[@]}"; do - IFS=$'\t' read -r regex _ <<< "$entry" - if grep -qE "$regex" <<< "$files"; then - return 0 - fi - done - return 1 - } - - decide() { - local decision="$1" reason="$2" files="${3:-}" - echo "Decision: run_docker_tests=$decision ($reason)" - echo "run_docker_tests=$decision" >> "$GITHUB_OUTPUT" - { - echo "## Docker test gating" + job_flags="$(printf '%s\n' "$changed_files" | python3 .github/actions/select-ci-jobs/select_ci_jobs.py)" + printf '%s\n' "$job_flags" >> "$GITHUB_OUTPUT" + + { + echo "## CI test job selection" + echo "" + echo "$reason." + echo "" + echo "| Job flag | Runs? |" + echo "|---|---|" + while IFS='=' read -r key value; do + [ -n "$key" ] && echo "| \`$key\` | $value |" + done <<< "$job_flags" + if [ -n "$changed_files" ]; then echo "" - if [ "$decision" = "true" ]; then - echo "Docker tests will **run**: $reason." - else - echo "Docker tests will be **skipped**: $reason." - fi + echo "
Changed files" echo "" - echo "Triggered jobs: $triggered_jobs." - if [ -n "$files" ]; then - echo "" - render_table "$files" - fi - } >> "$GITHUB_STEP_SUMMARY" - } - - if [ "$EVENT_NAME" != "pull_request" ]; then - decide true "non-PR event ($EVENT_NAME)" - exit 0 - fi - - if ! changed_files="$(gh api --paginate "repos/$REPO/pulls/$PR_NUMBER/files" --jq '.[].filename')"; then - # Fail-safe: a transient API error must not block merge. Default to running. - echo "::warning::Could not list changed files; defaulting to running tests" - decide true "fail-safe (could not list changed files)" - exit 0 - fi - - printf '%s\n' "$changed_files" - - if any_match "$changed_files"; then - decide true "relevant paths changed" "$changed_files" - else - decide false "no relevant paths changed" "$changed_files" - fi + printf '%s\n' "$changed_files" | sed 's/^/- /' + echo "" + echo "
" + fi + } >> "$GITHUB_STEP_SUMMARY" config: name: Load Config @@ -233,7 +210,7 @@ jobs: name: Build Base Docker Image runs-on: [self-hosted, gpu] needs: [changes, config] - if: needs.changes.outputs.run_docker_tests == 'true' + if: needs.changes.outputs.run_any == 'true' steps: - name: Checkout Code uses: actions/checkout@v6 @@ -255,8 +232,7 @@ jobs: runs-on: [self-hosted, gpu] needs: [changes, config] if: >- - github.event_name != 'push' && - needs.changes.outputs.run_docker_tests == 'true' + needs.changes.outputs.run_curobo_image == 'true' steps: - name: Checkout Code uses: actions/checkout@v6 @@ -281,10 +257,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 180 continue-on-error: true - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_tasks == 'true' steps: - uses: actions/checkout@v6 with: @@ -306,10 +282,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 180 continue-on-error: true - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_tasks == 'true' steps: - uses: actions/checkout@v6 with: @@ -331,10 +307,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 180 continue-on-error: true - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_tasks == 'true' steps: - uses: actions/checkout@v6 with: @@ -355,10 +331,10 @@ jobs: name: isaaclab (core) [1/3] runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_core == 'true' steps: - uses: actions/checkout@v6 with: @@ -378,10 +354,10 @@ jobs: name: isaaclab (core) [2/3] runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_core == 'true' steps: - uses: actions/checkout@v6 with: @@ -401,10 +377,10 @@ jobs: name: isaaclab (core) [3/3] runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_core == 'true' steps: - uses: actions/checkout@v6 with: @@ -424,10 +400,10 @@ jobs: name: isaaclab_rl runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_rl == 'true' steps: - uses: actions/checkout@v6 with: @@ -446,10 +422,10 @@ jobs: name: isaaclab_mimic runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_mimic == 'true' steps: - uses: actions/checkout@v6 with: @@ -467,10 +443,10 @@ jobs: name: isaaclab_contrib runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_contrib == 'true' steps: - uses: actions/checkout@v6 with: @@ -488,10 +464,10 @@ jobs: name: isaaclab_teleop runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_teleop == 'true' steps: - uses: actions/checkout@v6 with: @@ -509,10 +485,10 @@ jobs: name: isaaclab_visualizers runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_visualizers == 'true' steps: - uses: actions/checkout@v6 with: @@ -530,10 +506,10 @@ jobs: name: isaaclab_assets runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_assets == 'true' steps: - uses: actions/checkout@v6 with: @@ -551,10 +527,10 @@ jobs: name: isaaclab_newton runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_newton == 'true' steps: - uses: actions/checkout@v6 with: @@ -572,10 +548,10 @@ jobs: name: isaaclab_physx runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_physx == 'true' steps: - uses: actions/checkout@v6 with: @@ -593,10 +569,10 @@ jobs: name: isaaclab_ov runs-on: [self-hosted, gpu] timeout-minutes: 180 - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_ov == 'true' env: USE_OVPHYSX_WHEELHOUSE: ${{ needs.config.outputs.ovphysx_wheelhouse_resource != '' && ((github.event_name == 'pull_request' && github.base_ref == 'develop' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name != 'pull_request' && github.ref_name == 'develop')) }} steps: @@ -650,10 +626,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 120 continue-on-error: true - needs: [build-curobo, config] + needs: [changes, build-curobo, config] if: >- - github.event_name != 'push' && - needs.build-curobo.result == 'success' + needs.build-curobo.result == 'success' && + needs.changes.outputs.run_curobo == 'true' steps: - uses: actions/checkout@v6 with: @@ -700,10 +676,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 120 continue-on-error: true - needs: [build-curobo, config] + needs: [changes, build-curobo, config] if: >- - github.event_name != 'push' && - needs.build-curobo.result == 'success' + needs.build-curobo.result == 'success' && + needs.changes.outputs.run_skillgen == 'true' steps: - uses: actions/checkout@v6 with: @@ -724,10 +700,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 300 continue-on-error: true - needs: [build, config] + needs: [changes, build, config] if: >- - github.event_name != 'push' && - needs.build.result == 'success' + needs.build.result == 'success' && + needs.changes.outputs.run_environments_training == 'true' steps: - uses: actions/checkout@v6 with: @@ -748,8 +724,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 120 continue-on-error: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} - needs: [build, config] - if: needs.build.result == 'success' + needs: [changes, build, config] + if: >- + needs.build.result == 'success' && + needs.changes.outputs.run_rendering == 'true' steps: - uses: actions/checkout@v6 with: @@ -777,8 +755,10 @@ jobs: runs-on: [self-hosted, gpu] timeout-minutes: 120 continue-on-error: ${{ github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' }} - needs: [build, config] - if: needs.build.result == 'success' + needs: [changes, build, config] + if: >- + needs.build.result == 'success' && + needs.changes.outputs.run_rendering_kitless == 'true' env: USE_OVPHYSX_WHEELHOUSE: ${{ needs.config.outputs.ovphysx_wheelhouse_resource != '' && ((github.event_name == 'pull_request' && github.base_ref == 'develop' && github.event.pull_request.head.repo.full_name == github.repository) || (github.event_name != 'pull_request' && github.ref_name == 'develop')) }} steps: diff --git a/source/isaaclab/changelog.d/ci-path-based-job-selection.skip b/source/isaaclab/changelog.d/ci-path-based-job-selection.skip new file mode 100644 index 000000000000..9b944da057bb --- /dev/null +++ b/source/isaaclab/changelog.d/ci-path-based-job-selection.skip @@ -0,0 +1,2 @@ +CI-only: select which Docker test jobs run on a pull request from the affected packages +(.github/actions/select-ci-jobs/select_ci_jobs.py) instead of running every job on every PR. From d41037b03cfe9f593833161697852aa389b5ab99 Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Tue, 7 Jul 2026 15:43:27 -0400 Subject: [PATCH 2/3] update --- .github/actions/select-ci-jobs/select_ci_jobs.py | 2 +- .github/actions/select-ci-jobs/test_select_ci_jobs.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/actions/select-ci-jobs/select_ci_jobs.py b/.github/actions/select-ci-jobs/select_ci_jobs.py index dc6f3bf34ab5..74b6bf3219c9 100644 --- a/.github/actions/select-ci-jobs/select_ci_jobs.py +++ b/.github/actions/select-ci-jobs/select_ci_jobs.py @@ -99,7 +99,7 @@ "source/isaaclab_mimic/": frozenset({"mimic"}), "source/isaaclab_rl/": frozenset({"rl"}) | _TASKS_FAMILY, "source/isaaclab_teleop/": frozenset({"teleop", "core"}) | _TASKS_FAMILY, - "source/isaaclab_visualizers/": frozenset({"visualizers", "core"}), + "source/isaaclab_visualizers/": frozenset({"visualizers", "core", "rendering", "rendering_kitless"}), "source/isaaclab_ov/": frozenset({"ov", "core"}) | _TASKS_FAMILY, "source/isaaclab_ovphysx/": frozenset({"ov", "core"}) | _TASKS_FAMILY, "source/isaaclab_contrib/": frozenset({"contrib", "core", "newton", "physx", "assets"}) | _TASKS_FAMILY, diff --git a/.github/actions/select-ci-jobs/test_select_ci_jobs.py b/.github/actions/select-ci-jobs/test_select_ci_jobs.py index e39900d44917..436c3de9db7e 100644 --- a/.github/actions/select-ci-jobs/test_select_ci_jobs.py +++ b/.github/actions/select-ci-jobs/test_select_ci_jobs.py @@ -82,8 +82,13 @@ def test_teleop_only_runs_teleop_tasks_family_and_core(): assert _true_jobs(["source/isaaclab_teleop/foo.py"]) == {"teleop", "core"} | _TASKS_FAMILY -def test_visualizers_only_runs_visualizers_and_core(): - assert _true_jobs(["source/isaaclab_visualizers/foo.py"]) == {"visualizers", "core"} +def test_visualizers_only_runs_visualizers_core_and_rendering(): + assert _true_jobs(["source/isaaclab_visualizers/foo.py"]) == { + "visualizers", + "core", + "rendering", + "rendering_kitless", + } @pytest.mark.parametrize("pkg", ["isaaclab_ov", "isaaclab_ovphysx"]) @@ -127,6 +132,8 @@ def test_multiple_leaf_packages_union_their_jobs(): "mimic", "visualizers", "core", + "rendering", + "rendering_kitless", } From 694d1700d55f571b2322d3046c5f040c2218e271 Mon Sep 17 00:00:00 2001 From: Matthew Taylor Date: Tue, 7 Jul 2026 15:52:35 -0400 Subject: [PATCH 3/3] ci demo: touch source/isaaclab_rl/ci_demo.txt --- source/isaaclab_rl/ci_demo.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 source/isaaclab_rl/ci_demo.txt diff --git a/source/isaaclab_rl/ci_demo.txt b/source/isaaclab_rl/ci_demo.txt new file mode 100644 index 000000000000..9ef5b78c5727 --- /dev/null +++ b/source/isaaclab_rl/ci_demo.txt @@ -0,0 +1 @@ +CI demo change to exercise path-based job selection.