Skip to content

Commit b30c8ef

Browse files
committed
✨ Wait for verification after building
Shortcake-Parent: update-how-we-show-messages
1 parent 9f24d83 commit b30c8ef

File tree

2 files changed

+320
-60
lines changed

2 files changed

+320
-60
lines changed

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 126 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
import fastar
1414
import rignore
1515
import typer
16-
from httpx import Client
16+
from httpx import Client, HTTPError
1717
from pydantic import AfterValidator, BaseModel, EmailStr, TypeAdapter, ValidationError
1818
from rich.text import Text
1919
from rich_toolkit import RichToolkit
2020
from rich_toolkit.menu import Option
21+
from rich_toolkit.progress import Progress
2122

2223
from fastapi_cloud_cli.commands.login import login
2324
from fastapi_cloud_cli.utils.api import APIClient, StreamLogError, TooManyRetriesError
@@ -210,6 +211,17 @@ def to_human_readable(cls, status: "DeploymentStatus") -> str:
210211
}[status]
211212

212213

214+
SUCCESSFUL_STATUSES = {DeploymentStatus.success, DeploymentStatus.verifying_skipped}
215+
FAILED_STATUSES = {
216+
DeploymentStatus.failed,
217+
DeploymentStatus.verifying_failed,
218+
DeploymentStatus.deploying_failed,
219+
DeploymentStatus.building_image_failed,
220+
DeploymentStatus.extracting_failed,
221+
}
222+
TERMINAL_STATUSES = SUCCESSFUL_STATUSES | FAILED_STATUSES
223+
224+
213225
class CreateDeploymentResponse(BaseModel):
214226
id: str
215227
app_id: str
@@ -440,6 +452,82 @@ def _configure_app(toolkit: RichToolkit, path_to_deploy: Path) -> AppConfig:
440452
return app_config
441453

442454

455+
POLL_INTERVAL = 2.0
456+
POLL_TIMEOUT = 120.0
457+
458+
459+
def _poll_deployment_status(
460+
client: APIClient,
461+
app_id: str,
462+
deployment_id: str,
463+
timeout: float = POLL_TIMEOUT,
464+
interval: float = POLL_INTERVAL,
465+
) -> DeploymentStatus:
466+
start = time.monotonic()
467+
consecutive_errors = 0
468+
max_errors = 5
469+
470+
while time.monotonic() - start < timeout:
471+
try:
472+
response = client.get(f"/apps/{app_id}/deployments/{deployment_id}")
473+
response.raise_for_status()
474+
status = DeploymentStatus(response.json()["status"])
475+
consecutive_errors = 0
476+
except HTTPError as e:
477+
consecutive_errors += 1
478+
logger.debug("Error polling deployment status: %s", e)
479+
if consecutive_errors >= max_errors:
480+
raise
481+
time.sleep(interval)
482+
continue
483+
484+
if status in TERMINAL_STATUSES:
485+
return status
486+
487+
time.sleep(interval)
488+
489+
raise TimeoutError("Deployment verification timed out")
490+
491+
492+
def _verify_deployment(
493+
toolkit: RichToolkit,
494+
client: APIClient,
495+
app_id: str,
496+
deployment: CreateDeploymentResponse,
497+
) -> None:
498+
with Progress(
499+
title="Verifying deployment...",
500+
console=toolkit.console,
501+
style=toolkit.style,
502+
inline_logs=True,
503+
) as progress:
504+
progress.metadata["done_emoji"] = "✅"
505+
try:
506+
final_status = _poll_deployment_status(client, app_id, deployment.id)
507+
except TimeoutError:
508+
progress.metadata["done_emoji"] = "⚠️"
509+
progress.log(
510+
f"Could not confirm deployment status. "
511+
f"Check the dashboard: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
512+
)
513+
return
514+
515+
if final_status in SUCCESSFUL_STATUSES:
516+
progress.title = "Deployment verified!"
517+
progress.log(
518+
f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
519+
)
520+
else:
521+
progress.metadata["done_emoji"] = "❌"
522+
progress.title = "Deployment failed"
523+
human_status = DeploymentStatus.to_human_readable(final_status)
524+
progress.log(
525+
f"😔 Oh no! Deployment failed: {human_status}. "
526+
f"Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
527+
)
528+
raise typer.Exit(1)
529+
530+
443531
def _wait_for_deployment(
444532
toolkit: RichToolkit, app_id: str, deployment: CreateDeploymentResponse
445533
) -> None:
@@ -451,71 +539,61 @@ def _wait_for_deployment(
451539
)
452540
toolkit.print_line()
453541

