Skip to content
Open
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
3 changes: 3 additions & 0 deletions src/ucode/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 13 additions & 7 deletions src/ucode/databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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"],
Expand All @@ -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")
Expand All @@ -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:
Expand Down
14 changes: 14 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
24 changes: 24 additions & 0 deletions tests/test_databricks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down
Loading