Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/fastapi_cloud_cli/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
from ._errors import get_http_error_code as get_http_error_code
from ._errors import get_http_error_hint as get_http_error_hint
from ._errors import handle_http_error as handle_http_error
from ._models import BUILD_FAILED_STATUSES as BUILD_FAILED_STATUSES
from ._models import SUCCESSFUL_STATUSES as SUCCESSFUL_STATUSES
from ._models import TERMINAL_STATUSES as TERMINAL_STATUSES
from ._models import AppLogEntry as AppLogEntry
from ._models import BuildFailure as BuildFailure
from ._models import BuildLogLineGeneric as BuildLogLineGeneric
from ._models import BuildLogLineMessage as BuildLogLineMessage
from ._models import CustomDomain as CustomDomain
from ._models import CustomDomainRecord as CustomDomainRecord
from ._models import CustomDomainStatus as CustomDomainStatus
from ._models import Deployment as Deployment
from ._models import DeploymentStatus as DeploymentStatus
from ._retry import STREAM_LOGS_MAX_RETRIES as STREAM_LOGS_MAX_RETRIES
from .client import APIClient as APIClient
30 changes: 25 additions & 5 deletions src/fastapi_cloud_cli/api/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ class BuildLogLineMessage(BaseModel):
id: str | None = None


class BuildFailure(BaseModel):
error_code: str
error_title: str
error_message: str
error_hint: str


BuildLogLine = BuildLogLineMessage | BuildLogLineGeneric
BuildLogAdapter: TypeAdapter[BuildLogLine] = TypeAdapter(
Annotated[BuildLogLine, Field(discriminator="type")]
Expand Down Expand Up @@ -125,16 +132,29 @@ def to_human_readable(cls, status: "DeploymentStatus") -> str:
}[status]


class Deployment(BaseModel):
id: str
app_id: str
slug: str
status: DeploymentStatus
created_at: str
url: str | None = None
dashboard_url: str | None = None
failure: BuildFailure | None = None


SUCCESSFUL_STATUSES = {DeploymentStatus.success, DeploymentStatus.verifying_skipped}
FAILED_STATUSES = {
BUILD_FAILED_STATUSES = {
DeploymentStatus.building_image_failed,
DeploymentStatus.building_image_failed_timeout,
DeploymentStatus.extracting_failed,
DeploymentStatus.extracting_failed_archive_too_large,
}
FAILED_STATUSES = BUILD_FAILED_STATUSES | {
DeploymentStatus.failed,
DeploymentStatus.verifying_failed,
DeploymentStatus.verification_failed_oom,
DeploymentStatus.deploying_failed,
DeploymentStatus.deploying_skipped,
DeploymentStatus.building_image_failed,
DeploymentStatus.building_image_failed_timeout,
DeploymentStatus.extracting_failed,
DeploymentStatus.extracting_failed_archive_too_large,
}
TERMINAL_STATUSES = SUCCESSFUL_STATUSES | FAILED_STATUSES
6 changes: 6 additions & 0 deletions src/fastapi_cloud_cli/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
BuildLogLine,
CustomDomain,
CustomDomainsAPIResponse,
Deployment,
DeploymentStatus,
)
from ._retry import (
Expand Down Expand Up @@ -136,6 +137,11 @@ def handle_http_errors(

raise typer.Exit(1) from None

def get_deployment(self, deployment_id: str) -> Deployment:
response = self.get(f"/deployments/{deployment_id}")
response.raise_for_status()
return Deployment.model_validate(response.json())

def get_custom_domains(self, *, app_id: str) -> CustomDomainsAPIResponse:
response = self.get(f"/apps/{app_id}/custom-domains")
response.raise_for_status()
Expand Down
29 changes: 25 additions & 4 deletions src/fastapi_cloud_cli/commands/deploy/wait.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
import contextlib
import time
from itertools import cycle
from textwrap import dedent

import typer
from httpx import HTTPError
from rich.text import Text
from rich_toolkit import RichToolkit

from fastapi_cloud_cli.api import (
SUCCESSFUL_STATUSES,
APIClient,
BuildFailure,
DeploymentStatus,
StreamLogError,
TooManyRetriesError,
)
from fastapi_cloud_cli.commands.deploy.cloud import CreateDeploymentResponse
from fastapi_cloud_cli.utils.build_logs import print_build_error
from fastapi_cloud_cli.utils.cli import FastAPIRichToolkit
from fastapi_cloud_cli.utils.execution import is_ci_enabled

# (bullet emoji, message) — the emoji replaces the progress animation
WAITING_MESSAGES = [
Expand Down Expand Up @@ -89,7 +95,7 @@ def _verify_deployment(


def _wait_for_deployment(
toolkit: RichToolkit,
toolkit: FastAPIRichToolkit,
client: APIClient,
app_id: str,
deployment: CreateDeploymentResponse,
Expand All @@ -107,12 +113,15 @@ def _wait_for_deployment(
"Checking the status of your deployment",
inline_logs=True,
lines_to_show=20,
# Keep the log panel replaceable by a diagnosis, but retain CI logs.
preserve_logs=is_ci_enabled(),
emoji="👀",
done_emoji="🚀",
) as progress,
):
build_complete = False
build_failed = False
build_error: BuildFailure | None = None

try:
for log in client.stream_build_logs(deployment.id):
Expand All @@ -128,6 +137,12 @@ def _wait_for_deployment(

if log.type == "failed":
build_failed = True
build_error = None

with contextlib.suppress(HTTPError):
build_error = client.get_deployment(deployment.id).failure

progress.transient = build_error is not None
# the headline comes from the title once there are log
# lines, and from current_message when there are none
progress.title = "Build failed"
Expand Down Expand Up @@ -157,10 +172,16 @@ def _wait_for_deployment(
raise typer.Exit(1) from None

if build_failed:
toolkit.print_line()
if build_error is not None:
print_build_error(toolkit, build_error)
toolkit.print_line()
message = "Check out the logs at"
else:
toolkit.print_line()
message = "Oh no! Something went wrong. Check out the logs at"
toolkit.print(
f"Oh no! Something went wrong. Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]",
emoji="😔",
f"{message} [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]",
emoji="👀" if build_error is not None else "😔",
)
raise typer.Exit(1)

Expand Down
Loading