@@ -31,10 +31,8 @@ use crate::error::{ErrorCategory, HmError};
3131/// the backend rejects the build, authentication fails, the network is
3232/// unreachable, the local daemon is down, or the pipeline fails to render.
3333pub async fn handle ( args : RunArgs , ctx : RunContext ) -> Result < i32 > {
34- // 1. Build the backend. Cloud needs auth + org resolution BEFORE any
35- // (local) render work — fail fast on a missing token.
36- // Resolution: explicit --backend > legacy --cloud alias > config.backend
37- // (figment-layered default "docker").
34+ // 1. Resolve the backend name: explicit --backend > legacy --cloud alias >
35+ // config.backend (figment-layered default "docker").
3836 let backend_name = args
3937 . backend
4038 . clone ( )
@@ -47,7 +45,13 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
4745 } )
4846 . unwrap_or_else ( || ctx. config . backend . clone ( ) ) ;
4947
50- let backend: Box < dyn hm_exec:: ExecutionBackend > = if backend_name == "cloud" {
48+ // 2. Cloud needs auth + org resolution up front — fail fast on a missing
49+ // token before any render work. We resolve the credentials here but
50+ // defer *constructing* the backend (and, for local runs, *connecting* to
51+ // Docker) until after the pipeline renders, so an unknown slug or a
52+ // missing/ambiguous pipeline argument fails with a helpful message
53+ // instead of a daemon-connection error.
54+ let cloud_creds = if backend_name == "cloud" {
5155 let api_url = ctx. config . cloud . api_url . clone ( ) ;
5256 let token = hm_config:: creds:: cloud_token ( & api_url) . context (
5357 "`hm run --backend cloud` requires authentication — run `hm cloud login` or set HARMONT_API_TOKEN" ,
@@ -57,23 +61,43 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
5761 . clone ( )
5862 . or_else ( || ctx. config . cloud . org . clone ( ) )
5963 . context ( "no organization — pass --org or set `[cloud] org = \" …\" ` in .hm/config.toml or ~/.config/hm/config.toml" ) ?;
60- let client = harmont_cloud:: HarmontClient :: with_base_url ( token, & api_url) ;
61- Box :: new ( hm_exec:: CloudBackend :: new ( client, api_url, org) )
64+ Some ( ( api_url, token, org) )
65+ } else if backend_name != "docker" {
66+ anyhow:: bail!( "unknown --backend '{backend_name}'\n available: docker, cloud" ) ;
6267 } else {
63- // Local execution on a hm-vm VmBackend, selected by name.
64- let vm_backend: std:: sync:: Arc < dyn hm_vm:: VmBackend > = match backend_name. as_str ( ) {
65- "docker" => std:: sync:: Arc :: new (
68+ None
69+ } ;
70+
71+ // 3. Render + parse the plan once (shared by every backend). This validates
72+ // the pipeline argument — unknown slug, or zero/many declared pipelines
73+ // — before we connect to any daemon.
74+ let ( repo_root, slug, ir_json) = render_pipeline ( & args, & ctx) . await ?;
75+ let plan = hm_exec:: Plan :: parse ( ir_json) . map_err ( |e| backend_anyhow ( & e) ) ?;
76+
77+ // 4. Pick the renderer — this validates `--format` — before any daemon
78+ // connection, so an unknown format fails fast without a running Docker.
79+ let use_logs = args. logs
80+ || std:: env:: var_os ( "CI" ) . is_some_and ( |v| !v. is_empty ( ) )
81+ || !hm_render:: stderr_interactive ( ) ;
82+ let renderer = hm_render:: renderer_for ( & args. format , ctx. output . color_enabled ( ) , use_logs) ?;
83+
84+ // 5. Build the backend. For local runs this is where we connect to Docker.
85+ let backend: Box < dyn hm_exec:: ExecutionBackend > =
86+ if let Some ( ( api_url, token, org) ) = cloud_creds {
87+ let client = harmont_cloud:: HarmontClient :: with_base_url ( token, & api_url) ;
88+ Box :: new ( hm_exec:: CloudBackend :: new ( client, api_url, org) )
89+ } else {
90+ // Local execution on a hm-vm VmBackend (docker).
91+ let vm_backend: std:: sync:: Arc < dyn hm_vm:: VmBackend > = std:: sync:: Arc :: new (
6692 hm_vm:: docker:: DockerBackend :: connect ( ) . map_err ( |e| anyhow:: anyhow!( "{e:#}" ) ) ?,
67- ) ,
68- other => anyhow:: bail!( "unknown --backend '{other}'\n available: docker, cloud" ) ,
93+ ) ;
94+ Box :: new ( hm_exec:: LocalBackend :: new (
95+ resolve_parallelism ( & args) ,
96+ vm_backend,
97+ ) )
6998 } ;
70- Box :: new ( hm_exec:: LocalBackend :: new (
71- resolve_parallelism ( & args) ,
72- vm_backend,
73- ) )
74- } ;
7599
76- // 2 . Capability-driven flag validation (replaces the old silent ignoring).
100+ // 6 . Capability-driven flag validation (replaces the old silent ignoring).
77101 let caps = backend. capabilities ( ) ;
78102 if args. no_watch && !caps. supports_no_watch {
79103 anyhow:: bail!(
@@ -94,9 +118,7 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
94118 ) ;
95119 }
96120
97- // 3. Render + parse the plan once (shared by every backend).
98- let ( repo_root, slug, ir_json) = render_pipeline ( & args, & ctx) . await ?;
99- let plan = hm_exec:: Plan :: parse ( ir_json) . map_err ( |e| backend_anyhow ( & e) ) ?;
121+ // 7. Assemble the run request.
100122 let ( branch, commit) = git_metadata ( & repo_root, args. branch . clone ( ) ) ;
101123 let req = hm_exec:: RunRequest {
102124 plan,
@@ -116,13 +138,7 @@ pub async fn handle(args: RunArgs, ctx: RunContext) -> Result<i32> {
116138 } ,
117139 } ;
118140
119- // 4. Renderer selection (unchanged): logs stream in CI or with --logs.
120- let use_logs = args. logs
121- || std:: env:: var_os ( "CI" ) . is_some_and ( |v| !v. is_empty ( ) )
122- || !hm_render:: stderr_interactive ( ) ;
123- let renderer = hm_render:: renderer_for ( & args. format , ctx. output . color_enabled ( ) , use_logs) ?;
124-
125- // 5. Start, drive events, own Ctrl-C, await the outcome.
141+ // 8. Start, drive events, own Ctrl-C, await the outcome.
126142 let handle = backend. start ( req) . await . map_err ( |e| backend_anyhow ( & e) ) ?;
127143 let ( events, control) = handle. into_parts ( ) ;
128144 let _ctrlc = crate :: signal:: install_ctrlc ( control. cancel_token ( ) ) ;
0 commit comments