Skip to content
Draft
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
92 changes: 92 additions & 0 deletions .github/workflows/build-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Copyright (c) 2026, The Isaac AutoData Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0

name: Build Isaac AutoData image

# Publishes the prebuilt GPU test image to GHCR so premerge E2E can skip the
# ~30-minute from-scratch build. The tag is a content hash
# (scripts/ci/image_tag.sh), so it rebuilds only when an image input changes.
on:
workflow_dispatch:
push:
branches: [ "main" ]
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the reported YAML lint error.

Use [main] instead of [ "main" ]; if the truthy-value rule is enforced, quote the on key or adjust the yamllint configuration.

🧰 Tools
🪛 YAMLlint (1.37.1)

[warning] 12-12: truthy value should be one of [false, true]

(truthy)


[error] 15-15: too many spaces inside brackets

(brackets)


[error] 15-15: too many spaces inside brackets

(brackets)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-image.yml around lines 12 - 15, Update the workflow
trigger configuration under the top-level on key to use the compact [main]
branch list instead of [ "main" ]. If YAML lint still flags on as a truthy
value, quote that key or apply the repository’s established yamllint
configuration.

Source: Linters/SAST tools

paths:
- "docker/**"
- ".gitmodules"
- "submodules/IsaacLab-Arena"
- "setup.py"
- "pyproject.toml"
- "scripts/ci/image_tag.sh"
- ".github/workflows/build-image.yml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read
packages: write
Comment on lines +28 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scope GHCR write access to the publishing job.

Move contents: read and packages: write under jobs.build_and_push.permissions. Keeping package write at workflow scope unnecessarily grants it to any future job.

🔐 Proposed fix
-permissions:
-  contents: read
-  packages: write
-
 jobs:
   build_and_push:
+    permissions:
+      contents: read
+      packages: write
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
permissions:
contents: read
packages: write
jobs:
build_and_push:
permissions:
contents: read
packages: write
🧰 Tools
🪛 zizmor (1.26.1)

[error] 31-31: overly broad permissions (excessive-permissions): packages: write is overly broad at the workflow level

(excessive-permissions)


[warning] 31-31: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-image.yml around lines 29 - 31, Move the contents
and packages permission declarations from workflow scope into the build_and_push
job’s permissions block, preserving contents: read and packages: write while
limiting GHCR write access to that publishing job.

Source: Linters/SAST tools


jobs:
build_and_push:
name: Build & push prebuilt image (GHCR)
runs-on: [self-hosted, gpu]
timeout-minutes: 120

# cuRobo arch baked into the image. Keep in sync with IMAGE_CUDA_ARCH in
# scripts/ci/image_tag.sh so the published tag matches what consumers pull.
env:
TORCH_CUDA_ARCH_LIST: "8.9+PTX"

Comment on lines +38 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Enforce one CUDA-architecture input across tagging and building.

image_tag.sh hashes IMAGE_CUDA_ARCH, while this workflow independently sets TORCH_CUDA_ARCH_LIST. If they drift, an image can be published under a tag computed for a different architecture. Set IMAGE_CUDA_ARCH explicitly in both publishing and E2E jobs and validate that both values match.

Proposed guard
     env:
+      IMAGE_CUDA_ARCH: "8.9+PTX"
       TORCH_CUDA_ARCH_LIST: "8.9+PTX"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-image.yml around lines 38 - 42, Use a single CUDA
architecture value for both image tagging and building: define IMAGE_CUDA_ARCH
explicitly in the publishing and E2E jobs, derive or align TORCH_CUDA_ARCH_LIST
with it, and add validation that the two values match before running either
workflow.

steps:
- name: nvidia-smi
run: nvidia-smi

