From e1cbb89df056dbcb751d120b3eae918cf96f6a2d Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Mon, 15 Jun 2026 01:40:57 -0700 Subject: [PATCH] fix(cli): sync missing lockfile before deploy --- lib/cli/src/crewai_cli/deploy/main.py | 26 +++++++++ lib/cli/tests/deploy/test_deploy_main.py | 68 ++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/lib/cli/src/crewai_cli/deploy/main.py b/lib/cli/src/crewai_cli/deploy/main.py index 47be2d374..9f609b145 100644 --- a/lib/cli/src/crewai_cli/deploy/main.py +++ b/lib/cli/src/crewai_cli/deploy/main.py @@ -1,3 +1,5 @@ +from pathlib import Path +import subprocess from typing import Any from crewai_core.plus_api import CreateCrewPayload @@ -58,6 +60,28 @@ def _env_summary(env_vars: dict[str, str]) -> str: return f"{len(env_vars)} env vars: {keys}" +def _ensure_lockfile_for_deploy() -> None: + """Create a uv lockfile before deploy when a project has not been run yet.""" + project_root = Path.cwd() + if not (project_root / "pyproject.toml").is_file(): + return + if (project_root / "uv.lock").is_file() or (project_root / "poetry.lock").is_file(): + return + + from crewai_cli.install_crew import install_crew + + console.print( + "No lockfile found. Installing dependencies before deployment...", + style="bold blue", + ) + try: + install_crew([], raise_on_error=True) + except subprocess.CalledProcessError as e: + raise SystemExit(e.returncode) from e + except Exception as e: + raise SystemExit(1) from e + + class DeployCommand(BaseCommand, PlusAPIMixin): """ A class to handle deployment-related operations for CrewAI projects. @@ -116,6 +140,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin): uuid (Optional[str]): The UUID of the crew to deploy. skip_validate (bool): Skip pre-deploy validation checks. """ + _ensure_lockfile_for_deploy() if not _run_predeploy_validation(skip_validate): return self._telemetry.start_deployment_span(uuid) @@ -161,6 +186,7 @@ class DeployCommand(BaseCommand, PlusAPIMixin): confirm (bool): Whether to skip the interactive confirmation prompt. skip_validate (bool): Skip pre-deploy validation checks. """ + _ensure_lockfile_for_deploy() if not _run_predeploy_validation(skip_validate): return self._telemetry.create_crew_deployment_span() diff --git a/lib/cli/tests/deploy/test_deploy_main.py b/lib/cli/tests/deploy/test_deploy_main.py index 2e80068cc..61fb399d3 100644 --- a/lib/cli/tests/deploy/test_deploy_main.py +++ b/lib/cli/tests/deploy/test_deploy_main.py @@ -2,16 +2,84 @@ import sys import unittest from io import StringIO from pathlib import Path +import subprocess from unittest.mock import MagicMock, Mock, patch import pytest import json +import crewai_cli.deploy.main as deploy_main import httpx from crewai_cli.deploy.main import DeployCommand from crewai_cli.utils import parse_toml +def test_ensure_lockfile_for_deploy_runs_install_when_lock_missing( + monkeypatch, tmp_path: Path +): + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") + calls = [] + + def fake_install_crew(proxy_options, *, raise_on_error=False): + calls.append((proxy_options, raise_on_error)) + + monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) + + deploy_main._ensure_lockfile_for_deploy() + + assert calls == [([], True)] + + +def test_ensure_lockfile_for_deploy_skips_when_lock_exists(monkeypatch, tmp_path: Path): + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") + (tmp_path / "uv.lock").write_text("# lock\n") + calls = [] + + def fake_install_crew(proxy_options, *, raise_on_error=False): + calls.append((proxy_options, raise_on_error)) + + monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) + + deploy_main._ensure_lockfile_for_deploy() + + assert calls == [] + + +def test_ensure_lockfile_for_deploy_skips_without_pyproject( + monkeypatch, tmp_path: Path +): + monkeypatch.chdir(tmp_path) + calls = [] + + def fake_install_crew(proxy_options, *, raise_on_error=False): + calls.append((proxy_options, raise_on_error)) + + monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) + + deploy_main._ensure_lockfile_for_deploy() + + assert calls == [] + + +def test_ensure_lockfile_for_deploy_failure_exits_nonzero( + monkeypatch, tmp_path: Path +): + monkeypatch.chdir(tmp_path) + (tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n") + + def fake_install_crew(proxy_options, *, raise_on_error=False): + raise subprocess.CalledProcessError(42, ["uv", "sync"]) + + monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew) + + with pytest.raises(SystemExit) as exc_info: + deploy_main._ensure_lockfile_for_deploy() + + assert exc_info.value.code == 42 + + class TestDeployCommand(unittest.TestCase): @patch("crewai_cli.command.get_auth_token") @patch("crewai_cli.deploy.main.get_project_name")