mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-22 02:46:39 +00:00
fix(cli): keep deploy push on the AMP create source (#7345)
Push was choosing ZIP vs git from a local origin remote, so adding origin later rebuilt the last ZIP with no files. Prefer AMP zip_deployment from status, and fall back to the old origin heuristic when that field is missing.
This commit is contained in:
@@ -201,6 +201,7 @@ def crew(self) -> Crew:
|
||||
```shell Terminal
|
||||
crewai deploy push
|
||||
```
|
||||
يحافظ الدفع على المصدر المستخدم عند الإنشاء. إضافة `origin` لاحقًا لا تحوّل نشر ZIP إلى git.
|
||||
|
||||
- **حالة النشر**:
|
||||
```shell Terminal
|
||||
|
||||
@@ -338,6 +338,7 @@ crewai org switch <organization_id>
|
||||
|
||||
- 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:
|
||||
|
||||
|
||||
@@ -286,6 +286,7 @@ crewai org switch <organization_id>
|
||||
|
||||
- CrewAI AMP 플랫폼에서 배포 프로세스를 시작합니다.
|
||||
- 성공적으로 시작되면, Deployment created successfully! 메시지와 함께 Deployment Name 및 고유한 Deployment ID(UUID)가 출력됩니다.
|
||||
- Push는 생성 시 사용한 소스를 유지합니다. 나중에 git `origin`을 추가해도 ZIP 배포가 git으로 바뀌지 않습니다.
|
||||
|
||||
- **배포 상태**: 배포 상태를 확인하려면 다음을 사용합니다:
|
||||
|
||||
|
||||
@@ -307,6 +307,7 @@ crewai org switch <organization_id>
|
||||
|
||||
- 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:
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user