Skip to content

Commit 95bc7eb

Browse files
authored
fix(cli): change deployment messages for detached deploys (#1138)
1 parent 1870dd7 commit 95bc7eb

2 files changed

Lines changed: 85 additions & 19 deletions

File tree

projects/fal/src/fal/cli/deploy.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def _deploy(args):
6262
deploy_check_source = _resolve_deploy_check_source(args, client)
6363
if deploy_check_source is not None:
6464
# The pre-deploy summary already shows the resolved app name.
65-
res = deploy_with_check(
65+
res, is_first_deploy = deploy_with_check(
6666
args,
6767
client,
6868
app_ref,
@@ -115,7 +115,7 @@ def _deploy(args):
115115
)
116116
raise
117117

118-
_render_deploy_result(args, res)
118+
_render_deploy_result(args, res, is_first_deploy=is_first_deploy, team=team)
119119

120120

121121
def _resolve_team_and_app_ref(args) -> tuple[str | None, tuple[str | None, str | None]]:
@@ -138,32 +138,90 @@ def _resolve_team_and_app_ref(args) -> tuple[str | None, tuple[str | None, str |
138138
return team, app_ref
139139

140140

141-
def _render_deploy_result(args, res) -> None:
141+
def _apps_command_hint(
142+
command: str, environment_name: str | None, team: str | None
143+
) -> str:
144+
# `fal apps ...` does not re-resolve the team or environment from
145+
# pyproject.toml, so hints have to carry what this deploy resolved.
146+
hint = f"fal apps {command}"
147+
if environment_name:
148+
hint += f" --env {environment_name}"
149+
if team:
150+
hint += f" --team {team}"
151+
return hint
152+
153+
154+
def _render_deploy_result(
155+
args, res, *, is_first_deploy: bool = False, team: str | None = None
156+
) -> None:
142157
app_id = res.revision
143158
resolved_app_name = res.app_name
144159

160+
# --detach only stops the CLI from following the rollout; the deployment is
161+
# still in progress when the command returns. A first deployment has no
162+
# revision to roll out from, so the flag is a no-op there.
163+
detached = args.attach_to_deployment is False and not is_first_deploy
164+
145165
if args.output == "json":
146166
args.console.print(
147-
json.dumps({"revision": app_id, "app_name": resolved_app_name})
167+
json.dumps(
168+
{
169+
"revision": app_id,
170+
"app_name": resolved_app_name,
171+
"status": "rolling_out" if detached else "deployed",
172+
}
173+
)
148174
)
149175
elif args.output == "pretty":
150176
from rich.text import Text
151177

152-
from fal.console.icons import get_check_icon, get_section_icon
178+
from fal.console.icons import (
179+
get_check_icon,
180+
get_section_icon,
181+
get_status_progress_icon,
182+
)
153183
from fal.console.rules import print_rule
154184
from fal.flags import URL_OUTPUT
155185

156-
check_icon = get_check_icon(args.console)
157186
section_icon = get_section_icon(args.console)
158-
args.console.print(
159-
f"{check_icon} Deployed successfully",
160-
style="bold green",
161-
)
187+
if detached:
188+
progress_icon = get_status_progress_icon(args.console)
189+
args.console.print(
190+
f"{progress_icon} Deployment started (detached)",
191+
style="bold yellow",
192+
)
193+
# A detached rolling deployment moves the alias first and brings the
194+
# new revision's runners up afterwards, with no automatic rollback.
195+
args.console.print(
196+
"The rollout is still in progress: the new revision already "
197+
"receives traffic while its runners start up, and a failed rollout "
198+
"is not rolled back automatically.",
199+
)
200+
else:
201+
check_icon = get_check_icon(args.console)
202+
args.console.print(
203+
f"{check_icon} Deployed successfully",
204+
style="bold green",
205+
)
162206
args.console.print("")
163207

164208
# Build panel content with grouped sections
165209
lines = Text()
166210

211+
if detached:
212+
lines.append(f"{section_icon} Status: rolling out ", style="bold")
213+
lines.append(f"(revision {app_id})\n", style="dim")
214+
progress_hint = _apps_command_hint(
215+
f"runners {resolved_app_name}", args.env, team
216+
)
217+
rollback_hint = _apps_command_hint(
218+
f"set-rev {resolved_app_name} <revision>", args.env, team
219+
)
220+
lines.append(" Progress: ", style="dim")
221+
lines.append(f"{progress_hint}\n", style="cyan")
222+
lines.append(" Roll back: ", style="dim")
223+
lines.append(f"{rollback_hint}\n\n", style="cyan")
224+
167225
# Auth mode section
168226
AUTH_EXPLANATIONS = {
169227
"public": "no authentication required",
@@ -198,9 +256,10 @@ def _render_deploy_result(args, res) -> None:
198256
lines.append(f" {res.log_url}", style="cyan")
199257

200258
title = Text(resolved_app_name, style="bold")
201-
print_rule(args.console, title, style="green")
259+
rule_style = "yellow" if detached else "green"
260+
print_rule(args.console, title, style=rule_style)
202261
args.console.print(lines)
203-
print_rule(args.console, "", style="green")
262+
print_rule(args.console, "", style=rule_style)
204263

205264
# Reminder about scaling parameter inheritance
206265
if not args.app_scale_settings:
@@ -292,7 +351,7 @@ def valid_auth_option(option):
292351
action="store_true",
293352
dest="attach_to_deployment",
294353
help=(
295-
"Attach to the deployment process. "
354+
"Attach to the deployment process and wait for the rollout to finish. "
296355
"Only applies when --strategy is rolling (the default)."
297356
),
298357
)
@@ -301,8 +360,10 @@ def valid_auth_option(option):
301360
action="store_false",
302361
dest="attach_to_deployment",
303362
help=(
304-
"Do not attach to the deployment process. "
305-
"Only applies when --strategy is rolling (the default)."
363+
"Do not attach to the deployment process: return as soon as the new "
364+
"revision is registered, while the rollout continues in the background. "
365+
"Only applies when --strategy is rolling (the default), and has no "
366+
"effect on the first deployment of an app (nothing to roll out from)."
306367
),
307368
)
308369
parser.set_defaults(attach_to_deployment=None)

projects/fal/src/fal/cli/deploy_check.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ def deploy_with_check(
113113
prepare_options_handler: ProgressCallback | None = None,
114114
message: str | None = None,
115115
annotations: dict[str, str] | None = None,
116-
) -> DeploymentResult:
116+
) -> tuple[DeploymentResult, bool]:
117+
"""Deploy after the pre-deploy summary. Returns the result and whether this
118+
was the app's first deployment."""
117119
from fal.api import deploy as deploy_api
118120
from fal.api.deploy import _validate_attach_to_deployment
119121

@@ -153,12 +155,13 @@ def deploy_with_check(
153155
assume_yes=args.yes,
154156
)
155157
try:
156-
return deploy_api.execute_prepared_deployment(
158+
result = deploy_api.execute_prepared_deployment(
157159
prepared,
158160
result_handler=result_handler,
159161
build_result_handler=build_result_handler,
160162
prepare_options_handler=prepare_options_handler,
161163
)
164+
return result, is_first_deploy
162165
except Exception:
163166
print_deploy_failure_nudge(
164167
args.console, run_hint, already_nudged=is_first_deploy
@@ -206,8 +209,10 @@ def is_first_deployment(
206209
) -> bool:
207210
"""Best-effort check for whether ``app_name`` has no production alias yet.
208211
209-
Returns ``False`` (i.e. does not nudge) if the status can't be determined,
210-
so a flaky lookup never blocks or misleads a deploy.
212+
Drives the first-deploy nudge and the post-deploy summary (a first deploy
213+
has no revision to roll out from, so ``--detach`` is a no-op there).
214+
Returns ``False`` if the status can't be determined, so a flaky lookup
215+
never blocks a deploy and never hides an in-progress rollout.
211216
"""
212217
if not app_name:
213218
return False

0 commit comments

Comments
 (0)