diff --git a/lib/cli/src/crewai_cli/deploy/validate.py b/lib/cli/src/crewai_cli/deploy/validate.py index 544ff20cf..b76303d56 100644 --- a/lib/cli/src/crewai_cli/deploy/validate.py +++ b/lib/cli/src/crewai_cli/deploy/validate.py @@ -212,8 +212,16 @@ class DeployValidator: if crew_path is None: return self.results + agents_dir = self.project_root / "agents" + + self._check_pyproject() + self._check_lockfile() + agents_dir_ok = self._check_json_agents_dir(agents_dir) + + project = None try: - project = validate_crew_project(crew_path, self.project_root / "agents") + if agents_dir_ok: + project = validate_crew_project(crew_path, agents_dir) except JSONProjectValidationError as e: self._add( Severity.ERROR, @@ -232,15 +240,27 @@ class DeployValidator: ) return self.results - agents_dir = self.project_root / "agents" - - self._check_pyproject() - self._check_lockfile() - self._check_env_vars_json(crew_path, agents_dir, project.agent_names) + if project is not None: + self._check_env_vars_json(crew_path, agents_dir, project.agent_names) self._check_version_vs_lockfile() return self.results + def _check_json_agents_dir(self, agents_dir: Path) -> bool: + if agents_dir.is_dir(): + return True + self._add( + Severity.ERROR, + "missing_agents_dir", + "Cannot find agents/ directory", + detail=( + "JSON crew projects load agent definitions from " + f"{agents_dir.relative_to(self.project_root)}/*.jsonc or *.json." + ), + hint="Create agents/ and add one JSON or JSONC file per agent.", + ) + return False + def _check_env_vars_json( self, crew_path: Path, agents_dir: Path, agent_names: list[str] ) -> None: diff --git a/lib/cli/tests/deploy/test_archive.py b/lib/cli/tests/deploy/test_archive.py index c4847bafa..03ac6e744 100644 --- a/lib/cli/tests/deploy/test_archive.py +++ b/lib/cli/tests/deploy/test_archive.py @@ -168,6 +168,39 @@ type = "crew" assert "run_crew" not in pyproject +def test_create_project_zip_keeps_json_project_root_shape(tmp_path: Path): + (tmp_path / "pyproject.toml").write_text( + """ +[project] +name = "json_crew" +version = "0.1.0" +dependencies = ["crewai[tools]==1.14.8a1"] + +[tool.crewai] +type = "crew" +""".strip() + + "\n" + ) + (tmp_path / "uv.lock").write_text("# lock\n") + (tmp_path / "agents").mkdir() + (tmp_path / "agents" / "foo.jsonc").write_text("{}\n") + (tmp_path / "crew.jsonc").write_text("{}\n") + + archive_path = create_project_zip("json_crew", 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 == { + "agents/foo.jsonc", + "crew.jsonc", + "pyproject.toml", + "uv.lock", + } + + def test_create_project_zip_does_not_rewrite_json_project_scripts(tmp_path: Path): (tmp_path / "pyproject.toml").write_text( """ diff --git a/lib/cli/tests/deploy/test_validate.py b/lib/cli/tests/deploy/test_validate.py index 07ceef14f..1176fbbdf 100644 --- a/lib/cli/tests/deploy/test_validate.py +++ b/lib/cli/tests/deploy/test_validate.py @@ -200,6 +200,41 @@ def test_json_runtime_fields_are_deploy_errors(tmp_path: Path) -> None: assert "runtime-only" in finding.detail +def test_json_crew_requires_agents_dir_without_classic_errors(tmp_path: Path) -> None: + _scaffold_json_crew(tmp_path) + for path in (tmp_path / "agents").iterdir(): + path.unlink() + (tmp_path / "agents").rmdir() + + v = DeployValidator(project_root=tmp_path) + v.run() + + codes = _codes(v) + assert "missing_agents_dir" in codes + assert "missing_src_dir" not in codes + assert "missing_crew_py" not in codes + assert "missing_agents_yaml" not in codes + assert "missing_tasks_yaml" not in codes + + +def test_json_crew_reports_project_metadata_before_invalid_json( + tmp_path: Path, +) -> None: + _scaffold_json_crew(tmp_path) + (tmp_path / "pyproject.toml").unlink() + (tmp_path / "uv.lock").unlink() + (tmp_path / "crew.jsonc").write_text('{"agents": ["researcher"], "tasks": []}\n') + + v = DeployValidator(project_root=tmp_path) + v.run() + + codes = _codes(v) + assert "missing_pyproject" in codes + assert "missing_lockfile" in codes + assert "invalid_crew_json" in codes + assert "missing_src_dir" not in codes + + def test_missing_pyproject_errors(tmp_path: Path) -> None: v = _run_without_import_check(tmp_path) assert "missing_pyproject" in _codes(v) diff --git a/lib/cli/tests/test_create_crew.py b/lib/cli/tests/test_create_crew.py index d7f34e53b..5d87f8f3e 100644 --- a/lib/cli/tests/test_create_crew.py +++ b/lib/cli/tests/test_create_crew.py @@ -715,6 +715,23 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch): assert not (tmp_path / "json_crew" / "src").exists() assert not (tmp_path / "json_crew" / "tests").exists() assert not (tmp_path / "json_crew" / "config.jsonc").exists() + generated_paths = { + path.relative_to(tmp_path / "json_crew").as_posix() + for path in (tmp_path / "json_crew").rglob("*") + if path.is_file() + } + assert not any( + path.endswith("/crew.py") or path == "crew.py" for path in generated_paths + ) + assert not any( + path.endswith("/agents.yaml") or path == "agents.yaml" + for path in generated_paths + ) + assert not any( + path.endswith("/tasks.yaml") or path == "tasks.yaml" + for path in generated_paths + ) + assert not any(path.startswith("src/") for path in generated_paths) pyproject = tomli.loads((tmp_path / "json_crew" / "pyproject.toml").read_text()) dependency = pyproject["project"]["dependencies"][0]