@@ -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
121121def _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 )
0 commit comments