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:
Vidit Ostwal
2026-09-10 00:23:40 +05:30
committed by GitHub
parent 57df7f5202
commit 4ed4aba6cd
6 changed files with 250 additions and 23 deletions

View File

@@ -201,6 +201,7 @@ def crew(self) -> Crew:
```shell Terminal
crewai deploy push
```
يحافظ الدفع على المصدر المستخدم عند الإنشاء. إضافة `origin` لاحقًا لا تحوّل نشر ZIP إلى git.
- **حالة النشر**:
```shell Terminal

View File

@@ -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:

View File

@@ -286,6 +286,7 @@ crewai org switch <organization_id>
- CrewAI AMP 플랫폼에서 배포 프로세스를 시작합니다.
- 성공적으로 시작되면, Deployment created successfully! 메시지와 함께 Deployment Name 및 고유한 Deployment ID(UUID)가 출력됩니다.
- Push는 생성 시 사용한 소스를 유지합니다. 나중에 git `origin`을 추가해도 ZIP 배포가 git으로 바뀌지 않습니다.
- **배포 상태**: 배포 상태를 확인하려면 다음을 사용합니다:

View File

@@ -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:

View File

@@ -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:

View File

@@ -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")