Skip to content

Commit b23d6e0

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

2 files changed

Lines changed: 319 additions & 59 deletions

File tree

src/fastapi_cloud_cli/commands/deploy.py

Lines changed: 125 additions & 47 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,73 +539,63 @@ 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(
548+
build_complete = False
549+
550+
with APIClient() as client:
551+
with toolkit.progress(
467552
next(messages),
468553
inline_logs=True,
469554
lines_to_show=20,
470555
done_emoji="🚀",
471-
) as progress,
472-
APIClient() as client,
473-
):
474-
try:
475-
for log in client.stream_build_logs(deployment.id):
476-
time_elapsed = time.monotonic() - started_at
477-
478-
if log.type == "message":
479-
progress.log(Text.from_ansi(log.message.rstrip()))
556+
) as progress:
557+
try:
558+
for log in client.stream_build_logs(deployment.id):
559+
time_elapsed = time.monotonic() - started_at
480560

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

488-
progress.log("")
564+
if log.type == "complete":
565+
build_complete = True
566+
progress.title = "Build complete!"
567+
break
489568

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

494-
break
576+
if time_elapsed > 30:
577+
messages = cycle(LONG_WAIT_MESSAGES)
495578

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

503-
if time_elapsed > 30:
504-
messages = cycle(LONG_WAIT_MESSAGES)
582+
last_message_changed_at = time.monotonic()
505583

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

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

516-
Unable to stream build logs. Check the dashboard for status: [link={deployment.dashboard_url}]{deployment.dashboard_url}[/link]
517-
""").strip()
518-
)
595+
if build_complete:
596+
toolkit.print_line()
519597

520-
raise typer.Exit(1) from None
598+
_verify_deployment(toolkit, client, app_id, deployment)
521599

522600

523601
class SignupToWaitingList(BaseModel):

0 commit comments

Comments
 (0)