diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 0000000..8ad0d0b --- /dev/null +++ b/.github/workflows/build-image.yml @@ -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" ] + 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 + +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" + + 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 + + - 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 + + - 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 }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af77172..f98ddba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ concurrency: permissions: contents: read + packages: read jobs: @@ -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 + + - name: Pull prebuilt image (fast path) + run: ./scripts/ci/pull_or_build_image.sh + - name: Run E2E tests run: ./scripts/ci/run_tests.sh diff --git a/README.md b/README.md index 027a54f..4b69d5d 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/docker/run_docker.sh b/docker/run_docker.sh index da462c5..68e33dd 100755 --- a/docker/run_docker.sh +++ b/docker/run_docker.sh @@ -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") @@ -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 "" @@ -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 diff --git a/scripts/ci/image_tag.sh b/scripts/ci/image_tag.sh new file mode 100755 index 0000000..5a17ab8 --- /dev/null +++ b/scripts/ci/image_tag.sh @@ -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}" diff --git a/scripts/ci/pull_or_build_image.sh b/scripts/ci/pull_or_build_image.sh new file mode 100755 index 0000000..db33f52 --- /dev/null +++ b/scripts/ci/pull_or_build_image.sh @@ -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