diff --git a/docs/edge/ar/concepts/cli.mdx b/docs/edge/ar/concepts/cli.mdx index 3beb6f13b..28162a61c 100644 --- a/docs/edge/ar/concepts/cli.mdx +++ b/docs/edge/ar/concepts/cli.mdx @@ -201,6 +201,7 @@ def crew(self) -> Crew: ```shell Terminal crewai deploy push ``` + يحافظ الدفع على المصدر المستخدم عند الإنشاء. إضافة `origin` لاحقًا لا تحوّل نشر ZIP إلى git. - **حالة النشر**: ```shell Terminal diff --git a/docs/edge/en/concepts/cli.mdx b/docs/edge/en/concepts/cli.mdx index b09f78d87..1562c9e0c 100644 --- a/docs/edge/en/concepts/cli.mdx +++ b/docs/edge/en/concepts/cli.mdx @@ -338,6 +338,7 @@ crewai org switch - Initiates the deployment process on the CrewAI AMP platform. - Upon successful initiation, it will output the Deployment created successfully! message along with the Deployment Name and a unique Deployment ID (UUID). + - Push keeps the source used at create. Adding a git `origin` later does not switch a ZIP deployment to git. - **Deployment Status**: You can check the status of your deployment with: diff --git a/docs/edge/ko/concepts/cli.mdx b/docs/edge/ko/concepts/cli.mdx index 7a681937f..7f2a62e99 100644 --- a/docs/edge/ko/concepts/cli.mdx +++ b/docs/edge/ko/concepts/cli.mdx @@ -286,6 +286,7 @@ crewai org switch - CrewAI AMP 플랫폼에서 배포 프로세스를 시작합니다. - 성공적으로 시작되면, Deployment created successfully! 메시지와 함께 Deployment Name 및 고유한 Deployment ID(UUID)가 출력됩니다. + - Push는 생성 시 사용한 소스를 유지합니다. 나중에 git `origin`을 추가해도 ZIP 배포가 git으로 바뀌지 않습니다. - **배포 상태**: 배포 상태를 확인하려면 다음을 사용합니다: diff --git a/docs/edge/pt-BR/concepts/cli.mdx b/docs/edge/pt-BR/concepts/cli.mdx index 7577e3408..4fe206dc2 100644 --- a/docs/edge/pt-BR/concepts/cli.mdx +++ b/docs/edge/pt-BR/concepts/cli.mdx @@ -307,6 +307,7 @@ crewai org switch - Inicia o processo de deployment na plataforma CrewAI AMP. - Após a iniciação bem-sucedida, será exibida a mensagem Deployment created successfully! juntamente com o Nome do Deployment e um Deployment ID (UUID) único. + - O push mantém a origem usada na criação. Adicionar um `origin` git depois não troca um deployment ZIP por git. - **Status do Deployment**: Você pode verificar o status do seu deployment com: diff --git a/lib/cli/src/crewai_cli/deploy/main.py b/lib/cli/src/crewai_cli/deploy/main.py index cf4b47cb3..c42f9be2b 100644 --- a/lib/cli/src/crewai_cli/deploy/main.py +++ b/lib/cli/src/crewai_cli/deploy/main.py @@ -77,6 +77,14 @@ def _display_git_remote_help() -> None: ) +def _zip_deployment_flag(status: dict[str, Any] | None) -> bool | None: + """Return the AMP zip_deployment flag, or None when it cannot be used.""" + if not status or "zip_deployment" not in status: + return None + value = status["zip_deployment"] + return value if isinstance(value, bool) else None + + def _env_summary(env_vars: dict[str, str]) -> str: """Return a compact description of environment variables for prompts.""" if not env_vars: @@ -307,30 +315,102 @@ class DeployCommand(BaseCommand, PlusAPIMixin): repository = self._prepare_git_repository() remote_repo_url = repository.origin_url() if repository else None - if remote_repo_url and uuid: - response = self.plus_api_client.deploy_by_uuid(uuid) - elif remote_repo_url and self.project_name: - response = self.plus_api_client.deploy_by_name(self.project_name) - elif uuid: - _display_git_remote_help() - env_vars = fetch_and_json_env_file() - response = self._update_crew_from_zip(uuid, repository, env_vars) - elif self.project_name: - _display_git_remote_help() - deployment_uuid = self._deployment_uuid_by_name() - env_vars = fetch_and_json_env_file() - response = self._update_crew_from_zip( - deployment_uuid, - repository, - env_vars, + status = self._deployment_status(uuid, self.project_name) + if status is not None and self._can_deploy_from_amp( + uuid, self.project_name, status + ): + response = self._deploy_from_amp_source( + uuid, self.project_name, repository, status ) else: - self._standard_no_param_error_message() - return + response = self._deploy_from_local_source( + uuid, self.project_name, repository, remote_repo_url + ) + if response is None: + self._standard_no_param_error_message() + return self._validate_response(response) self._display_deployment_info(response.json()) + def _deployment_status( + self, + uuid: str | None, + project_name: str | None, + ) -> dict[str, Any] | None: + """Fetch deployment status without failing the command.""" + try: + if uuid: + response = self.plus_api_client.crew_status_by_uuid(uuid) + elif project_name: + response = self.plus_api_client.crew_status_by_name(project_name) + else: + return None + if not response.is_success: + return None + payload = response.json() + except Exception: + return None + return payload if isinstance(payload, dict) else None + + def _can_deploy_from_amp( + self, + uuid: str | None, + project_name: str | None, + status: dict[str, Any] | None, + ) -> bool: + """Return True when AMP reported a usable zip_deployment flag.""" + zip_deployment = _zip_deployment_flag(status) + if zip_deployment is None: + return False + if zip_deployment: + return bool(uuid or (status and status.get("uuid"))) + return bool(uuid or project_name) + + def _deploy_from_amp_source( + self, + uuid: str | None, + project_name: str | None, + repository: git.Repository | None, + status: dict[str, Any], + ) -> Any: + """Deploy using AMP zip_deployment.""" + if _zip_deployment_flag(status): + deployment_uuid = uuid or str(status["uuid"]) + env_vars = fetch_and_json_env_file() + return self._update_crew_from_zip(deployment_uuid, repository, env_vars) + if uuid: + return self.plus_api_client.deploy_by_uuid(uuid) + if not project_name: + raise ValueError("project_name is required to deploy by name") + return self.plus_api_client.deploy_by_name(project_name) + + def _deploy_from_local_source( + self, + uuid: str | None, + project_name: str | None, + repository: git.Repository | None, + remote_repo_url: str | None, + ) -> Any | None: + """Deploy using local origin, as before AMP zip_deployment existed.""" + if remote_repo_url and uuid: + return self.plus_api_client.deploy_by_uuid(uuid) + if remote_repo_url and project_name: + return self.plus_api_client.deploy_by_name(project_name) + if uuid: + _display_git_remote_help() + env_vars = fetch_and_json_env_file() + return self._update_crew_from_zip(uuid, repository, env_vars) + if project_name: + _display_git_remote_help() + env_vars = fetch_and_json_env_file() + return self._update_crew_from_zip( + self._deployment_uuid_by_name(), + repository, + env_vars, + ) + return None + def _deployment_uuid_by_name(self) -> str: """Resolve the current project's deployment UUID by project name.""" if not self.project_name: diff --git a/lib/cli/tests/deploy/test_deploy_main.py b/lib/cli/tests/deploy/test_deploy_main.py index e471f2f43..0a3dc4b4d 100644 --- a/lib/cli/tests/deploy/test_deploy_main.py +++ b/lib/cli/tests/deploy/test_deploy_main.py @@ -223,6 +223,21 @@ class TestDeployCommand(unittest.TestCase): self.mock_browser_open = self.mock_browser_open_patcher.start() self.addCleanup(self.mock_browser_open_patcher.stop) + def _status_response( + self, + *, + uuid: str = "test-uuid", + zip_deployment: bool | None = None, + is_success: bool = True, + ) -> MagicMock: + response = MagicMock() + response.is_success = is_success + payload: dict = {"uuid": uuid} + if zip_deployment is not None: + payload["zip_deployment"] = zip_deployment + response.json.return_value = payload + return response + def test_init_success(self): self.assertEqual(self.deploy_command.project_name, "test_project") self.mock_plus_api.assert_called_once_with(api_key="test_token") @@ -414,6 +429,9 @@ class TestDeployCommand(unittest.TestCase): mock_repository.return_value.create_initial_commit_if_needed.return_value = ( False ) + self.mock_client.crew_status_by_uuid.return_value = self._status_response( + zip_deployment=False + ) mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"uuid": "test-uuid"} @@ -421,6 +439,7 @@ class TestDeployCommand(unittest.TestCase): self.deploy_command.deploy(uuid="test-uuid", skip_validate=True) + self.mock_client.crew_status_by_uuid.assert_called_once_with("test-uuid") self.mock_client.deploy_by_uuid.assert_called_once_with("test-uuid") mock_display.assert_called_once_with({"uuid": "test-uuid"}) @@ -433,6 +452,9 @@ class TestDeployCommand(unittest.TestCase): mock_repository.return_value.create_initial_commit_if_needed.return_value = ( False ) + self.mock_client.crew_status_by_name.return_value = self._status_response( + zip_deployment=False + ) mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"uuid": "test-uuid"} @@ -440,6 +462,7 @@ class TestDeployCommand(unittest.TestCase): self.deploy_command.deploy(skip_validate=True) + self.mock_client.crew_status_by_name.assert_called_once_with("test_project") self.mock_client.deploy_by_name.assert_called_once_with("test_project") mock_display.assert_called_once_with({"uuid": "test-uuid"}) @@ -453,6 +476,9 @@ class TestDeployCommand(unittest.TestCase): repository.origin_url.return_value = "https://github.com/test/repo.git" repository.fetch.side_effect = ValueError("fetch failed") repository.create_initial_commit_if_needed.return_value = False + self.mock_client.crew_status_by_name.return_value = self._status_response( + zip_deployment=False + ) mock_response = MagicMock() mock_response.status_code = 200 mock_response.is_success = True @@ -482,6 +508,9 @@ class TestDeployCommand(unittest.TestCase): repository.create_initial_commit_if_needed.side_effect = RuntimeError( "commit failed" ) + self.mock_client.crew_status_by_name.return_value = self._status_response( + zip_deployment=False + ) mock_response = MagicMock() mock_response.status_code = 200 mock_response.is_success = True @@ -513,6 +542,9 @@ class TestDeployCommand(unittest.TestCase): False ) mock_create_project_zip.return_value = Path("/tmp/test_project.zip") + self.mock_client.crew_status_by_uuid.return_value = self._status_response( + zip_deployment=True + ) mock_response = MagicMock() mock_response.status_code = 200 mock_response.json.return_value = {"uuid": "test-uuid"} @@ -520,6 +552,7 @@ class TestDeployCommand(unittest.TestCase): self.deploy_command.deploy(uuid="test-uuid", skip_validate=True) + self.mock_client.crew_status_by_uuid.assert_called_once_with("test-uuid") self.mock_client.update_crew_from_zip.assert_called_once_with( "test-uuid", Path("/tmp/test_project.zip"), @@ -541,14 +574,12 @@ class TestDeployCommand(unittest.TestCase): False ) mock_create_project_zip.return_value = Path("/tmp/test_project.zip") - status_response = MagicMock() - status_response.status_code = 200 - status_response.is_success = True - status_response.json.return_value = {"uuid": "test-uuid"} update_response = MagicMock() update_response.status_code = 200 update_response.json.return_value = {"uuid": "test-uuid"} - self.mock_client.crew_status_by_name.return_value = status_response + self.mock_client.crew_status_by_name.return_value = self._status_response( + zip_deployment=True + ) self.mock_client.update_crew_from_zip.return_value = update_response self.deploy_command.deploy(skip_validate=True) @@ -562,6 +593,118 @@ class TestDeployCommand(unittest.TestCase): self.mock_client.deploy_by_name.assert_not_called() mock_display.assert_called_once_with({"uuid": "test-uuid"}) + @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") + @patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info") + def test_deploy_zip_amp_source_uploads_even_when_origin_exists( + self, mock_display, mock_repository, mock_fetch_env, mock_create_project_zip + ): + mock_fetch_env.return_value = {"ENV_VAR": "value"} + mock_repository.return_value.origin_url.return_value = ( + "https://github.com/test/repo.git" + ) + mock_repository.return_value.create_initial_commit_if_needed.return_value = ( + False + ) + mock_create_project_zip.return_value = Path("/tmp/test_project.zip") + self.mock_client.crew_status_by_uuid.return_value = self._status_response( + zip_deployment=True + ) + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"uuid": "test-uuid"} + self.mock_client.update_crew_from_zip.return_value = mock_response + + self.deploy_command.deploy(uuid="test-uuid", skip_validate=True) + + self.mock_client.update_crew_from_zip.assert_called_once_with( + "test-uuid", + Path("/tmp/test_project.zip"), + env={"ENV_VAR": "value"}, + ) + self.mock_client.deploy_by_uuid.assert_not_called() + mock_display.assert_called_once_with({"uuid": "test-uuid"}) + + @patch("crewai_cli.deploy.main.git.Repository") + @patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info") + def test_deploy_falls_back_to_origin_when_zip_deployment_is_missing( + self, mock_display, mock_repository + ): + mock_repository.return_value.origin_url.return_value = ( + "https://github.com/test/repo.git" + ) + mock_repository.return_value.create_initial_commit_if_needed.return_value = ( + False + ) + self.mock_client.crew_status_by_uuid.return_value = self._status_response() + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"uuid": "test-uuid"} + self.mock_client.deploy_by_uuid.return_value = mock_response + + self.deploy_command.deploy(uuid="test-uuid", skip_validate=True) + + self.mock_client.deploy_by_uuid.assert_called_once_with("test-uuid") + self.mock_client.update_crew_from_zip.assert_not_called() + mock_display.assert_called_once_with({"uuid": "test-uuid"}) + + @patch("crewai_cli.deploy.main.git.Repository") + @patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info") + def test_deploy_falls_back_to_origin_when_status_request_fails( + self, mock_display, mock_repository + ): + mock_repository.return_value.origin_url.return_value = ( + "https://github.com/test/repo.git" + ) + mock_repository.return_value.create_initial_commit_if_needed.return_value = ( + False + ) + self.mock_client.crew_status_by_uuid.return_value = self._status_response( + zip_deployment=True, is_success=False + ) + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"uuid": "test-uuid"} + self.mock_client.deploy_by_uuid.return_value = mock_response + + self.deploy_command.deploy(uuid="test-uuid", skip_validate=True) + + self.mock_client.deploy_by_uuid.assert_called_once_with("test-uuid") + self.mock_client.update_crew_from_zip.assert_not_called() + mock_display.assert_called_once_with({"uuid": "test-uuid"}) + + @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") + @patch("crewai_cli.deploy.main.DeployCommand._display_deployment_info") + def test_deploy_falls_back_to_zip_when_status_fails_and_origin_is_missing( + self, mock_display, mock_repository, mock_fetch_env, mock_create_project_zip + ): + mock_fetch_env.return_value = {"ENV_VAR": "value"} + mock_repository.return_value.origin_url.return_value = None + mock_repository.return_value.create_initial_commit_if_needed.return_value = ( + False + ) + mock_create_project_zip.return_value = Path("/tmp/test_project.zip") + self.mock_client.crew_status_by_uuid.side_effect = httpx.ConnectError( + "offline" + ) + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {"uuid": "test-uuid"} + self.mock_client.update_crew_from_zip.return_value = mock_response + + self.deploy_command.deploy(uuid="test-uuid", skip_validate=True) + + self.mock_client.update_crew_from_zip.assert_called_once_with( + "test-uuid", + Path("/tmp/test_project.zip"), + env={"ENV_VAR": "value"}, + ) + self.mock_client.deploy_by_uuid.assert_not_called() + mock_display.assert_called_once_with({"uuid": "test-uuid"}) + @patch("crewai_cli.deploy.main.fetch_and_json_env_file") @patch("crewai_cli.deploy.main.git.Repository") @patch("builtins.input")