From 763381cf88062e2a0aac129a3d27241af390b7ef Mon Sep 17 00:00:00 2001 From: Xiang Shen Date: Wed, 12 Aug 2026 23:12:11 +0000 Subject: [PATCH] ucode: require Databricks CLI 1.11.0+ when configuring skills The skills MCP uploads skill bundles with `databricks fs cp`, which only writes correctly on Databricks CLI v1.11.0 and later (the create then finalize sequence). Before this change `ucode configure skills` inherited the global floor of v1.0.0, so a customer on an older CLI could register the connection and then have uploads silently fail at finalize. Introduce SKILLS_MCP_MIN_DATABRICKS_CLI_VERSION = (1, 11, 0) and enforce it at the top of `configure skills` (both the MCP and download paths, since both register the connection). Parameterize ensure_databricks_cli_version and install_databricks_cli with a `minimum` argument that defaults to the existing global floor, so every other command keeps the v1.0.0 behavior. The existing installer already upgrades transparently, so this is a single automatic upgrade for affected users rather than manual friction. Co-authored-by: Isaac --- src/ucode/cli.py | 3 +++ src/ucode/databricks.py | 20 +++++++++++++------- tests/test_cli.py | 14 ++++++++++++++ tests/test_databricks.py | 24 ++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/ucode/cli.py b/src/ucode/cli.py index 6d2c7edd..8e01f0ee 100644 --- a/src/ucode/cli.py +++ b/src/ucode/cli.py @@ -35,6 +35,7 @@ from ucode.agents.pi import PI_SETTINGS_BACKUP_PATH, PI_SETTINGS_PATH from ucode.config_io import is_dry_run, restore_file, set_dry_run from ucode.databricks import ( + SKILLS_MCP_MIN_DATABRICKS_CLI_VERSION, apply_pat_environment, build_shared_base_urls, discover_claude_models, @@ -2447,6 +2448,8 @@ def configure_skills( ``--location``). """ try: + # Gate on a CLI new enough for skills uploads before registering the connection. + install_databricks_cli(minimum=SKILLS_MCP_MIN_DATABRICKS_CLI_VERSION) locations = _parse_skill_locations(location) # `--skill` absent -> None (whole schema); present (even empty) -> the # explicit subset, so `--skill ""` downloads nothing. diff --git a/src/ucode/databricks.py b/src/ucode/databricks.py index eb709236..b9e2d941 100644 --- a/src/ucode/databricks.py +++ b/src/ucode/databricks.py @@ -53,6 +53,8 @@ AI_GATEWAY_V2_DOCS_URL = "https://docs.databricks.com/aws/en/ai-gateway/overview-beta" # v1.0.0 is the release that ships `databricks aitools`. MIN_DATABRICKS_CLI_VERSION = (1, 0, 0) +# v1.11.0 fixes `fs cp` (create -> finalize), which the skills MCP uploads rely on. +SKILLS_MCP_MIN_DATABRICKS_CLI_VERSION = (1, 11, 0) TOKEN_REFRESH_INTERVAL_SECONDS = 1800 @@ -676,7 +678,9 @@ def _run_databricks_cli_installer(brew_subcommand: str = "install") -> None: raise RuntimeError("Failed to install/upgrade Databricks CLI automatically.") from exc -def ensure_databricks_cli_version() -> None: +def ensure_databricks_cli_version( + minimum: tuple[int, int, int] = MIN_DATABRICKS_CLI_VERSION, +) -> None: try: result = run( ["databricks", "--version"], @@ -695,19 +699,21 @@ def ensure_databricks_cli_version() -> None: raise RuntimeError( f"Could not parse Databricks CLI version from `databricks --version` output: {output!r}" ) - if version < MIN_DATABRICKS_CLI_VERSION: + if version < minimum: current = ".".join(str(n) for n in version) - required = ".".join(str(n) for n in MIN_DATABRICKS_CLI_VERSION) + required = ".".join(str(n) for n in minimum) print_warning( f"Databricks CLI v{current} is too old (need v{required} or newer). Upgrading..." ) _run_databricks_cli_installer(brew_subcommand="upgrade") - ensure_databricks_cli_version() + ensure_databricks_cli_version(minimum) -def install_databricks_cli() -> None: +def install_databricks_cli( + minimum: tuple[int, int, int] = MIN_DATABRICKS_CLI_VERSION, +) -> None: if shutil.which("databricks"): - ensure_databricks_cli_version() + ensure_databricks_cli_version(minimum) return print_section("Bootstrap") @@ -718,7 +724,7 @@ def install_databricks_cli() -> None: raise RuntimeError( "Databricks CLI install completed, but `databricks` is still not on PATH." ) - ensure_databricks_cli_version() + ensure_databricks_cli_version(minimum) def install_ai_tools(agent_tokens: list[str], profile: str | None = None) -> None: diff --git a/tests/test_cli.py b/tests/test_cli.py index cb40e03d..4ef56204 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -555,6 +555,20 @@ def test_status_treats_available_tools_as_configured_agents(self): class TestConfigureSkillsCommand: + @pytest.fixture(autouse=True) + def _stub_install_cli(self): + # These tests cover dispatch only; stub the CLI installer configure_skills runs. + with patch("ucode.cli.install_databricks_cli") as mock_install: + yield mock_install + + def test_requires_skills_mcp_cli_floor(self, _stub_install_cli): + from ucode.databricks import SKILLS_MCP_MIN_DATABRICKS_CLI_VERSION + + with patch("ucode.cli.configure_skills_mcp_command"): + result = runner.invoke(app, ["configure", "skills", "--location", "a.b", "--mcp"]) + assert result.exit_code == 0, result.output + _stub_install_cli.assert_called_once_with(minimum=SKILLS_MCP_MIN_DATABRICKS_CLI_VERSION) + def test_mcp_flag_dispatches_location_set(self): with patch("ucode.cli.configure_skills_mcp_command") as mock_mcp: result = runner.invoke(app, ["configure", "skills", "--location", "a.b", "--mcp"]) diff --git a/tests/test_databricks.py b/tests/test_databricks.py index e1a7f67a..99bdcfd3 100644 --- a/tests/test_databricks.py +++ b/tests/test_databricks.py @@ -1888,6 +1888,30 @@ def test_raises_when_version_unparseable(self, tmp_path, monkeypatch): with pytest.raises(RuntimeError, match="Could not parse"): ensure_databricks_cli_version() + def test_custom_minimum_upgrades_version_below_it(self, tmp_path, monkeypatch): + import ucode.databricks as db_mod + + # v1.8.0 clears the default floor but not the skills-MCP floor (1.11.0). + env = self._fake_databricks(tmp_path, "Databricks CLI v1.8.0") + monkeypatch.setattr("os.environ", env) + upgraded = [] + monkeypatch.setattr( + db_mod, + "_run_databricks_cli_installer", + lambda brew_subcommand="install": upgraded.append(brew_subcommand), + ) + call_count = [0] + original = db_mod.ensure_databricks_cli_version + + def once(*a, **kw): + call_count[0] += 1 + if call_count[0] == 1: + original(*a, **kw) + + monkeypatch.setattr(db_mod, "ensure_databricks_cli_version", once) + once(db_mod.SKILLS_MCP_MIN_DATABRICKS_CLI_VERSION) + assert upgraded == ["upgrade"] + class TestRunDatabricksCliInstaller: @pytest.mark.parametrize("brew_subcommand", ["install", "upgrade"])