Skip to content
Merged
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
12 changes: 10 additions & 2 deletions .github/CI.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,13 @@ temporary `vX.Y.Z` tag; a **GA** dispatch keeps the `vX.Y.Z` tag and is the one
K2 Kitmaker promotes to the release registry (Artifactory). The cutover to
public PyPI happens out of band — until then, installs use the TestPyPI index.

Release-candidate fixes follow the normal review path first: create a PR
against `main`, wait for it to merge, cherry-pick the merged commit onto
`release/X.Y.0`, and only then dispatch the next RC from the release branch.
Do not direct-push unreviewed fixes to `release/X.Y.0`, and do not submit an RC
workflow or Kitmaker release before the fix has merged to `main` and has been
picked onto the release branch.

### Steps

1. **Land everything on `main`** and confirm it is green.
Expand Down Expand Up @@ -158,8 +165,9 @@ public PyPI happens out of band — until then, installs use the TestPyPI index.
--extra-index-url https://pypi.org/simple/ "holoscan-cli==X.Y.Zrc1"
```

5. **Iterate** if fixes are needed: cherry-pick onto `release/X.Y.0` (or merge
from `main`), then dispatch with `-f rc=2`, `-f rc=3`, … (bump each time).
5. **Iterate** if fixes are needed: merge the fix to `main`, cherry-pick the
merged commit onto `release/X.Y.0`, then dispatch with `-f rc=2`,
`-f rc=3`, … (bump each time).
6. **Cut GA** once an RC is accepted:

```bash
Expand Down
1 change: 1 addition & 0 deletions .github/scripts/assert_wheel_contents.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ required=(
'holoscan_cli/py\.typed$'
'holoscan_cli/metadata/.+\.schema\.json$'
'holoscan_cli/setup_scripts/.+'
'holoscan_cli/setup_scripts/requirements\.template\.txt$'
'holoscan_cli/testing/'
)
for pattern in "${required[@]}"; do
Expand Down
16 changes: 8 additions & 8 deletions src/holoscan_cli/container/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -501,17 +501,17 @@ def build(
run_command(cmd, dry_run=self.dryrun)

if extra_scripts:
setup_scripts_dir = get_holohub_setup_scripts_dir()
for script in extra_scripts:
script_path = get_holohub_setup_scripts_dir() / f"{script}.sh"
script_path = setup_scripts_dir / f"{script}.sh"
if not script_path.exists():
fatal(f"Script {script}.sh not found in {get_holohub_setup_scripts_dir()}")
fatal(f"Script {script}.sh not found in {setup_scripts_dir}")
try:
relative_script_path = script_path.relative_to(HoloscanContainer.HOLOHUB_ROOT)
script_build_context = HoloscanContainer.HOLOHUB_ROOT
except ValueError:
fatal(
f"Script {script}.sh at {script_path} is not within {HoloscanContainer.HOLOHUB_ROOT}. "
f"The HOLOSCAN_CLI_SETUP_SCRIPTS_DIR environment variable must resolve to a subdirectory within the project scope."
)
relative_script_path = script_path.relative_to(setup_scripts_dir)
script_build_context = setup_scripts_dir
cmd = [
self.DOCKER_EXE,
"build",
Expand All @@ -523,8 +523,8 @@ def build(
"--build-arg",
f"SCRIPT={relative_script_path}",
"-f",
str(get_holohub_setup_scripts_dir() / "Dockerfile.util"),
str(HoloscanContainer.HOLOHUB_ROOT),
str(setup_scripts_dir / "Dockerfile.util"),
str(script_build_context),
]
for tag_name in tags:
# We override the default tag so we can add the next scripts on top of this.
Expand Down
6 changes: 3 additions & 3 deletions src/holoscan_cli/setup_scripts/Dockerfile.util
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ ARG BASE_IMAGE
FROM ${BASE_IMAGE} AS base

ARG SCRIPT
COPY ${SCRIPT} ${SCRIPT}
RUN chmod +x ${SCRIPT}
RUN ${SCRIPT}
COPY . /tmp/holoscan-cli-setup/
RUN chmod +x /tmp/holoscan-cli-setup/${SCRIPT}
RUN /tmp/holoscan-cli-setup/${SCRIPT}
3 changes: 3 additions & 0 deletions src/holoscan_cli/setup_scripts/requirements.template.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
cookiecutter>=2.7.1
jsonschema>=4.26.0
referencing>=0.36.2
2 changes: 1 addition & 1 deletion src/holoscan_cli/setup_scripts/template.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ set -e

# Install dependencies used by project templates and metadata validation
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REQUIREMENTS_FILE="${SCRIPT_DIR}/../requirements.template.txt"
REQUIREMENTS_FILE="${SCRIPT_DIR}/requirements.template.txt"

if [[ ! -f "${REQUIREMENTS_FILE}" ]]; then
echo "requirements.template.txt not found at ${REQUIREMENTS_FILE}" >&2
Expand Down
40 changes: 40 additions & 0 deletions tests/unit/test_container_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,46 @@ def test_build_dryrun_emits_base_and_extra_script_layers(tmp_path, monkeypatch):
assert "holohub-my_app:feature-x-coverage" in layer


def test_build_dryrun_allows_bundled_extra_script_dir(tmp_path, monkeypatch):
"""Bundled setup scripts live outside the source project but can still
serve as the Docker build context for extra-script layers."""
root = tmp_path / "project"
project_dir = root / "applications" / "my_app"
project_dir.mkdir(parents=True)
dockerfile = project_dir / "Dockerfile"
dockerfile.write_text("FROM scratch\n", encoding="utf-8")
setup_dir = tmp_path / "package_setup"
setup_dir.mkdir()
(setup_dir / "coverage.sh").write_text("#!/bin/sh\n", encoding="utf-8")
(setup_dir / "Dockerfile.util").write_text("FROM scratch\n", encoding="utf-8")

calls = []
monkeypatch.setenv("HOLOSCAN_CLI_SETUP_SCRIPTS_DIR", str(setup_dir))
monkeypatch.setattr(container_core, "get_host_gpu", lambda: "dgpu")
monkeypatch.setattr(container_core, "get_compute_capacity", lambda: "90")
monkeypatch.setattr(container_core, "get_default_cuda_version", lambda: "13")
monkeypatch.setattr(container_core, "get_current_branch_slug", lambda: "feature-x")
monkeypatch.setattr(container_core, "get_git_short_sha", lambda: "abcdef0")
monkeypatch.setattr(container_core, "run_command", lambda cmd, **kwargs: calls.append(cmd))

c = _stub_container(
root,
project_metadata={
"project_name": "my_app",
"source_folder": str(project_dir),
"metadata": {"language": "python"},
},
)
c.dryrun = True

c.build(extra_scripts=["coverage"])

layer = calls[1]
assert "SCRIPT=coverage.sh" in layer
assert str(setup_dir / "Dockerfile.util") in layer
assert str(setup_dir) in layer


def test_build_dryrun_omits_base_sdk_version_when_not_configured(tmp_path, monkeypatch):
project_dir = tmp_path / "applications" / "my_app"
project_dir.mkdir(parents=True)
Expand Down
36 changes: 36 additions & 0 deletions tests/unit/test_package_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@

import importlib.metadata
import importlib.resources
import os
import subprocess
import sys
from pathlib import Path

Expand Down Expand Up @@ -67,6 +69,7 @@
"sccache.sh",
"template.sh",
"xvfb.sh",
"requirements.template.txt",
}


Expand Down Expand Up @@ -122,6 +125,39 @@ def test_setup_scripts_are_packaged():
assert not missing, f"missing bundled setup scripts: {missing}"


def test_bundled_template_script_uses_bundled_requirements(tmp_path):
"""The fallback template setup script must not depend on HoloHub's
``utilities/requirements.template.txt`` being present."""
setup_dir = importlib.resources.files("holoscan_cli.setup_scripts")
script = setup_dir.joinpath("template.sh")
requirements = setup_dir.joinpath("requirements.template.txt")
assert script.is_file()
assert requirements.is_file()

bin_dir = tmp_path / "bin"
bin_dir.mkdir()
args_file = tmp_path / "python-args.txt"
fake_python = bin_dir / "python3"
fake_python.write_text(
"#!/usr/bin/env bash\n" 'printf \'%s\\n\' "$@" > "${PYTHON_ARGS_FILE}"\n',
encoding="utf-8",
)
fake_python.chmod(0o755)

env = os.environ.copy()
env["PATH"] = f"{bin_dir}:{env['PATH']}"
env["PYTHON_ARGS_FILE"] = str(args_file)
subprocess.run(["bash", str(script)], check=True, env=env)

assert args_file.read_text(encoding="utf-8").splitlines() == [
"-m",
"pip",
"install",
"-r",
str(requirements),
]


# ---- pyproject.toml entry-point declarations --------------------------------


Expand Down
Loading