diff --git a/.github/CI.md b/.github/CI.md index 2aa86372..f003652e 100644 --- a/.github/CI.md +++ b/.github/CI.md @@ -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. @@ -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 diff --git a/.github/scripts/assert_wheel_contents.sh b/.github/scripts/assert_wheel_contents.sh index 2ab49776..0751376d 100755 --- a/.github/scripts/assert_wheel_contents.sh +++ b/.github/scripts/assert_wheel_contents.sh @@ -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 diff --git a/src/holoscan_cli/container/core.py b/src/holoscan_cli/container/core.py index 985b6b21..1bdc9c65 100644 --- a/src/holoscan_cli/container/core.py +++ b/src/holoscan_cli/container/core.py @@ -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", @@ -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. diff --git a/src/holoscan_cli/setup_scripts/Dockerfile.util b/src/holoscan_cli/setup_scripts/Dockerfile.util index 4d97b9d2..569cdc1d 100644 --- a/src/holoscan_cli/setup_scripts/Dockerfile.util +++ b/src/holoscan_cli/setup_scripts/Dockerfile.util @@ -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} diff --git a/src/holoscan_cli/setup_scripts/requirements.template.txt b/src/holoscan_cli/setup_scripts/requirements.template.txt new file mode 100644 index 00000000..d4489751 --- /dev/null +++ b/src/holoscan_cli/setup_scripts/requirements.template.txt @@ -0,0 +1,3 @@ +cookiecutter>=2.7.1 +jsonschema>=4.26.0 +referencing>=0.36.2 diff --git a/src/holoscan_cli/setup_scripts/template.sh b/src/holoscan_cli/setup_scripts/template.sh index 9739116e..863e0ca8 100644 --- a/src/holoscan_cli/setup_scripts/template.sh +++ b/src/holoscan_cli/setup_scripts/template.sh @@ -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 diff --git a/tests/unit/test_container_core.py b/tests/unit/test_container_core.py index f5d9fef9..1d49b8c0 100644 --- a/tests/unit/test_container_core.py +++ b/tests/unit/test_container_core.py @@ -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) diff --git a/tests/unit/test_package_data.py b/tests/unit/test_package_data.py index b9e21613..40f0cc05 100644 --- a/tests/unit/test_package_data.py +++ b/tests/unit/test_package_data.py @@ -32,6 +32,8 @@ import importlib.metadata import importlib.resources +import os +import subprocess import sys from pathlib import Path @@ -67,6 +69,7 @@ "sccache.sh", "template.sh", "xvfb.sh", + "requirements.template.txt", } @@ -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 --------------------------------