Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ _isaac_sim
docker/.isaac-lab-docker-history
# ignore uv environment
env_isaaclab
.venv/
tools/wheel_builder/build/
29 changes: 29 additions & 0 deletions .github/actions/ecr-build-push-pull/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ inputs:
description: Tag used for the ECR layer cache image (e.g. "cache-base", "cache-curobo").
required: false
default: 'cache'
pull-on-deps-hit:
description: >
Pull the image locally after a deps-cache hit. Needed by jobs that run
the image in later steps of the same job: the ECR login lives in a
temporary docker config that is removed when this action ends, so
steps after the action cannot pull.
required: false
default: 'false'
runs:
using: composite
steps:
Expand Down Expand Up @@ -268,6 +276,27 @@ runs:
docker tag "${{ inputs.image-tag }}" "${DEPS_ECR_IMAGE}"
docker push "${DEPS_ECR_IMAGE}"

##### 8b: Pull deps-cached image for same-job use #####

# On a deps-cache hit step 5 only aliases the commit tag registry-side.
# Jobs that docker-run the image in later steps opt in here, while the
# temporary docker config (and its ECR login) still exists.

- name: Pull deps-cached image
if: >
steps.deps-cache.outputs.deps-cache-hit == 'true' &&
inputs.pull-on-deps-hit == 'true'
shell: bash
run: |
if docker image inspect "${{ inputs.image-tag }}" >/dev/null 2>&1; then
echo "🟢 Image already available locally, skipping pull"
else
echo "🔵 Pulling ${ECR_IMAGE} from ECR..."
docker pull "${ECR_IMAGE}"
docker tag "${ECR_IMAGE}" "${{ inputs.image-tag }}"
echo "🟢 Image pulled and tagged as ${{ inputs.image-tag }}"
fi

##### 9: Cleanup docker config #####

- name: Cleanup docker config
Expand Down
133 changes: 24 additions & 109 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,112 +77,27 @@ env:
jobs:
changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
run_docker_tests: ${{ steps.detect.outputs.run_docker_tests }}
steps:
- id: detect
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
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"
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"
echo ""
if [ "$decision" = "true" ]; then
echo "Docker tests will **run**: $reason."
else
echo "Docker tests will be **skipped**: $reason."
fi
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
# Docker test jobs run only when paths in the patterns table change.
# config.yaml is included because it controls the base image names and
# tags consumed by the Docker build jobs.
uses: ./.github/workflows/detect-changes.yml
with:
summary-title: Docker test gating
summary-note: >-
Triggered jobs: ${{ github.event_name == 'push'
&& 'Docker base build job + rendering-correctness + rendering-correctness-kitless'
|| 'Docker build jobs + all test-* matrix jobs' }}.
patterns: |
^source/ Library source code
^docker/ Container build inputs
^tools/ Build tooling
^apps/ Standalone apps
^scripts/ Standalone scripts
^\.github/workflows/build\.yaml$ This workflow file
^\.github/workflows/config\.yaml$ Base image config
^\.github/workflows/detect-changes\.yml$ Path-gating workflow
^\.github/actions/ CI actions
^\.github/test-subsets/ CI test subset config

config:
name: Load Config
Expand Down Expand Up @@ -233,7 +148,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 == 'true'
steps:
- name: Checkout Code
uses: actions/checkout@v6
Expand All @@ -256,7 +171,7 @@ jobs:
needs: [changes, config]
if: >-
github.event_name != 'push' &&
needs.changes.outputs.run_docker_tests == 'true'
needs.changes.outputs.run == 'true'
steps:
- name: Checkout Code
uses: actions/checkout@v6
Expand All @@ -280,7 +195,7 @@ jobs:
name: arm-ci
runs-on: [self-hosted, arm64]
needs: [changes, config]
if: needs.changes.outputs.run_docker_tests == 'true'
if: needs.changes.outputs.run == 'true'
timeout-minutes: 60
continue-on-error: true
steps:
Expand Down
144 changes: 144 additions & 0 deletions .github/workflows/detect-changes.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# 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

# Shared path-gating engine, called via workflow_call so the gate job needs
# no checkout: GitHub resolves local reusable workflows server-side from the
# caller's own commit. Pull requests run the gated jobs only when a changed
# file matches one of the given patterns; non-PR events and changed-file
# listing failures fail open (run). Consuming jobs should skip via `if:` so
# required checks report green instead of never triggering (a workflow-level
# `paths:` filter would leave a required check pending forever).

name: Detect Changed Paths

on:
workflow_call:
inputs:
patterns:
description: >
One entry per line: an extended regex matched against the changed
file paths, then whitespace, then a human-readable description for
the step-summary table. The regex itself must not contain
whitespace (the first whitespace run is the column separator).
required: true
type: string
summary-title:
description: 'Heading for the step-summary section.'
required: false
type: string
default: 'Path gating'
summary-note:
description: 'Optional extra line appended below the decision.'
required: false
type: string
default: ''
outputs:
run:
description: '"true" when the gated jobs should run, "false" otherwise.'
value: ${{ jobs.detect.outputs.run }}

jobs:
detect:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
run: ${{ steps.detect.outputs.run }}
steps:
- id: detect
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
EVENT_NAME: ${{ github.event_name }}
REPO: ${{ github.repository }}
PATTERNS_INPUT: ${{ inputs.patterns }}
SUMMARY_TITLE: ${{ inputs.summary-title }}
SUMMARY_NOTE: ${{ inputs.summary-note }}
run: |
set -euo pipefail

# Parse "regex<whitespace>description" lines into parallel arrays.
regexes=()
descriptions=()
while IFS= read -r line; do
[[ -z "${line//[[:space:]]/}" ]] && continue
regex="${line%%[[:space:]]*}"
desc="${line#"$regex"}"
desc="${desc#"${desc%%[![:space:]]*}"}"
regexes+=("$regex")
descriptions+=("${desc:-$regex}")
done <<< "$PATTERNS_INPUT"

render_table() {
local files="$1" i regex desc count sample shown
echo "| Pattern | What it covers | Matched files |"
echo "|---|---|---|"
for i in "${!regexes[@]}"; do
regex="${regexes[$i]}"
desc="${descriptions[$i]}"
# 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" regex
for regex in "${regexes[@]}"; do
if grep -qE "$regex" <<< "$files"; then
return 0
fi
done
return 1
}

decide() {
local decision="$1" reason="$2" files="${3:-}"
echo "Decision: run=$decision ($reason)"
echo "run=$decision" >> "$GITHUB_OUTPUT"
{
echo "## ${SUMMARY_TITLE}"
echo ""
if [ "$decision" = "true" ]; then
echo "Gated jobs will **run**: $reason."
else
echo "Gated jobs will be **skipped**: $reason."
fi
if [ -n "$SUMMARY_NOTE" ]; then
echo ""
echo "$SUMMARY_NOTE"
fi
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
Loading
Loading