mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-01 21:28:10 +00:00
fix(cli): address deploy zip review feedback
This commit is contained in:
@@ -8,6 +8,7 @@ from typing import Any
|
||||
import zipfile
|
||||
|
||||
from crewai_cli import git
|
||||
from crewai_cli.deploy.validate import normalize_package_name
|
||||
from crewai_cli.utils import parse_toml
|
||||
|
||||
|
||||
@@ -156,7 +157,9 @@ def _is_json_crew_project(root: Path) -> bool:
|
||||
|
||||
package_name = _package_name(root)
|
||||
if package_name is None:
|
||||
return False
|
||||
raise ValueError(
|
||||
"Could not derive a valid Python package name from [project].name."
|
||||
)
|
||||
|
||||
return not (root / "src" / package_name / "crew.py").is_file()
|
||||
|
||||
@@ -167,9 +170,10 @@ def _read_pyproject(root: Path) -> dict[str, Any]:
|
||||
if not pyproject_path.is_file():
|
||||
return {}
|
||||
try:
|
||||
return parse_toml(pyproject_path.read_text())
|
||||
pyproject = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return {}
|
||||
return pyproject if isinstance(pyproject, dict) else {}
|
||||
|
||||
|
||||
def _package_name(root: Path) -> str | None:
|
||||
@@ -182,8 +186,8 @@ def _package_name(root: Path) -> str | None:
|
||||
if not isinstance(name, str) or not name.strip():
|
||||
return None
|
||||
|
||||
folder = name.replace(" ", "_").replace("-", "_").lower()
|
||||
return re.sub(r"[^a-zA-Z0-9_]", "", folder)
|
||||
package_name = normalize_package_name(name)
|
||||
return package_name or None
|
||||
|
||||
|
||||
def _class_name(package_name: str) -> str:
|
||||
@@ -201,7 +205,9 @@ def _add_json_crew_deploy_wrapper(root: Path) -> None:
|
||||
"""Add Python wrapper files required to deploy a JSON crew project."""
|
||||
package_name = _package_name(root)
|
||||
if package_name is None:
|
||||
return
|
||||
raise ValueError(
|
||||
"Could not derive a valid Python package name from [project].name."
|
||||
)
|
||||
|
||||
package_dir = root / "src" / package_name
|
||||
config_dir = package_dir / "config"
|
||||
|
||||
@@ -268,11 +268,12 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
)
|
||||
except Exception as commit_error:
|
||||
console.print(
|
||||
"Could not create an initial Git commit. Continuing with ZIP deployment.",
|
||||
"Could not create an initial Git commit. "
|
||||
"Continuing with ZIP deployment using Git file listing.",
|
||||
style="yellow",
|
||||
)
|
||||
console.print(str(commit_error), style="dim")
|
||||
return None
|
||||
return repository
|
||||
|
||||
return repository
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import subprocess
|
||||
|
||||
import click
|
||||
|
||||
from crewai_cli.deploy.validate import normalize_package_name
|
||||
from crewai_cli.utils import build_env_with_all_tool_credentials, parse_toml
|
||||
|
||||
|
||||
@@ -30,10 +31,23 @@ def _is_json_crew_project(project_root: Path | None = None) -> bool:
|
||||
pyproject = parse_toml(pyproject_path.read_text())
|
||||
except Exception:
|
||||
return True
|
||||
if not isinstance(pyproject, dict):
|
||||
return True
|
||||
|
||||
declared_type: str | None = (
|
||||
(pyproject.get("tool") or {}).get("crewai", {}).get("type")
|
||||
tool_config = pyproject.get("tool") or {}
|
||||
crewai_config = tool_config.get("crewai") if isinstance(tool_config, dict) else None
|
||||
declared_type = (
|
||||
crewai_config.get("type") if isinstance(crewai_config, dict) else None
|
||||
)
|
||||
project_config = pyproject.get("project") or {}
|
||||
project_name = (
|
||||
project_config.get("name") if isinstance(project_config, dict) else None
|
||||
)
|
||||
if isinstance(project_name, str):
|
||||
package_name = normalize_package_name(project_name)
|
||||
if package_name and (root / "src" / package_name / "crew.py").is_file():
|
||||
return False
|
||||
|
||||
return declared_type != "flow"
|
||||
|
||||
|
||||
|
||||
@@ -138,3 +138,24 @@ type = "crew"
|
||||
assert "JsonCrew" in crew_py
|
||||
assert "from json_crew.crew import JsonCrew" in main_py
|
||||
assert "run_crew = \"json_crew.main:run\"" in pyproject
|
||||
|
||||
|
||||
def test_create_project_zip_rejects_empty_normalized_package_name(tmp_path: Path):
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "!!!"
|
||||
version = "0.1.0"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
+ "\n"
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match=r"Could not derive a valid Python package name",
|
||||
):
|
||||
create_project_zip("invalid", project_dir=tmp_path)
|
||||
|
||||
@@ -440,6 +440,41 @@ class TestDeployCommand(unittest.TestCase):
|
||||
)
|
||||
self.mock_client.create_crew.assert_not_called()
|
||||
|
||||
@patch("crewai_cli.deploy.main.create_project_zip")
|
||||
@patch("crewai_cli.deploy.main.fetch_and_json_env_file")
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
def test_create_crew_without_remote_uses_git_file_list_when_commit_fails(
|
||||
self, mock_repository, mock_fetch_env, mock_create_project_zip
|
||||
):
|
||||
mock_fetch_env.return_value = {"ENV_VAR": "value"}
|
||||
repository = mock_repository.return_value
|
||||
repository.origin_url.return_value = None
|
||||
repository.create_initial_commit_if_needed.side_effect = RuntimeError(
|
||||
"commit failed"
|
||||
)
|
||||
mock_create_project_zip.return_value = Path("/tmp/test_project.zip")
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 201
|
||||
mock_response.is_success = True
|
||||
mock_response.json.return_value = {"uuid": "zip-uuid", "status": "created"}
|
||||
self.mock_client.create_crew_from_zip.return_value = mock_response
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
self.deploy_command.create_crew(confirm=True, skip_validate=True)
|
||||
output = fake_out.getvalue()
|
||||
|
||||
self.assertIn("Continuing with ZIP deployment using Git", output)
|
||||
self.assertIn("file listing", output)
|
||||
mock_create_project_zip.assert_called_once_with(
|
||||
"test_project", repository=repository
|
||||
)
|
||||
self.mock_client.create_crew_from_zip.assert_called_once_with(
|
||||
Path("/tmp/test_project.zip"),
|
||||
name="test_project",
|
||||
env={"ENV_VAR": "value"},
|
||||
)
|
||||
self.mock_client.create_crew.assert_not_called()
|
||||
|
||||
def test_list_crews(self):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -33,6 +34,28 @@ type = "crew"
|
||||
install_crew_module.install_crew([])
|
||||
|
||||
|
||||
def test_install_crew_json_project_with_python_package_installs_project(
|
||||
fp, monkeypatch, tmp_path: Path
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
"""
|
||||
[project]
|
||||
name = "hybrid-crew"
|
||||
|
||||
[tool.crewai]
|
||||
type = "crew"
|
||||
""".strip()
|
||||
)
|
||||
(tmp_path / "crew.jsonc").write_text("{}\n")
|
||||
package_dir = tmp_path / "src" / "hybrid_crew"
|
||||
package_dir.mkdir(parents=True)
|
||||
(package_dir / "crew.py").write_text("class HybridCrew: ...\n")
|
||||
fp.register(["uv", "sync"], stdout="")
|
||||
|
||||
install_crew_module.install_crew([])
|
||||
|
||||
|
||||
def test_install_crew_flow_project_installs_project(fp, monkeypatch, tmp_path: Path):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text(
|
||||
@@ -64,3 +87,16 @@ def test_install_crew_install_project_false_adds_no_install_project(fp):
|
||||
fp.register(["uv", "sync", "--no-install-project", "--frozen"], stdout="")
|
||||
|
||||
install_crew_module.install_crew(["--frozen"], install_project=False)
|
||||
|
||||
|
||||
def test_install_crew_reraises_sync_failure_when_requested(fp):
|
||||
fp.register(["uv", "sync"], returncode=1, stderr="sync failed\n")
|
||||
|
||||
with pytest.raises(subprocess.CalledProcessError):
|
||||
install_crew_module.install_crew([], raise_on_error=True)
|
||||
|
||||
|
||||
def test_install_crew_swallows_sync_failure_by_default(fp):
|
||||
fp.register(["uv", "sync"], returncode=1, stderr="sync failed\n")
|
||||
|
||||
install_crew_module.install_crew([])
|
||||
|
||||
Reference in New Issue
Block a user