mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-05 15:09:22 +00:00
fix(cli): harden json run and zip fallback
This commit is contained in:
@@ -75,12 +75,15 @@ def create_project_zip(
|
||||
def _project_files(root: Path, repository: git.Repository | None = None) -> list[Path]:
|
||||
"""Return project-relative files to include in the archive."""
|
||||
if repository is not None:
|
||||
files = [Path(path) for path in repository.deployable_files()]
|
||||
return [
|
||||
path
|
||||
for path in files
|
||||
if not _is_excluded(path) and _is_regular_file(root / path)
|
||||
]
|
||||
return _repository_project_files(root, repository)
|
||||
|
||||
try:
|
||||
repository = git.Repository(path=str(root), fetch=False)
|
||||
except ValueError:
|
||||
repository = None
|
||||
|
||||
if repository is not None:
|
||||
return _repository_project_files(root, repository)
|
||||
|
||||
return [
|
||||
path
|
||||
@@ -89,6 +92,16 @@ def _project_files(root: Path, repository: git.Repository | None = None) -> list
|
||||
]
|
||||
|
||||
|
||||
def _repository_project_files(root: Path, repository: git.Repository) -> list[Path]:
|
||||
"""Return deployable files from Git while applying local safety excludes."""
|
||||
files = [Path(path) for path in repository.deployable_files()]
|
||||
return [
|
||||
path
|
||||
for path in files
|
||||
if not _is_excluded(path) and _is_regular_file(root / path)
|
||||
]
|
||||
|
||||
|
||||
def _walk_files(root: Path) -> list[Path]:
|
||||
"""List regular files below root as project-relative paths."""
|
||||
return [
|
||||
|
||||
@@ -234,7 +234,11 @@ class DeployCommand(BaseCommand, PlusAPIMixin):
|
||||
style="yellow",
|
||||
)
|
||||
console.print(str(init_error), style="dim")
|
||||
return None
|
||||
try:
|
||||
return git.Repository(fetch=False)
|
||||
except Exception as repository_error:
|
||||
console.print(str(repository_error), style="dim")
|
||||
return None
|
||||
|
||||
_display_git_repository_help()
|
||||
return repository
|
||||
|
||||
@@ -262,17 +262,31 @@ def _has_lockfile(project_root: Path | None = None) -> bool:
|
||||
return (root / "uv.lock").is_file() or (root / "poetry.lock").is_file()
|
||||
|
||||
|
||||
def _has_project_venv(project_root: Path | None = None) -> bool:
|
||||
"""Return True when the project already has a local uv environment."""
|
||||
root = project_root or Path.cwd()
|
||||
return (root / ".venv").is_dir()
|
||||
|
||||
|
||||
def _install_json_crew_dependencies_if_needed() -> None:
|
||||
"""Lock and sync JSON crew projects only when no lockfile exists."""
|
||||
"""Prepare JSON crew dependencies without mutating existing lockfiles."""
|
||||
project_root = Path.cwd()
|
||||
if not (project_root / "pyproject.toml").is_file() or _has_lockfile(project_root):
|
||||
if not (project_root / "pyproject.toml").is_file():
|
||||
return
|
||||
|
||||
has_lockfile = _has_lockfile(project_root)
|
||||
if has_lockfile and _has_project_venv(project_root):
|
||||
return
|
||||
|
||||
from crewai_cli.install_crew import install_crew
|
||||
|
||||
try:
|
||||
click.echo("Installing dependencies...")
|
||||
install_crew([], raise_on_error=True, install_project=False)
|
||||
if has_lockfile:
|
||||
click.echo("Syncing dependencies from lockfile...")
|
||||
install_crew(["--frozen"], raise_on_error=True)
|
||||
else:
|
||||
click.echo("Installing dependencies...")
|
||||
install_crew([], raise_on_error=True)
|
||||
except subprocess.CalledProcessError as e:
|
||||
raise SystemExit(e.returncode) from e
|
||||
except Exception as e:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
import pytest
|
||||
@@ -56,6 +57,42 @@ def test_create_project_zip_uses_repository_file_list(tmp_path: Path):
|
||||
assert names == {"pyproject.toml", "uv.lock"}
|
||||
|
||||
|
||||
def test_create_project_zip_without_repository_uses_git_ignore_rules(
|
||||
tmp_path: Path,
|
||||
):
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / ".gitignore").write_text("node_modules/\nsecret.txt\n")
|
||||
(tmp_path / "src").mkdir()
|
||||
(tmp_path / "src" / "main.py").write_text("print('hello')\n")
|
||||
(tmp_path / "node_modules").mkdir()
|
||||
(tmp_path / "node_modules" / "package.json").write_text("{}\n")
|
||||
(tmp_path / "secret.txt").write_text("secret\n")
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["git", "init"],
|
||||
cwd=tmp_path,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
text=True,
|
||||
)
|
||||
except (FileNotFoundError, subprocess.CalledProcessError) as exc:
|
||||
pytest.skip(f"git is not available in this environment: {exc}")
|
||||
|
||||
archive_path = create_project_zip("demo", project_dir=tmp_path)
|
||||
try:
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
names = set(archive.namelist())
|
||||
finally:
|
||||
archive_path.unlink(missing_ok=True)
|
||||
|
||||
assert names == {
|
||||
".gitignore",
|
||||
"pyproject.toml",
|
||||
"src/main.py",
|
||||
}
|
||||
|
||||
|
||||
def test_create_project_zip_does_not_fallback_when_repository_listing_fails(
|
||||
tmp_path: Path,
|
||||
):
|
||||
|
||||
@@ -414,6 +414,25 @@ class TestDeployCommand(unittest.TestCase):
|
||||
)
|
||||
self.mock_client.create_crew.assert_not_called()
|
||||
|
||||
@patch("crewai_cli.deploy.main.git.Repository")
|
||||
def test_prepare_git_repository_returns_repo_when_init_commit_fails(
|
||||
self, mock_repository
|
||||
):
|
||||
recovered_repository = MagicMock()
|
||||
mock_repository.side_effect = [
|
||||
ValueError("not a Git repository"),
|
||||
recovered_repository,
|
||||
]
|
||||
mock_repository.initialize.side_effect = RuntimeError("commit failed")
|
||||
|
||||
with patch("sys.stdout", new=StringIO()) as fake_out:
|
||||
repository = self.deploy_command._prepare_git_repository()
|
||||
|
||||
self.assertIs(repository, recovered_repository)
|
||||
self.assertIn("Git auto-setup did not complete", fake_out.getvalue())
|
||||
mock_repository.initialize.assert_called_once_with()
|
||||
self.assertEqual(mock_repository.call_count, 2)
|
||||
|
||||
@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")
|
||||
|
||||
@@ -182,7 +182,7 @@ def test_json_project_env_run_failure_exits_nonzero(monkeypatch, tmp_path: Path)
|
||||
def test_json_run_installs_dependencies_when_pyproject_has_no_lockfile(
|
||||
monkeypatch, tmp_path: Path
|
||||
):
|
||||
"""JSON crew runs should lock/sync project dependencies only once."""
|
||||
"""JSON crew runs should lock/sync project dependencies only when needed."""
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
calls = []
|
||||
@@ -196,11 +196,11 @@ def test_json_run_installs_dependencies_when_pyproject_has_no_lockfile(
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == [([], True, False)]
|
||||
assert calls == [([], True, None)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lockfile", ["uv.lock", "poetry.lock"])
|
||||
def test_json_run_skips_dependency_install_when_lockfile_exists(
|
||||
def test_json_run_syncs_frozen_when_lockfile_exists_without_venv(
|
||||
monkeypatch, tmp_path: Path, lockfile: str
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
@@ -217,6 +217,28 @@ def test_json_run_skips_dependency_install_when_lockfile_exists(
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == [(["--frozen"], True, None)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("lockfile", ["uv.lock", "poetry.lock"])
|
||||
def test_json_run_skips_dependency_install_when_lockfile_and_venv_exist(
|
||||
monkeypatch, tmp_path: Path, lockfile: str
|
||||
):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
(tmp_path / "pyproject.toml").write_text("[project]\nname = 'demo'\n")
|
||||
(tmp_path / lockfile).write_text("# lock\n")
|
||||
(tmp_path / ".venv").mkdir()
|
||||
calls = []
|
||||
|
||||
def fake_install_crew(
|
||||
proxy_options, *, raise_on_error=False, install_project=None
|
||||
):
|
||||
calls.append((proxy_options, raise_on_error, install_project))
|
||||
|
||||
monkeypatch.setattr("crewai_cli.install_crew.install_crew", fake_install_crew)
|
||||
|
||||
run_crew_module._install_json_crew_dependencies_if_needed()
|
||||
|
||||
assert calls == []
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user