454-
toolkit.print(
455-
f"You can also check the status at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]",
456-
)
457-
toolkit.print_line()
458-
459542
time_elapsed = 0.0
460543

461544
started_at = time.monotonic()
462545

463546
last_message_changed_at = time.monotonic()
464547

465-
with (
466-
toolkit.progress(
467-
next(messages), inline_logs=True, lines_to_show=20
468-
) as progress,
469-
APIClient() as client,
470-
):
471-
progress.metadata["done_emoji"] = "🚀"
472-
try:
473-
for log in client.stream_build_logs(deployment.id):
474-
time_elapsed = time.monotonic() - started_at
548+
build_complete = False
475549

476-
if log.type == "message":
477-
progress.log(Text.from_ansi(log.message.rstrip()))
550+
with APIClient() as client:
551+
with toolkit.progress(
552+
next(messages), inline_logs=True, lines_to_show=20
553+
) as progress:
554+
progress.metadata["done_emoji"] = "🚀"
555+
try:
556+
for log in client.stream_build_logs(deployment.id):
557+
time_elapsed = time.monotonic() - started_at
478558

479-
if log.type == "complete":
480-
progress.title = "Build complete!"
481-
progress.log("")
482-
progress.log(
483-
f"You can also check the app logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
484-
)
559+
if log.type == "message":
560+
progress.log(Text.from_ansi(log.message.rstrip()))
485561

486-
progress.log("")
562+
if log.type == "complete":
563+
build_complete = True
564+
progress.title = "Build complete!"
565+
break
487566

488-
progress.log(
489-
f"🐔 Ready the chicken! Your app is ready at [link={deployment.url}]{deployment.url}[/link]"
490-
)
567+
if log.type == "failed":
568+
progress.log("")
569+
progress.log(
570+
f"😔 Oh no! Something went wrong. Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
571+
)
572+
raise typer.Exit(1)
491573

492-
break
574+
if time_elapsed > 30:
575+
messages = cycle(LONG_WAIT_MESSAGES)
493576

494-
if log.type == "failed":
495-
progress.log("")
496-
progress.log(
497-
f"😔 Oh no! Something went wrong. Check out the logs at [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]"
498-
)
499-
raise typer.Exit(1)
577+
if (time.monotonic() - last_message_changed_at) > 2:
578+
progress.title = next(messages)
500579

501-
if time_elapsed > 30:
502-
messages = cycle(LONG_WAIT_MESSAGES)
580+
last_message_changed_at = time.monotonic()
503581

504-
if (time.monotonic() - last_message_changed_at) > 2:
505-
progress.title = next(messages)
582+
except (StreamLogError, TooManyRetriesError, TimeoutError) as e:
583+
progress.set_error(
584+
dedent(f"""
585+
[error]Build log streaming failed: {e}[/]
506586
507-
last_message_changed_at = time.monotonic()
587+
Unable to stream build logs. Check the dashboard for status: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]
588+
""").strip()
589+
)
508590

509-
except (StreamLogError, TooManyRetriesError, TimeoutError) as e:
510-
progress.set_error(
511-
dedent(f"""
512-
[error]Build log streaming failed: {e}[/]
591+
raise typer.Exit(1) from None
513592

514-
Unable to stream build logs. Check the dashboard for status: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]
515-
""").strip()
516-
)
593+
if build_complete:
594+
toolkit.print_line()
517595

518-
raise typer.Exit(1) from None
596+
_verify_deployment(toolkit, client, app_id, deployment)
519597

520598

521599
class SignupToWaitingList(BaseModel):

0 commit comments

Comments
 (0)