From ed167a7e8f96cc0789a8e648acd99a90b3ee5904 Mon Sep 17 00:00:00 2001 From: marcelsafin <179933638+marcelsafin@users.noreply.github.com> Date: Tue, 8 Sep 2026 16:25:59 +0200 Subject: [PATCH] fix: enforce bundle step version pins Assisted-by: GitHub Copilot (model: gpt-5.6-sol, autonomous) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../bundler/services/primitives.py | 15 ++++++++++++++ tests/unit/test_bundler_primitives.py | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/specify_cli/bundler/services/primitives.py b/src/specify_cli/bundler/services/primitives.py index 01fa14769e..4888aa939a 100644 --- a/src/specify_cli/bundler/services/primitives.py +++ b/src/specify_cli/bundler/services/primitives.py @@ -399,6 +399,7 @@ def install(self, component: ComponentRef) -> None: f"is disabled; re-run without --offline or install it first with " f"'specify workflow step add {component.id}'." ) + self._assert_pinned_version(component) from ... import workflow_step_add with _chdir(self._root): @@ -436,6 +437,20 @@ def refresh(self, component: ComponentRef) -> None: finally: shutil.rmtree(backup_dir.parent, ignore_errors=True) + def _assert_pinned_version(self, component: ComponentRef) -> None: + if not component.version: + return + try: + from ...workflows.catalog import StepCatalog + + info = StepCatalog(self._root).get_step_info(component.id) + except Exception: # noqa: BLE001 - catalog unreachable: cannot enforce + return + if info: + _assert_pinned_version( + "Step", component.id, component.version, info.get("version") + ) + def remove(self, component: ComponentRef) -> None: from ... import workflow_step_remove diff --git a/tests/unit/test_bundler_primitives.py b/tests/unit/test_bundler_primitives.py index bbbac1133b..5235f37845 100644 --- a/tests/unit/test_bundler_primitives.py +++ b/tests/unit/test_bundler_primitives.py @@ -120,6 +120,26 @@ def test_workflow_version_mismatch_refuses(tmp_path: Path, monkeypatch): manager.install(component) +def test_step_version_mismatch_refuses(tmp_path: Path, monkeypatch): + import specify_cli + from specify_cli.workflows.catalog import StepCatalog + + monkeypatch.setattr( + StepCatalog, "get_step_info", lambda self, sid: {"version": "9.9.9"} + ) + calls: list[str] = [] + monkeypatch.setattr( + specify_cli, "workflow_step_add", lambda sid: calls.append(sid) + ) + + manager = primitive_manager("steps", tmp_path, allow_network=True) + component = ComponentRef(kind="steps", id="step-a", version="0.3.0") + + with pytest.raises(BundlerError, match="pinned to version 0.3.0"): + manager.install(component) + assert calls == [] + + def test_preset_install_preserves_explicit_zero_priority(tmp_path: Path, monkeypatch): import specify_cli._assets as assets