From 108a5f9c324f614c3b46f7bcbaa3fa8c8dd1310e Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 3 Sep 2026 10:28:32 -0700 Subject: [PATCH 1/5] Skip the image invariants on a deps-cache hit and link _isaac_sim only in Kit images The invariants step from #7405 inspects the commit-tagged image locally, but on a deps-cache hit ecr-build-push-pull only aliases that tag in ECR, so every first run of a new commit on a dependency-stable branch failed before any test job started. The action now reports whether it built the image and the step runs only then; a cache hit reuses an image that passed the check when it was built. run_tests.sh mounts runtime directories under /isaac-sim into every container, so the kit-less image has that directory too, and the unconditional _isaac_sim link made isaaclab.sh (#7466) treat it as a downloaded Isaac Sim next to the image's VIRTUAL_ENV. Link only where Kit's python.sh exists. --- .github/actions/ecr-build-push-pull/README.md | 6 ++++++ .github/actions/ecr-build-push-pull/action.yml | 9 +++++++++ .github/actions/run-tests/run_tests.sh | 7 +++++-- .github/workflows/build.yaml | 4 ++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/actions/ecr-build-push-pull/README.md b/.github/actions/ecr-build-push-pull/README.md index 2e306c978b8e..1febc3a3e91c 100644 --- a/.github/actions/ecr-build-push-pull/README.md +++ b/.github/actions/ecr-build-push-pull/README.md @@ -16,6 +16,12 @@ ECR is also used as the BuildKit layer cache. ecr-url: (optional, complete url for ECR storage) ``` +## Outputs + +- `built`: `'true'` when this run built the image, so it exists locally under `image-tag`. + An exact-tag hit pulls it as well; a deps-cache hit only creates the ECR tag unless + `pull-on-deps-hit` is set. Gate steps that need the image locally on this output. + ## ECR URL resolution order 1. `ecr-url` input diff --git a/.github/actions/ecr-build-push-pull/action.yml b/.github/actions/ecr-build-push-pull/action.yml index b289b4ebb7d8..4c82e48a04c6 100644 --- a/.github/actions/ecr-build-push-pull/action.yml +++ b/.github/actions/ecr-build-push-pull/action.yml @@ -50,6 +50,15 @@ inputs: that only need the tag pushed. required: false default: 'false' +outputs: + built: + description: > + 'true' when this run built the image, so it exists locally under image-tag. + An exact-tag hit pulls it as well; a deps-cache hit only creates the ECR tag + unless pull-on-deps-hit is set, so steps that need the image locally should + check this output. + value: ${{ steps.pull-exact.outputs.hit != 'true' && steps.deps-cache.outputs.deps-cache-hit != 'true' }} + runs: using: composite steps: diff --git a/.github/actions/run-tests/run_tests.sh b/.github/actions/run-tests/run_tests.sh index d831101c9239..07d93c98d7eb 100755 --- a/.github/actions/run-tests/run_tests.sh +++ b/.github/actions/run-tests/run_tests.sh @@ -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 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 554f9989ed00..a94383c4a72e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -208,6 +208,7 @@ jobs: enable-cache: true - name: Build and push to ECR + id: image uses: ./.github/actions/ecr-build-push-pull with: image-tag: ${{ needs.config.outputs.ci_image_tag }} @@ -219,7 +220,10 @@ jobs: # #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. + # Only a freshly built image is on this runner; a deps-cache hit reuses one that + # passed this check when it was built. - name: Verify image invariants + if: steps.image.outputs.built == 'true' shell: bash env: IMAGE_TAG: ${{ needs.config.outputs.ci_image_tag }} From d2fbce5379a74e21e88bd9185be962c56b12d616 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 3 Sep 2026 10:38:34 -0700 Subject: [PATCH 2/5] Pin that the run-tests script links _isaac_sim only where Kit is installed --- docker/test/test_container_profiles.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docker/test/test_container_profiles.py b/docker/test/test_container_profiles.py index 39d39c974829..8500ba859ce6 100644 --- a/docker/test/test_container_profiles.py +++ b/docker/test/test_container_profiles.py @@ -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 @@ -288,6 +289,20 @@ def test_kitless_compose_service_has_no_isaac_sim_mounts(): assert all("/kit/" not in mount["target"].lower() for mount in mounts) +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") From d9ec1cf79a64bc7b3b5b1d324ce4975d85986339 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 3 Sep 2026 13:42:07 -0700 Subject: [PATCH 3/5] Verify a freshly built image before it is published The invariants step ran after ecr-build-push-pull returned, but the action pushes the commit tag and the deps tag inside itself, so an image that fails the check is already in the shared cache. That happened four seconds apart on newton-world-prefixes (run 33735349483): the deps tag deps-6aebe721c56a91f7 was pushed at 09:05:07 and the invariant failed at 09:05:11, leaving an image whose Isaac Sim extensions cannot load. Any later run whose install inputs hash the same then gets a cache hit and inherits it, so gating the check on whether this run built the image would let the second push of the change that broke it go green. Move the assertion into the action, before every push, behind a verify-command input the base image job passes. A failure now publishes nothing, so the next run rebuilds and fails again. The built output added for the old gate is gone with it. --- .github/actions/ecr-build-push-pull/README.md | 14 +++++-- .../actions/ecr-build-push-pull/action.yml | 37 ++++++++++++++----- .github/workflows/build.yaml | 22 ++--------- docker/test/test_container_profiles.py | 17 +++++++++ 4 files changed, 59 insertions(+), 31 deletions(-) diff --git a/.github/actions/ecr-build-push-pull/README.md b/.github/actions/ecr-build-push-pull/README.md index 1febc3a3e91c..48a2df398d78 100644 --- a/.github/actions/ecr-build-push-pull/README.md +++ b/.github/actions/ecr-build-push-pull/README.md @@ -16,11 +16,17 @@ ECR is also used as the BuildKit layer cache. ecr-url: (optional, complete url for ECR storage) ``` -## Outputs +## Verifying a freshly built image -- `built`: `'true'` when this run built the image, so it exists locally under `image-tag`. - An exact-tag hit pulls it as well; a deps-cache hit only creates the ECR tag unless - `pull-on-deps-hit` is set. Gate steps that need the image locally on this output. +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 diff --git a/.github/actions/ecr-build-push-pull/action.yml b/.github/actions/ecr-build-push-pull/action.yml index 4c82e48a04c6..35930997ff84 100644 --- a/.github/actions/ecr-build-push-pull/action.yml +++ b/.github/actions/ecr-build-push-pull/action.yml @@ -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 @@ -50,15 +60,6 @@ inputs: that only need the tag pushed. required: false default: 'false' -outputs: - built: - description: > - 'true' when this run built the image, so it exists locally under image-tag. - An exact-tag hit pulls it as well; a deps-cache hit only creates the ECR tag - unless pull-on-deps-hit is set, so steps that need the image locally should - check this output. - value: ${{ steps.pull-exact.outputs.hit != 'true' && steps.deps-cache.outputs.deps-cache-hit != 'true' }} - runs: using: composite steps: @@ -251,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' + 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' && diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a94383c4a72e..94872194797a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -207,8 +207,10 @@ 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 - id: image uses: ./.github/actions/ecr-build-push-pull with: image-tag: ${{ needs.config.outputs.ci_image_tag }} @@ -216,23 +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. - # Only a freshly built image is on this runner; a deps-cache hit reuses one that - # passed this check when it was built. - - name: Verify image invariants - if: steps.image.outputs.built == 'true' - 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 diff --git a/docker/test/test_container_profiles.py b/docker/test/test_container_profiles.py index 8500ba859ce6..89d83bcfcc7a 100644 --- a/docker/test/test_container_profiles.py +++ b/docker/test/test_container_profiles.py @@ -289,6 +289,23 @@ 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. From 9fa54fde468f13342c8caf40e10fa103ef5a0968 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 3 Sep 2026 14:05:21 -0700 Subject: [PATCH 4/5] Take a test path for image verification, matching the sibling actions run-tests and run-package-tests take a test-path and own the pytest invocation; passing a whole shell command was a vocabulary of its own and put the runner's uv incantation in the caller. --- .github/actions/ecr-build-push-pull/README.md | 11 ++++++----- .../actions/ecr-build-push-pull/action.yml | 19 ++++++++++--------- .github/workflows/build.yaml | 2 +- docker/test/test_container_profiles.py | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/actions/ecr-build-push-pull/README.md b/.github/actions/ecr-build-push-pull/README.md index 48a2df398d78..493cc471879b 100644 --- a/.github/actions/ecr-build-push-pull/README.md +++ b/.github/actions/ecr-build-push-pull/README.md @@ -18,15 +18,16 @@ ECR is also used as the BuildKit layer cache. ## Verifying a freshly built image -Pass `verify-command` to assert against the image before it is published: +Pass `verify-test-path` 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 + verify-test-path: 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. +The tests run only on a full build, with `IMAGE_TAG` and `IMAGE_DIGEST` set, so the caller's job +needs `uv` (`astral-sh/setup-uv`). 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 +them: that image passed when it was built. ## ECR URL resolution order diff --git a/.github/actions/ecr-build-push-pull/action.yml b/.github/actions/ecr-build-push-pull/action.yml index 35930997ff84..ee7a608eee58 100644 --- a/.github/actions/ecr-build-push-pull/action.yml +++ b/.github/actions/ecr-build-push-pull/action.yml @@ -37,14 +37,14 @@ inputs: description: Tag used for the ECR layer cache image (e.g. "cache-base", "cache-curobo"). required: false default: 'cache' - verify-command: + verify-test-path: 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. + Path to tests asserted against a freshly built image, before it is tagged or pushed. They + run with IMAGE_TAG and IMAGE_DIGEST set; a failure 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. + Not run on an exact-tag or deps-cache hit: those serve an image that already passed when it + was built. required: false default: '' pull-on-deps-hit: @@ -257,18 +257,19 @@ runs: # rebuilding it, so anything published unverified stays unverified. - name: Verify freshly built image if: > - inputs.verify-command != '' && + inputs.verify-test-path != '' && steps.pull-exact.outputs.hit != 'true' && steps.deps-cache.outputs.deps-cache-hit != 'true' shell: bash env: IMAGE_TAG: ${{ inputs.image-tag }} - VERIFY_COMMAND: ${{ inputs.verify-command }} + TEST_PATH: ${{ inputs.verify-test-path }} run: | set -euo pipefail IMAGE_DIGEST="$(docker image inspect --format '{{.Id}}' "${IMAGE_TAG}")" export IMAGE_DIGEST - bash -c "${VERIFY_COMMAND}" + uv run --no-project --with pytest \ + python -m pytest -q "${TEST_PATH}" - name: Tag built image with ECR-prefixed name if: > diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 94872194797a..b1f33e545325 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -218,7 +218,7 @@ jobs: isaacsim-version: ${{ needs.config.outputs.isaacsim_image_tag }} dockerfile-path: docker/Dockerfile.base cache-tag: cache-base - verify-command: uv run --no-project --with pytest python -m pytest -q docker/test/test_image_invariants.py + verify-test-path: docker/test/test_image_invariants.py build-curobo: name: Build cuRobo Docker Image diff --git a/docker/test/test_container_profiles.py b/docker/test/test_container_profiles.py index 89d83bcfcc7a..3023744a44db 100644 --- a/docker/test/test_container_profiles.py +++ b/docker/test/test_container_profiles.py @@ -303,7 +303,7 @@ def test_image_is_verified_before_it_is_published(): 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" + assert "verify-test-path: docker/test/test_image_invariants.py" in build, "the base job must ask for them" def test_run_tests_links_isaac_sim_only_where_kit_is_installed(): From 96df455810ed48f3cf9941376396ea330b43b030 Mon Sep 17 00:00:00 2001 From: jichuanh Date: Thu, 3 Sep 2026 15:11:48 -0700 Subject: [PATCH 5/5] Drop the digest binding from image verification The digest was read from the tag and compared with the same tag a second later, so it could not detect a stale tag. The verify step is now the same line the dockerfile-contract and kit-less validation jobs run, with only IMAGE_TAG set. The wiring test parses build.yaml instead of matching a substring a comment could satisfy. --- .github/actions/ecr-build-push-pull/README.md | 4 ++-- .github/actions/ecr-build-push-pull/action.yml | 8 +++----- docker/test/test_container_profiles.py | 8 ++++++-- docker/test/test_image_invariants.py | 14 -------------- 4 files changed, 11 insertions(+), 23 deletions(-) diff --git a/.github/actions/ecr-build-push-pull/README.md b/.github/actions/ecr-build-push-pull/README.md index 493cc471879b..2dea04797126 100644 --- a/.github/actions/ecr-build-push-pull/README.md +++ b/.github/actions/ecr-build-push-pull/README.md @@ -24,8 +24,8 @@ Pass `verify-test-path` to assert against the image before it is published: verify-test-path: docker/test/test_image_invariants.py ``` -The tests run only on a full build, with `IMAGE_TAG` and `IMAGE_DIGEST` set, so the caller's job -needs `uv` (`astral-sh/setup-uv`). A failure fails the action with nothing pushed, so the next run +The tests run only on a full build, with `IMAGE_TAG` set, so the caller's job needs `uv` +(`astral-sh/setup-uv`). 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 them: that image passed when it was built. diff --git a/.github/actions/ecr-build-push-pull/action.yml b/.github/actions/ecr-build-push-pull/action.yml index ee7a608eee58..7c595e7d3407 100644 --- a/.github/actions/ecr-build-push-pull/action.yml +++ b/.github/actions/ecr-build-push-pull/action.yml @@ -39,9 +39,9 @@ inputs: default: 'cache' verify-test-path: description: > - Path to tests asserted against a freshly built image, before it is tagged or pushed. They - run with IMAGE_TAG and IMAGE_DIGEST set; a failure fails the action with nothing published, - so the next run rebuilds instead of inheriting the bad image from the cache. + Path to a test file or directory asserted against a freshly built image, before it is + tagged or pushed. Tests run with IMAGE_TAG set; a failure 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 when it was built. @@ -266,8 +266,6 @@ runs: TEST_PATH: ${{ inputs.verify-test-path }} 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 "${TEST_PATH}" diff --git a/docker/test/test_container_profiles.py b/docker/test/test_container_profiles.py index 3023744a44db..9a17fc87aab7 100644 --- a/docker/test/test_container_profiles.py +++ b/docker/test/test_container_profiles.py @@ -302,8 +302,12 @@ def test_image_is_verified_before_it_is_published(): 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-test-path: docker/test/test_image_invariants.py" in build, "the base job must ask for them" + build = yaml.safe_load((REPO_ROOT / ".github" / "workflows" / "build.yaml").read_text(encoding="utf-8")) + (base_build,) = [ + step for step in build["jobs"]["build"]["steps"] if step.get("uses") == "./.github/actions/ecr-build-push-pull" + ] + + assert base_build["with"]["verify-test-path"] == "docker/test/test_image_invariants.py" def test_run_tests_links_isaac_sim_only_where_kit_is_installed(): diff --git a/docker/test/test_image_invariants.py b/docker/test/test_image_invariants.py index ffab8edf5781..74aea9920244 100644 --- a/docker/test/test_image_invariants.py +++ b/docker/test/test_image_invariants.py @@ -38,20 +38,6 @@ def _require_image(): pytest.skip("IMAGE_TAG is unset; no built image to assert against") -def test_image_under_test_is_the_one_that_was_built(): - """Fail loudly rather than assert against whatever a stale tag happens to point at.""" - expected = os.environ.get("IMAGE_DIGEST", "") - if not expected: - pytest.skip("IMAGE_DIGEST is unset; cannot bind the tag to a specific image") - actual = subprocess.run( - ["docker", "image", "inspect", "--format", "{{.Id}}", IMAGE_TAG], - capture_output=True, - text=True, - check=True, - ).stdout.strip() - assert actual == expected, f"{IMAGE_TAG} is {actual}, expected {expected}" - - def test_no_prebundled_package_lost_its_entry_point(): """A dangling ``__init__.py`` in a prebundle stops Isaac Sim extensions loading.