Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
12 changes: 12 additions & 0 deletions .github/actions/ecr-build-push-pull/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ ECR is also used as the BuildKit layer cache.
ecr-url: (optional, complete url for ECR storage)
```

## Verifying a freshly built image

Pass `verify-command` to assert against the image before it is published:

```yaml
verify-command: uv run --no-project --with pytest python -m pytest -q docker/test/test_image_invariants.py
```

It runs only on a full build, with `IMAGE_TAG` and `IMAGE_DIGEST` exported. A failure fails the
action with nothing pushed, so the next run rebuilds rather than serving the bad image from the
deps cache. Exact-tag and deps-cache hits skip it: that image passed the command when it was built.

## ECR URL resolution order

1. `ecr-url` input
Expand Down
28 changes: 28 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,16 @@ inputs:
description: Tag used for the ECR layer cache image (e.g. "cache-base", "cache-curobo").
required: false
default: 'cache'
verify-command:
description: >
Command run against a freshly built image, before it is tagged or pushed, with IMAGE_TAG
and IMAGE_DIGEST exported. A failing command fails the action with nothing published, so
the next run rebuilds instead of inheriting the bad image from the cache.

Not run on an exact-tag or deps-cache hit: those serve an image that already passed this
command when it was built.
required: false
default: ''
pull-on-deps-hit:
description: >
Pull the image locally after a deps-cache hit. Needed by jobs that run
Expand Down Expand Up @@ -242,6 +252,24 @@ runs:
cache-to: ${{ steps.resolve-ecr.outputs.available == 'true' && format('type=registry,ref={0},mode=max', env.CACHE_IMAGE) || '' }}
deps-hash: ${{ steps.deps-hash.outputs.hash }}

# Assert against the image while it is only local: the push steps below publish under both
# the commit tag and the deps tag, and a deps-cache hit later serves that image without
# rebuilding it, so anything published unverified stays unverified.
- name: Verify freshly built image
if: >
inputs.verify-command != '' &&
steps.pull-exact.outputs.hit != 'true' &&
steps.deps-cache.outputs.deps-cache-hit != 'true'
Comment thread
hujc7 marked this conversation as resolved.
shell: bash
env:
IMAGE_TAG: ${{ inputs.image-tag }}
VERIFY_COMMAND: ${{ inputs.verify-command }}
run: |
set -euo pipefail
IMAGE_DIGEST="$(docker image inspect --format '{{.Id}}' "${IMAGE_TAG}")"
export IMAGE_DIGEST
bash -c "${VERIFY_COMMAND}"

- name: Tag built image with ECR-prefixed name
if: >
steps.resolve-ecr.outputs.available == 'true' &&
Expand Down
7 changes: 5 additions & 2 deletions .github/actions/run-tests/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -320,8 +320,11 @@ run_tests() {
set -e
cd /workspace/isaaclab
mkdir -p tests
rm _isaac_sim || true
ln -s /isaac-sim _isaac_sim
# The runtime mounts above create /isaac-sim in every image. Link it only where Kit
# lives there: in the kit-less image the link would read as a downloaded Isaac Sim,
# which isaaclab.sh refuses to combine with the image's VIRTUAL_ENV.
rm -f _isaac_sim
if [ -x /isaac-sim/python.sh ]; then ln -s /isaac-sim _isaac_sim; fi
if [ -n \"\${WARP_CACHE_PATH:-}\" ]; then
./isaaclab.sh -p tools/verify_warp_cache.py
fi
Expand Down
18 changes: 4 additions & 14 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@ jobs:
with:
enable-cache: true

# #6329 aborts the pip install when it strands a prebundled package's __init__.py
# (nvbugs 6343978: 14 Isaac Sim extensions fail to load). The images install with
# ``uv sync``, which never runs that guard, so assert the same invariant on the image.
- name: Build and push to ECR
uses: ./.github/actions/ecr-build-push-pull
with:
Expand All @@ -215,20 +218,7 @@ jobs:
isaacsim-version: ${{ needs.config.outputs.isaacsim_image_tag }}
dockerfile-path: docker/Dockerfile.base
cache-tag: cache-base

# #6329 aborts the pip install when it strands a prebundled package's __init__.py
# (nvbugs 6343978: 14 Isaac Sim extensions fail to load). The images install with
# ``uv sync``, which never runs that guard, so assert the same invariant on the image.
- name: Verify image invariants
shell: bash
env:
IMAGE_TAG: ${{ needs.config.outputs.ci_image_tag }}
run: |
set -euo pipefail
IMAGE_DIGEST="$(docker image inspect --format '{{.Id}}' "${IMAGE_TAG}")"
export IMAGE_DIGEST
uv run --no-project --with pytest \
python -m pytest -q docker/test/test_image_invariants.py
verify-command: uv run --no-project --with pytest python -m pytest -q docker/test/test_image_invariants.py

build-curobo:
name: Build cuRobo Docker Image
Expand Down
32 changes: 32 additions & 0 deletions docker/test/test_container_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from docker.utils import ContainerInterface, volume_mounts

DOCKER_DIR = Path(__file__).resolve().parents[1]
REPO_ROOT = DOCKER_DIR.parent


@pytest.fixture
Expand Down Expand Up @@ -288,6 +289,37 @@ def test_kitless_compose_service_has_no_isaac_sim_mounts():
assert all("/kit/" not in mount["target"].lower() for mount in mounts)


def test_image_is_verified_before_it_is_published():
"""A published image must be a verified one.

The push steps publish under both the commit tag and the deps tag, and a later deps-cache hit
serves that image without rebuilding it, so anything published unverified stays unverified.
"""
action = yaml.safe_load(
(REPO_ROOT / ".github" / "actions" / "ecr-build-push-pull" / "action.yml").read_text(encoding="utf-8")
)
names = [step["name"] for step in action["runs"]["steps"] if "name" in step]

assert names.index("Verify freshly built image") < names.index("Push to ECR") < names.index("Push deps tag")

build = (REPO_ROOT / ".github" / "workflows" / "build.yaml").read_text(encoding="utf-8")
assert "verify-command: uv run" in build, "the base image job must hand its invariants to the action"


def test_run_tests_links_isaac_sim_only_where_kit_is_installed():
"""The kit-less image has no Kit under ``/isaac-sim``, which the runtime mounts create anyway.

Linking it as ``_isaac_sim`` there reads as a downloaded Isaac Sim, which ``isaaclab.sh``
refuses to combine with the image's ``VIRTUAL_ENV``.
"""
script = (REPO_ROOT / ".github" / "actions" / "run-tests" / "run_tests.sh").read_text(encoding="utf-8")

link_lines = [line.strip() for line in script.splitlines() if "ln -s /isaac-sim _isaac_sim" in line]

assert link_lines
assert all("/isaac-sim/python.sh" in line for line in link_lines), link_lines


def test_kitless_volume_key_resolves_owned_image_paths(monkeypatch: pytest.MonkeyPatch):
"""The explicit kit-less volume key resolves the paths prepared by its Dockerfile."""
monkeypatch.setenv("DOCKER_ISAACLAB_PATH", "/workspace/isaaclab")
Expand Down
Loading