- name: Clean up submodules directory
run: |
rm -f .git/modules/submodules/IsaacLab-Arena/index.lock || true
rm -rf submodules/* || true

- name: Mark repo as safe for git
run: git config --global --add safe.directory "$PWD"

- name: Checkout (recursive submodules)
uses: actions/checkout@v5
with:
submodules: recursive

- name: Pull LFS files
run: |
git lfs install --local
git lfs pull

- name: Resolve image tag
id: tag
run: echo "ref=$(./scripts/ci/image_tag.sh)" >> "$GITHUB_OUTPUT"

- name: Log in to GHCR
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: echo "$GHCR_TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
Comment on lines +69 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Log out of GHCR on every job exit.

This self-hosted runner is reusable, and docker login leaves credentials in Docker’s configuration. Add an if: always() cleanup step after the push.

Proposed cleanup
      - name: Log out of GHCR
        if: always()
        run: docker logout ghcr.io || true
🧰 Tools
🪛 zizmor (1.26.1)

[error] 72-72: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-image.yml around lines 69 - 72, Add an “Log out of
GHCR” step immediately after the image push flow, configured with if: always(),
that runs docker logout ghcr.io and tolerates logout failure so credentials are
removed on every job exit.


- name: Check whether this image is already published
id: exists
run: |
if docker manifest inspect "${{ steps.tag.outputs.ref }}" >/dev/null 2>&1; then
echo "build=false" >> "$GITHUB_OUTPUT"
echo "Image ${{ steps.tag.outputs.ref }} already published; nothing to do."
else
echo "build=true" >> "$GITHUB_OUTPUT"
fi

- name: Build image
if: steps.exists.outputs.build == 'true'
run: ./docker/run_docker.sh -c -b
Comment on lines +84 to +86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Force a fresh build before publishing.

Because this runs on a persistent self-hosted runner, isaac_autodata:curobo may already exist locally. Without -r, the workflow can reuse that stale image and publish it under the new content-hash tag.

Proposed fix
-          run: ./docker/run_docker.sh -c -b
+          run: ./docker/run_docker.sh -c -r -b
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Build image
if: steps.exists.outputs.build == 'true'
run: ./docker/run_docker.sh -c -b
- name: Build image
if: steps.exists.outputs.build == 'true'
run: ./docker/run_docker.sh -c -r -b
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-image.yml around lines 84 - 86, Update the “Build
image” workflow step to pass the force-rebuild option to docker/run_docker.sh,
ensuring any existing isaac_autodata:curobo image is rebuilt before publishing
while preserving the current conditional execution.


- name: Tag & push to GHCR
if: steps.exists.outputs.build == 'true'
run: |
docker tag isaac_autodata:curobo "${{ steps.tag.outputs.ref }}"
docker push "${{ steps.tag.outputs.ref }}"
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ concurrency:

permissions:
contents: read
packages: read

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Scope packages: read to test_e2e.

This permission is currently workflow-wide, so unrelated jobs such as pre_commit receive package-read authority. Move it to the E2E job with an explanatory comment; only that job pulls from GHCR.

Proposed permission scope
 permissions:
   contents: read
-  packages: read

 jobs:
   test_e2e:
+    permissions:
+      contents: read
+      # Required to pull the prebuilt image from GHCR.
+      packages: read
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
packages: read
permissions:
contents: read
jobs:
test_e2e:
permissions:
contents: read
# Required to pull the prebuilt image from GHCR.
packages: read
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 20-20: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment

(undocumented-permissions)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 20, Move the packages: read permission from
the workflow-wide permissions block into the test_e2e job, adding an explanatory
comment that only this job pulls from GHCR. Ensure unrelated jobs such as
pre_commit no longer receive package-read authority.

Source: Linters/SAST tools


jobs:

Expand Down Expand Up @@ -97,5 +98,15 @@ jobs:
git lfs install --local
git lfs pull

# Fast path: reuse the prebuilt GHCR image when its content hash matches.
# On a miss, run_docker.sh builds locally, so this never gates correctness.
- name: Log in to GHCR
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: echo "$GHCR_TOKEN" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
Comment on lines +103 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Log out of GHCR after the E2E job.

The self-hosted runner is reused, and docker login leaves credentials in Docker’s configuration. Add an if: always() cleanup step after the tests.

Proposed cleanup
      - name: Log out of GHCR
        if: always()
        run: docker logout ghcr.io || true
🧰 Tools
🪛 zizmor (1.26.1)

[error] 106-106: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 103 - 106, Add a GHCR logout cleanup
step after the E2E tests and login flow, using the workflow’s cleanup step with
if: always() and ensuring docker logout ghcr.io does not fail the job.


- name: Pull prebuilt image (fast path)
run: ./scripts/ci/pull_or_build_image.sh
Comment on lines +108 to +109

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make GHCR authentication best-effort.

A failed docker login exits the job before pull_or_build_image.sh can fall back to a local build. Make authentication non-blocking, or move it into the script’s best-effort path, and ensure login or pull failures invoke the local build.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 108 - 109, Update the workflow step
invoking pull_or_build_image.sh so GHCR authentication and image pulling are
best-effort: prevent docker login failures from terminating the job, and ensure
any login or pull failure reaches the script’s local-build fallback rather than
exiting the workflow.


- name: Run E2E tests
run: ./scripts/ci/run_tests.sh
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,19 @@ more space-separated test paths; defaults to the whole tree), `PYTEST_MARK`
(marker filter; empty runs everything), and `FORCE_REBUILD=true` (image rebuild,
used by the nightly).

Test datasets are pulled from Git LFS in CI; no external credentials are
required. The jobs run on `[self-hosted, gpu]` runners.

### Prebuilt image

Building the image from scratch (isaac-sim base + Lab/Arena install + cuRobo
compile) takes ~30 minutes. To skip that, `.github/workflows/build-image.yml`
publishes the image to GHCR (`ghcr.io/isaac-sim/isaac-autodata`) and the test
job pulls it instead of building. The image is tagged by a content hash over its
inputs (`docker/`, the pinned `IsaacLab-Arena` submodule, packaging metadata, and
the CUDA arch — see `scripts/ci/image_tag.sh`), so it rebuilds only when one of
those changes. On a cache miss the test job falls back to a local build, so the
prebuilt image is purely an accelerator, never a correctness dependency.

The image bakes cuRobo for the runner GPU arch (`8.9+PTX`, L40S); building for a
different GPU generation means bumping that value in `build-image.yml` and
`image_tag.sh`.
11 changes: 10 additions & 1 deletion docker/run_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,15 @@ DATASETS_HOST_MOUNT_DIRECTORY="$HOME/datasets"

FORCE_REBUILD=false
NO_CACHE=""
BUILD_ONLY=false

while getopts ":d:crRvh" OPTION; do
while getopts ":d:crRbvh" OPTION; do
case $OPTION in
d) DATASETS_HOST_MOUNT_DIRECTORY=$OPTARG ;;
c) INSTALL_CUROBO=true ;;
r) FORCE_REBUILD=true ;;
R) FORCE_REBUILD=true; NO_CACHE="--no-cache" ;;
b) BUILD_ONLY=true ;;
v) set -x ;;
h)
script_name=$(basename "$0")
Expand All @@ -67,6 +69,7 @@ while getopts ":d:crRvh" OPTION; do
echo " -c Install cuRobo, auto-detects the GPU arch (override with the TORCH_CUDA_ARCH_LIST env var)."
echo " -r Force rebuilding the image."
echo " -R Force rebuilding the image without cache."
echo " -b Build the image only, then exit (no container run). Used by CI image publishing."
echo " -v Verbose (set -x)."
echo " -h Show this help."
echo ""
Expand Down Expand Up @@ -108,6 +111,12 @@ else
"${REPO_ROOT}"
fi

# Build-only mode (CI image publishing): stop before creating/running a container.
if [ "$BUILD_ONLY" = "true" ]; then
echo "Build-only mode: image ${DOCKER_IMAGE_NAME}:${DOCKER_VERSION_TAG} is ready."
exit 0
fi

# Remove a previously-exited container of the same name so we can recreate it.
if [ "$(docker ps -a --quiet --filter status=exited --filter "name=^${CONTAINER_NAME}$")" ]; then
docker rm "${CONTAINER_NAME}" >/dev/null
Expand Down
31 changes: 31 additions & 0 deletions scripts/ci/image_tag.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
# Copyright (c) 2026, The Isaac AutoData Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# Print the GHCR reference for the prebuilt Isaac AutoData test image.
#
# The tag is a content hash of the inputs that determine the image: the docker/
# tree, the pinned IsaacLab-Arena submodule (which transitively pins IsaacLab),
# packaging metadata, and the CUDA arch. The build workflow and the E2E pull step
# both call this, so an unchanged PR resolves to the tag main published (a cache
# hit). Ids come from the git tree, so no submodule checkout is needed.

set -euo pipefail

# GHCR repo for the prebuilt image (must be lowercase).
IMAGE_REPO="${IMAGE_REPO:-ghcr.io/isaac-sim/isaac-autodata}"
# cuRobo is compiled for this arch and baked into the image (L40S = 8.9).
# Keep in sync with TORCH_CUDA_ARCH_LIST in .github/workflows/build-image.yml.
IMAGE_CUDA_ARCH="${IMAGE_CUDA_ARCH:-8.9+PTX}"

inputs=$(git rev-parse \
"HEAD:docker" \
"HEAD:.gitmodules" \
"HEAD:submodules/IsaacLab-Arena" \
"HEAD:setup.py" \
"HEAD:pyproject.toml")

hash=$(printf '%s\n%s\n' "${inputs}" "${IMAGE_CUDA_ARCH}" | sha256sum | cut -c1-16)
echo "${IMAGE_REPO}:curobo-${hash}"
24 changes: 24 additions & 0 deletions scripts/ci/pull_or_build_image.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
# Copyright (c) 2026, The Isaac AutoData Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: Apache-2.0
#
# Pull the prebuilt E2E image from GHCR (keyed by content hash) and tag it
# isaac_autodata:curobo so run_docker.sh reuses it. On any miss, do nothing and
# let run_docker.sh build locally, so correctness never depends on the cache.

set -euo pipefail

SCRIPT_DIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)
LOCAL_TAG="isaac_autodata:curobo"

REMOTE=$("${SCRIPT_DIR}/image_tag.sh")

echo ">>> Prebuilt image: ${REMOTE}"
if docker pull "${REMOTE}"; then
docker tag "${REMOTE}" "${LOCAL_TAG}"
echo ">>> Tagged as ${LOCAL_TAG}; run_docker.sh will reuse it (no build)."
else
echo ">>> Prebuilt image unavailable (cache miss); run_docker.sh will build locally."
fi
Comment on lines +19 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Invalidate stale local images on pull failure.

If docker pull fails, an older isaac_autodata:curobo tag remains available. run_docker.sh can then reuse that stale image instead of taking the documented local-build fallback.

🐛 Proposed fix
 else
+    docker image rm -f "${LOCAL_TAG}" >/dev/null 2>&1 || true
     echo ">>> Prebuilt image unavailable (cache miss); run_docker.sh will build locally."
 fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if docker pull "${REMOTE}"; then
docker tag "${REMOTE}" "${LOCAL_TAG}"
echo ">>> Tagged as ${LOCAL_TAG}; run_docker.sh will reuse it (no build)."
else
echo ">>> Prebuilt image unavailable (cache miss); run_docker.sh will build locally."
fi
if docker pull "${REMOTE}"; then
docker tag "${REMOTE}" "${LOCAL_TAG}"
echo ">>> Tagged as ${LOCAL_TAG}; run_docker.sh will reuse it (no build)."
else
docker image rm -f "${LOCAL_TAG}" >/dev/null 2>&1 || true
echo ">>> Prebuilt image unavailable (cache miss); run_docker.sh will build locally."
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/pull_or_build_image.sh` around lines 22 - 27, Update the docker
pull failure branch in the image handling flow to remove the existing local tag
before announcing the local-build fallback. Use the same LOCAL_TAG referenced by
the successful docker tag and ensure cleanup does not prevent run_docker.sh from
proceeding to build locally.

Loading