diff --git a/src/fastapi_cloud_cli/commands/deploy.py b/src/fastapi_cloud_cli/commands/deploy.py index 4daa8855..6c6b6bac 100644 --- a/src/fastapi_cloud_cli/commands/deploy.py +++ b/src/fastapi_cloud_cli/commands/deploy.py @@ -6,6 +6,7 @@ from enum import Enum from itertools import cycle from pathlib import Path +from textwrap import dedent from typing import Any, Dict, List, Optional, Union import fastar @@ -388,12 +389,15 @@ def _wait_for_deployment( last_message_changed_at = time.monotonic() - except (BuildLogError, TooManyRetriesError) as e: - logger.error("Build log streaming failed: %s", e) - toolkit.print_line() - toolkit.print( - f"⚠️ Unable to stream build logs. Check the dashboard for status: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]" + except (BuildLogError, TooManyRetriesError, TimeoutError) as e: + progress.set_error( + dedent(f""" + [error]Build log streaming failed: {e}[/] + + Unable to stream build logs. Check the dashboard for status: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link] + """).strip() ) + raise typer.Exit(1) from e diff --git a/src/fastapi_cloud_cli/utils/api.py b/src/fastapi_cloud_cli/utils/api.py index fe494c20..5c817039 100644 --- a/src/fastapi_cloud_cli/utils/api.py +++ b/src/fastapi_cloud_cli/utils/api.py @@ -114,8 +114,7 @@ def wrapper(*args: P.args, **kwargs: P.kwargs) -> Generator[T, None, None]: for attempt_number in range(total_attempts): if time.monotonic() - start > timeout.total_seconds(): raise TimeoutError( - "Build log streaming timed out after %ds", - timeout.total_seconds(), + f"Build log streaming timed out after {timeout.total_seconds():.0f}s" ) with attempt(attempt_number): diff --git a/tests/test_cli_deploy.py b/tests/test_cli_deploy.py index 3415eda7..ea3a9a22 100644 --- a/tests/test_cli_deploy.py +++ b/tests/test_cli_deploy.py @@ -15,6 +15,7 @@ from fastapi_cloud_cli.cli import app from fastapi_cloud_cli.config import Settings +from fastapi_cloud_cli.utils.api import BuildLogError, TooManyRetriesError from tests.conftest import ConfiguredApp from tests.utils import Keys, build_logs_response, changing_dir @@ -771,11 +772,14 @@ def test_shows_no_apps_found_message_when_team_has_no_apps( ) +@pytest.mark.parametrize( + "error", + [BuildLogError, TooManyRetriesError, TimeoutError], +) @pytest.mark.respx(base_url=settings.base_api_url) -def test_handles_build_log_streaming_error( - logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter +def test_shows_error_message_on_build_exception( + logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter, error: Exception ) -> None: - """Test that BuildLogError is caught and shows dashboard link (lines 384, 387-392).""" app_data = _get_random_app() team_data = _get_random_team() app_id = app_data["id"] @@ -802,11 +806,10 @@ def test_handles_build_log_streaming_error( return_value=Response(200) ) - respx_mock.get(f"/deployments/{deployment_data['id']}/build-logs").mock( - return_value=Response(422, text="Error") - ) - - with changing_dir(tmp_path): + with changing_dir(tmp_path), patch( + "fastapi_cloud_cli.utils.api.APIClient.stream_build_logs", + side_effect=error, + ): result = runner.invoke(app, ["deploy"]) assert result.exit_code == 1 @@ -815,10 +818,8 @@ def test_handles_build_log_streaming_error( @pytest.mark.respx(base_url=settings.base_api_url) -def test_shows_error_message_when_build_log_streaming_fails( - logged_in_cli: None, - tmp_path: Path, - respx_mock: respx.MockRouter, +def test_shows_error_message_on_build_log_http_error( + logged_in_cli: None, tmp_path: Path, respx_mock: respx.MockRouter ) -> None: app_data = _get_random_app() team_data = _get_random_team() @@ -853,6 +854,7 @@ def test_shows_error_message_when_build_log_streaming_fails( with changing_dir(tmp_path), patch("time.sleep"): result = runner.invoke(app, ["deploy"]) + assert result.exit_code == 1 assert "Unable to stream build logs" in result.output assert deployment_data["dashboard_url"] in result.output