-
Notifications
You must be signed in to change notification settings - Fork 3.8k
[CI] Split arm-ci into a standalone workflow #6699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| # 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 | ||
|
|
||
| name: 'Detect Changes' | ||
| description: >- | ||
| Decide whether a caller's gated jobs should run for the triggering event by | ||
| matching the pull request's changed files against caller-supplied ERE | ||
| patterns. Shared by build.yaml and arm-ci.yml. Non-PR events (and a file | ||
| listing failure) fail open with should_run=true. | ||
| Callers gate their jobs with `if: steps/needs ... should_run == 'true'` rather | ||
| than a workflow-level `paths:` filter, so a job that is (or may become) a | ||
| required status check still gets created and reports green when skipped — a | ||
| not-triggered required check would otherwise stay pending forever. | ||
|
|
||
| inputs: | ||
| patterns: | ||
| description: >- | ||
| Newline-delimited entries of the form `<ERE regex> :: <description>` | ||
| (description optional). If any regex matches a changed file path, | ||
| should_run is true. | ||
| required: true | ||
| triggered-jobs-push: | ||
| description: "Human-readable list of jobs triggered on push events (step-summary text only)." | ||
| required: false | ||
| default: "(post-merge integration jobs)" | ||
| triggered-jobs-pr: | ||
| description: "Human-readable list of jobs triggered on pull_request events (step-summary text only)." | ||
| required: false | ||
| default: "(gated jobs)" | ||
|
|
||
| outputs: | ||
| should_run: | ||
| description: "Whether the caller's gated jobs should run for this event." | ||
| value: ${{ steps.detect.outputs.should_run }} | ||
|
|
||
| runs: | ||
| using: composite | ||
| steps: | ||
| - id: detect | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| REPO: ${{ github.repository }} | ||
| PATTERNS: ${{ inputs.patterns }} | ||
| TRIGGERED_JOBS_PUSH: ${{ inputs.triggered-jobs-push }} | ||
| TRIGGERED_JOBS_PR: ${{ inputs.triggered-jobs-pr }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| # Parse "<regex> :: <description>" lines into parallel arrays (blank | ||
| # lines skipped; the description is optional). | ||
| regexes=(); descs=() | ||
| while IFS= read -r line; do | ||
| [ -z "${line//[[:space:]]/}" ] && continue | ||
| regexes+=("${line%% :: *}") | ||
| if [[ "$line" == *" :: "* ]]; then descs+=("${line#* :: }"); else descs+=(""); fi | ||
| done < <(printf '%s\n' "$PATTERNS") | ||
|
|
||
| if [ "$EVENT_NAME" = "push" ]; then | ||
| triggered_jobs="$TRIGGERED_JOBS_PUSH" | ||
| else | ||
| triggered_jobs="$TRIGGERED_JOBS_PR" | ||
| fi | ||
|
|
||
| any_match() { | ||
| local files="$1" regex | ||
| for regex in "${regexes[@]}"; do | ||
| if grep -qE -- "$regex" <<< "$files"; then | ||
| return 0 | ||
| fi | ||
| done | ||
| return 1 | ||
| } | ||
|
|
||
| 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="${descs[$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 | ||
| # paste -sd cycles delimiter chars, so join on ',' then space it out. | ||
| sample=$(grep -m 3 -E -- "$regex" <<< "$files" | paste -sd , - | sed 's/,/, /g') | ||
| [ "$count" -gt 3 ] && sample="$sample (and $((count - 3)) more)" | ||
| echo "| \`$shown\` | ${desc:--} | $sample |" | ||
| else | ||
| echo "| \`$shown\` | ${desc:--} | - |" | ||
| fi | ||
| done | ||
| } | ||
|
|
||
| decide() { | ||
| local decision="$1" reason="$2" files="${3:-}" | ||
| echo "Decision: should_run=$decision ($reason)" | ||
| echo "should_run=$decision" >> "$GITHUB_OUTPUT" | ||
| { | ||
| echo "## Change detection" | ||
| echo "" | ||
| if [ "$decision" = "true" ]; then | ||
| echo "Gated jobs will **run**: $reason." | ||
| else | ||
| echo "Gated jobs 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| # 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 | ||
|
|
||
| # region help | ||
| # aarch64 build + marker-gated tests on NVIDIA DGX Spark self-hosted runners. | ||
| # | ||
| # Split out of build.yaml (Docker + Tests) so this slow arm64 build+render owns | ||
| # its own workflow run. In build.yaml it shared the run with every test-* job, | ||
| # so the whole run stayed open until arm-ci finished (up to its 60-minute | ||
| # timeout) — and GitHub only exposes "Re-run failed jobs" once the entire run | ||
| # completes, forcing a fast test job that failed early to wait on arm-ci before | ||
| # it could be retried. As its own workflow, arm-ci no longer gates the main | ||
| # workflow's lifecycle, and it can be re-run independently. | ||
| # | ||
| # arm-ci is currently informational (continue-on-error), but it is wired to be | ||
| # promotable to a REQUIRED status check: it triggers on every PR and gates the | ||
| # expensive arm64 build behind the shared `changes` detector + `if:`. An | ||
| # irrelevant PR SKIPS the job (which branch protection reads as green) instead | ||
| # of never creating the check — a workflow-level `paths:` filter is deliberately | ||
| # avoided because a not-triggered required check stays pending forever and | ||
| # blocks the PR. This mirrors the `changes`+`if:` pattern build.yaml already | ||
| # uses for its required test jobs. | ||
| # | ||
| # Trigger parity with the former build.yaml job: | ||
| # - push to protected branches always runs (post-merge integration); | ||
| # - pull_request runs only when arm-relevant paths change (via `changes`+`if:`). | ||
| # endregion | ||
|
|
||
| name: ARM CI | ||
|
|
||
| on: | ||
| push: | ||
| branches: | ||
| - main | ||
| - develop | ||
| - 'release/**' | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
| branches: | ||
| - main | ||
| - develop | ||
| - 'release/**' | ||
| workflow_dispatch: | ||
|
|
||
| # Concurrency control to prevent parallel runs on the same PR | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
|
|
||
| env: | ||
| NGC_API_KEY: ${{ secrets.NGC_API_KEY }} | ||
|
|
||
| jobs: | ||
| # Shared change-detection gate. The trigger is unfiltered (see header) so the | ||
| # arm-ci check is always creatable; this decides whether the expensive jobs | ||
| # actually run. Lists arm-ci's real inputs — including the root files | ||
| # Dockerfile.base copies (pyproject.toml, environment.yml, isaaclab.*) that a | ||
| # path filter would otherwise miss. | ||
| changes: | ||
| name: Detect Changes | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| outputs: | ||
| should_run: ${{ steps.detect.outputs.should_run }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 1 | ||
| sparse-checkout: .github/actions/detect-changes | ||
| sparse-checkout-cone-mode: false | ||
| - id: detect | ||
| uses: ./.github/actions/detect-changes | ||
| with: | ||
| triggered-jobs-push: arm-ci | ||
| triggered-jobs-pr: arm-ci | ||
| patterns: | | ||
| ^source/ :: Library source code | ||
| ^docker/ :: Container build inputs | ||
| ^tools/ :: Build tooling | ||
| ^apps/ :: Standalone apps | ||
| ^scripts/ :: Standalone scripts | ||
| ^\.dockerignore$ :: Docker build context filter | ||
| ^pyproject\.toml$ :: Root Python project (OV pins + Docker copy) | ||
| ^environment\.yml$ :: Conda env (Docker copy) | ||
| ^isaaclab\. :: isaaclab.sh/.bat entrypoints (Docker copy) | ||
| ^\.github/workflows/arm-ci\.yml$ :: This workflow file | ||
| ^\.github/workflows/config\.yaml$ :: Base image config | ||
| ^\.github/actions/ :: CI actions | ||
|
|
||
| # Loads the Isaac Sim base image name/tag from config.yaml and computes the | ||
| # per-run CI image tag. Inlined here (rather than shared with build.yaml) | ||
| # because arm-ci builds its own independent -arm64 image and only needs three | ||
| # of the values build.yaml's config job produces. | ||
| config: | ||
| name: Load Config | ||
| needs: [changes] | ||
| if: needs.changes.outputs.should_run == 'true' | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| isaacsim_image_name: ${{ steps.load.outputs.isaacsim_image_name }} | ||
| isaacsim_image_tag: ${{ steps.load.outputs.isaacsim_image_tag }} | ||
| ci_image_tag: ${{ steps.image_tag.outputs.ci_image_tag }} | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 1 | ||
| sparse-checkout: .github/workflows/config.yaml | ||
| sparse-checkout-cone-mode: false | ||
| - id: load | ||
| run: | | ||
| set -euo pipefail | ||
| f=.github/workflows/config.yaml | ||
| echo "isaacsim_image_name=$(yq -r .isaacsim_image_name "$f")" >> "$GITHUB_OUTPUT" | ||
| echo "isaacsim_image_tag=$(yq -r .isaacsim_image_tag "$f")" >> "$GITHUB_OUTPUT" | ||
| # NOTE: this ci_image_tag formula is intentionally duplicated in build.yaml's | ||
| # config job — the two images are independent (-arm64 here vs x86 there), so | ||
| # they are not shared. Keep the two formulas in sync if either changes. | ||
| - id: image_tag | ||
| shell: bash | ||
| env: | ||
| EVENT_NAME: ${{ github.event_name }} | ||
| PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| REF_NAME: ${{ github.ref_name }} | ||
| SHA: ${{ github.sha }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [ "$EVENT_NAME" = "pull_request" ]; then | ||
| ref_component="pr-${PR_NUMBER}" | ||
| else | ||
| ref_component="$REF_NAME" | ||
| fi | ||
|
|
||
| # Sanitize the ref name for use as a Docker tag suffix. | ||
| sanitized_ref=$(echo "$ref_component" | sed 's/[^a-zA-Z0-9._-]/-/g') | ||
| echo "ci_image_tag=isaac-lab-ci:${sanitized_ref}-${SHA}" >> "$GITHUB_OUTPUT" | ||
| echo "CI image tag: isaac-lab-ci:${sanitized_ref}-${SHA}" | ||
|
Comment on lines
+126
to
+145
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is valid but I would say non-blocking since this is already commented here and is a nit |
||
|
|
||
| # aarch64 build + marker-gated tests on NVIDIA DGX Spark self-hosted runners. | ||
| # Build and test must share one runner because ECR is not wired for arm64 — | ||
| # the locally-built image cannot be handed off across machines. | ||
| arm-ci: | ||
| name: Build & Test | ||
| runs-on: [self-hosted, arm64] | ||
| needs: [changes, config] | ||
| if: needs.changes.outputs.should_run == 'true' | ||
| timeout-minutes: 60 | ||
| continue-on-error: true | ||
| steps: | ||
| - uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 1 | ||
| lfs: true | ||
|
|
||
| - name: Build base image (linux/arm64) | ||
| uses: ./.github/actions/docker-build | ||
| with: | ||
| image-tag: ${{ needs.config.outputs.ci_image_tag }}-arm64 | ||
| isaacsim-base-image: ${{ needs.config.outputs.isaacsim_image_name }} | ||
| isaacsim-version: ${{ needs.config.outputs.isaacsim_image_tag }} | ||
| dockerfile-path: docker/Dockerfile.base | ||
| platform: linux/arm64 | ||
| # The Spark runner is long-lived self-hosted with no ECR, so its local | ||
| # deps-cache tags accumulate; evict ones older than 14 days each run. | ||
| evict-stale-cache: "true" | ||
|
|
||
| - name: Resolve OV runtime pins from pyproject | ||
| uses: ./.github/actions/resolve-ov-pins | ||
| id: ov_pins | ||
|
|
||
| - name: Run arm_ci marker tests | ||
| uses: ./.github/actions/run-tests | ||
| with: | ||
| test-path: tools | ||
| result-file: arm-ci-report.xml | ||
| container-name: isaac-lab-arm-ci-${{ github.run_id }}-${{ github.run_attempt }} | ||
| image-tag: ${{ needs.config.outputs.ci_image_tag }}-arm64 | ||
| # ovphysx stays pinned to 0.4.13 here (see the aarch64 note below). | ||
| extra-pip-packages: "${{ steps.ov_pins.outputs.ovrtx }} ovphysx==0.4.13" | ||
| # ovphysx-backed test params require ovphysx >= 0.5.1, which has no aarch64 | ||
| # wheel yet; keep the newton/ovrtx rendering coverage, mirroring the public | ||
| # pip-index fallback of rendering-correctness-kitless. Running them anyway | ||
| # exhausts ovrtx SyncScopeIds (>15 renderer creations in one process). | ||
| test-k-expr: not ovphysx | ||
| ci-marker: arm_ci | ||
| volume-mount-source: ${{ github.workspace }} | ||
|
|
||
| - name: Run shared Cartpole smoke | ||
| uses: ./.github/actions/run-tests | ||
| with: | ||
| test-path: source/isaaclab/test/install_ci/misc/cartpole_training_smoke.py | ||
| result-file: arm-ci-cartpole-smoke-report.xml | ||
| container-name: isaac-lab-arm-ci-cartpole-${{ github.run_id }}-${{ github.run_attempt }} | ||
| image-tag: ${{ needs.config.outputs.ci_image_tag }}-arm64 | ||
| volume-mount-source: ${{ github.workspace }} | ||
|
|
||
| - name: Upload test reports | ||
| if: always() | ||
| uses: actions/upload-artifact@v7 | ||
| with: | ||
| name: arm-ci-reports | ||
| path: reports/ | ||
| retention-days: 7 | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (non-blocking): Both callers name the job
Detect Changes, should we add aDetect Changes (ARM)andDetect Changes (Docker)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The info is available at workflow level, e.g.
ARM CI / Detect Changes / Detect Changes (pull_request).