1use crate::{
2 canister_build::cache::DefaultCanisterBuildCacheCleanup,
3 deployment_truth::DeploymentReceiptV1,
4 fleet_install_input::{ResolvedFleetInstallInput, load_and_resolve_fleet_install_input},
5 fleet_install_plan::{
6 FleetInstallPlanRequest, PersistedFleetInstallPlan, compile_and_persist_fleet_install_plan,
7 },
8 network::resolve_canonical_network_id_from_root,
9 release_set::{AppConfigSnapshot, icp_root, workspace_root},
10};
11use config_selection::resolve_install_config_path;
12use std::{
13 fmt,
14 path::{Path, PathBuf},
15 time::{Duration, Instant},
16};
17use thiserror::Error as ThisError;
18
19mod build_network;
20mod build_snapshot;
21mod build_targets;
22mod capabilities;
23mod clock;
24mod commands;
25mod config_selection;
26mod coordinator_install;
27mod coordinator_install_journal;
28mod current_execution;
29mod deployment_truth_gate;
30mod execution_preflight;
31mod fleet_catalog_closeout;
32mod fleet_catalog_publication;
33mod fleet_install_session;
34mod fleet_registry_activation;
35mod fleet_registry_activation_journal;
36mod fleet_subnet_root_component_registry_preparation;
37mod fleet_subnet_root_install;
38mod fleet_subnet_root_install_journal;
39mod fleet_subnet_root_registry_join;
40mod fleet_subnet_root_registry_mirror_activation;
41mod fleet_subnet_root_registry_sync;
42mod fleet_subnet_root_runtime_activation;
43mod fleet_subnet_root_store_bootstrap;
44mod identity;
45mod operations;
46mod options;
47mod output;
48mod phase_receipts;
49mod plan_artifacts;
50mod preparation;
51mod receipt_io;
52mod timing;
53mod truth_check;
54
55use crate::release_build::{PlannedReleaseBuild, ReleaseBuildPlanError, plan_release_build};
56use build_network::resolve_install_build_context;
57use build_snapshot::resolve_install_snapshot;
58pub use config_selection::{
59 ConfigDiscoveryError, current_canic_workspace_root, discover_canic_config_choices,
60 discover_canic_workspace_root_from, discover_workspace_canic_config_choices,
61 select_discovered_app_config_path, workspace_app_roots,
62};
63use coordinator_install::install_and_verify_fleet_coordinator;
64use fleet_catalog_closeout::{
65 PublishInstalledFleetCatalogRequest, publish_installed_fleet_catalog,
66};
67use fleet_registry_activation::{ActivateFleetRegistryRequest, activate_and_verify_fleet_registry};
68use fleet_subnet_root_component_registry_preparation::{
69 PrepareFleetSubnetRootComponentRegistriesRequest,
70 prepare_and_verify_fleet_subnet_root_component_registries,
71};
72use fleet_subnet_root_install::install_and_verify_fleet_subnet_roots;
73use fleet_subnet_root_registry_join::register_and_verify_fleet_subnet_roots_joining;
74use fleet_subnet_root_registry_mirror_activation::{
75 ActivateFleetSubnetRootRegistryMirrorsRequest,
76 activate_and_verify_fleet_subnet_root_registry_mirrors,
77};
78use fleet_subnet_root_registry_sync::{
79 SynchronizeFleetSubnetRootsRequest, synchronize_and_verify_fleet_subnet_roots,
80};
81use fleet_subnet_root_runtime_activation::{
82 ActivateFleetSubnetRootRuntimesRequest, activate_and_verify_fleet_subnet_root_runtimes,
83};
84use fleet_subnet_root_store_bootstrap::bootstrap_and_verify_fleet_subnet_root_stores;
85use identity::resolve_install_identity;
86pub use options::InstallRootOptions;
87use output::print_install_timing_summary;
88use phase_receipts::{
89 CompletedInstallPhase, InstallReceiptScope, write_completed_install_phase_receipt,
90};
91use plan_artifacts::emit_manifest_with_phase;
92use preparation::prepare_install_deployment_truth;
93pub use receipt_io::latest_deployment_truth_receipt_path_from_root;
94use timing::InstallTimingSummary as CurrentInstallTimingSummary;
95pub use truth_check::{check_install_deployment_truth, check_install_execution_preflight};
96
97fn install_icp(
98 icp_root: &Path,
99 environment: &str,
100 local_replica: Option<&crate::icp::LocalReplicaTarget>,
101) -> crate::icp::IcpCli {
102 crate::icp::IcpCli::new("icp", Some(environment.to_string()))
103 .with_cwd(icp_root)
104 .with_local_replica(local_replica.cloned())
105}
106
107#[cfg(test)]
108mod tests;
109
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum InstallRootBlockKind {
118 DeploymentExecutionPreflight,
119 DeploymentTruth,
120}
121
122#[derive(Debug, ThisError)]
129#[error("{message}")]
130pub struct InstallRootBlockedError {
131 kind: InstallRootBlockKind,
132 message: String,
133}
134
135impl InstallRootBlockedError {
136 pub(super) const fn new(kind: InstallRootBlockKind, message: String) -> Self {
137 Self { kind, message }
138 }
139
140 #[must_use]
141 pub const fn kind(&self) -> InstallRootBlockKind {
142 self.kind
143 }
144}
145
146#[derive(Clone, Copy, Debug, Eq, PartialEq)]
148pub enum InstallRootPhase {
149 WorkspaceDiscovery,
150 IcpProjectDiscovery,
151 Configuration,
152 BuildInputs,
153 Identity,
154 Preparation,
155 Manifest,
156 Planning,
157 Activation,
158}
159
160impl fmt::Display for InstallRootPhase {
161 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
162 formatter.write_str(match self {
163 Self::WorkspaceDiscovery => "workspace discovery",
164 Self::IcpProjectDiscovery => "ICP project discovery",
165 Self::Configuration => "configuration selection",
166 Self::BuildInputs => "build input validation",
167 Self::Identity => "deployment identity resolution",
168 Self::Preparation => "deployment preparation",
169 Self::Manifest => "manifest emission",
170 Self::Planning => "Fleet installation planning",
171 Self::Activation => "root activation",
172 })
173 }
174}
175
176#[derive(Debug, ThisError)]
178#[error("root install failed during {phase}: {source}")]
179pub struct InstallRootError {
180 phase: InstallRootPhase,
181 #[source]
182 source: Box<dyn std::error::Error>,
183}
184
185impl InstallRootError {
186 pub fn new<E>(phase: InstallRootPhase, source: E) -> Self
188 where
189 E: std::error::Error + 'static,
190 {
191 Self {
192 phase,
193 source: Box::new(source),
194 }
195 }
196
197 fn in_phase(phase: InstallRootPhase) -> impl FnOnce(Box<dyn std::error::Error>) -> Self {
198 move |source| Self { phase, source }
199 }
200
201 #[must_use]
202 pub const fn phase(&self) -> InstallRootPhase {
203 self.phase
204 }
205}
206
207#[derive(Debug, ThisError)]
208#[error("fresh Fleet installation requires --fleet-input <PATH>")]
209struct MissingFleetInstallInputError;
210
211pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, ConfigDiscoveryError> {
213 let workspace_root = current_canic_workspace_root()?;
214 let choices = config_selection::discover_workspace_canic_config_choices(&workspace_root)?;
215 if !choices.is_empty() {
216 return Ok(choices);
217 }
218
219 let icp_root = icp_root()?;
220 if icp_root != workspace_root {
221 return config_selection::discover_workspace_canic_config_choices(&icp_root);
222 }
223
224 Ok(choices)
225}
226
227pub fn install_root(options: InstallRootOptions) -> Result<(), InstallRootError> {
229 let (workspace_root, icp_root) = resolve_current_install_roots(&options)?;
230 let _build_cache_cleanup = DefaultCanisterBuildCacheCleanup::for_install(&workspace_root);
231 let config_path = current_install_config_path(&icp_root, &options)?;
232 let (build_context, install_snapshot) =
233 current_install_build_inputs(&workspace_root, &icp_root, &config_path, &options)
234 .map_err(InstallRootError::in_phase(InstallRootPhase::BuildInputs))?;
235 let (app_id, fleet_name) =
236 resolve_install_identity(&options, &config_path, &install_snapshot.app_id)
237 .map_err(InstallRootError::in_phase(InstallRootPhase::Identity))?;
238 let total_started_at = Instant::now();
239 let mut timings = CurrentInstallTimingSummary::default();
240 let environment = options.environment.as_str();
241 let artifact_root = if options.deployment_plan_override.is_some() {
242 crate::release_set::artifact_root_path(&icp_root, options.artifact_environment())
243 } else {
244 build_context.artifact_root()
245 };
246 let execution_context = current_execution::current_install_execution_context_at_root(
247 &workspace_root,
248 &icp_root,
249 &artifact_root,
250 );
251 let resolved_fleet_install_input =
252 resolve_current_fleet_install_input(&icp_root, environment, &options)
253 .map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
254
255 print_install_identity(&app_id, &fleet_name);
256 let prepared = prepare_install_deployment_truth(
257 &options,
258 &icp_root,
259 &config_path,
260 &fleet_name,
261 &execution_context,
262 &build_context,
263 &install_snapshot,
264 )
265 .map_err(InstallRootError::in_phase(InstallRootPhase::Preparation))?;
266 timings.build_all = prepared.timings.build_all;
267 let emitted_manifest = emit_manifest_with_phase(
268 &icp_root,
269 &install_snapshot,
270 &prepared.build_outputs,
271 &prepared.infrastructure_build_outputs,
272 prepared.plan_artifacts.as_ref(),
273 )
274 .map_err(InstallRootError::in_phase(InstallRootPhase::Manifest))?;
275 timings.emit_manifest = emitted_manifest.duration;
276 let finalized_release_build =
277 require_finalized_release_build(emitted_manifest.finalized_release_build)?;
278 let planned_install = plan_current_fleet_install(
279 &icp_root,
280 environment,
281 &fleet_name,
282 &app_id,
283 &config_path,
284 &finalized_release_build,
285 resolved_fleet_install_input,
286 )?;
287 let receipt_scope = InstallReceiptScope {
288 icp_root: &icp_root,
289 fleet: planned_install.fleet(),
290 check: &prepared.deployment_truth_check,
291 execution_context: Some(&execution_context),
292 };
293 persist_current_pre_root_receipts(
294 receipt_scope,
295 &prepared.pre_activation_receipts,
296 prepared.build_phase,
297 emitted_manifest.phase,
298 )?;
299 install_current_fleet_infrastructure(
300 &icp_root,
301 environment,
302 build_context.local_replica.as_ref(),
303 &config_path,
304 &planned_install,
305 &mut timings,
306 )?;
307
308 print_install_timing_summary(&timings, total_started_at.elapsed());
309 Ok(())
310}
311
312fn install_current_fleet_infrastructure(
313 icp_root: &Path,
314 environment: &str,
315 local_replica: Option<&crate::icp::LocalReplicaTarget>,
316 config_path: &Path,
317 planned: &PlannedCurrentFleetInstall,
318 timings: &mut CurrentInstallTimingSummary,
319) -> Result<(), InstallRootError> {
320 let (coordinator, coordinator_duration) = install_current_fleet_coordinator(
321 icp_root,
322 environment,
323 local_replica,
324 config_path,
325 &planned.plan,
326 )?;
327 timings.create_canisters = coordinator_duration;
328 let roots_duration = install_current_fleet_subnet_roots(
329 icp_root,
330 environment,
331 local_replica,
332 config_path,
333 planned,
334 coordinator.coordinator,
335 )?;
336 timings.create_canisters += roots_duration;
337 bootstrap_and_verify_fleet_subnet_root_stores(
338 icp_root,
339 environment,
340 local_replica,
341 config_path,
342 &planned.plan,
343 coordinator.coordinator,
344 planned.session.operation_id,
345 )
346 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
347 let joining_version = register_and_verify_fleet_subnet_roots_joining(
348 icp_root,
349 environment,
350 local_replica,
351 config_path,
352 &planned.plan,
353 coordinator.coordinator,
354 planned.session.operation_id,
355 )
356 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
357 synchronize_and_verify_fleet_subnet_roots(SynchronizeFleetSubnetRootsRequest {
358 icp_root,
359 environment,
360 local_replica,
361 config_path,
362 fleet_install_plan: &planned.plan,
363 coordinator: coordinator.coordinator,
364 install_operation_id: planned.session.operation_id,
365 joining_version: joining_version.clone(),
366 })
367 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
368 let active = activate_and_verify_fleet_registry(ActivateFleetRegistryRequest {
369 icp_root,
370 environment,
371 local_replica,
372 config_path,
373 fleet_install_plan: &planned.plan,
374 coordinator: coordinator.coordinator,
375 install_operation_id: planned.session.operation_id,
376 joining_version: joining_version.clone(),
377 })
378 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
379 activate_and_verify_fleet_subnet_root_registry_mirrors(
380 ActivateFleetSubnetRootRegistryMirrorsRequest {
381 icp_root,
382 environment,
383 local_replica,
384 config_path,
385 fleet_install_plan: &planned.plan,
386 coordinator: coordinator.coordinator,
387 install_operation_id: planned.session.operation_id,
388 joining_version,
389 active_registry: &active.registry,
390 active_version: active.version.clone(),
391 },
392 )
393 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
394 prepare_and_activate_current_fleet_subnet_roots(
395 icp_root,
396 environment,
397 local_replica,
398 config_path,
399 planned,
400 coordinator.coordinator,
401 )?;
402 publish_installed_fleet_catalog(PublishInstalledFleetCatalogRequest {
403 icp_root,
404 environment,
405 local_replica,
406 config_path,
407 fleet_name: planned.session.fleet_name.clone(),
408 fleet_install_plan: &planned.plan,
409 coordinator: coordinator.coordinator,
410 })
411 .map(|_| ())
412 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))
413}
414
415fn prepare_and_activate_current_fleet_subnet_roots(
416 icp_root: &Path,
417 environment: &str,
418 local_replica: Option<&crate::icp::LocalReplicaTarget>,
419 config_path: &Path,
420 planned: &PlannedCurrentFleetInstall,
421 coordinator: canic_core::cdk::types::Principal,
422) -> Result<(), InstallRootError> {
423 prepare_and_verify_fleet_subnet_root_component_registries(
424 PrepareFleetSubnetRootComponentRegistriesRequest {
425 icp_root,
426 environment,
427 local_replica,
428 config_path,
429 fleet_install_plan: &planned.plan,
430 coordinator,
431 install_operation_id: planned.session.operation_id,
432 },
433 )
434 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
435 activate_and_verify_fleet_subnet_root_runtimes(ActivateFleetSubnetRootRuntimesRequest {
436 icp_root,
437 environment,
438 local_replica,
439 config_path,
440 fleet_install_plan: &planned.plan,
441 coordinator,
442 install_operation_id: planned.session.operation_id,
443 })
444 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))
445}
446
447fn resolve_current_install_roots(
448 options: &InstallRootOptions,
449) -> Result<(PathBuf, PathBuf), InstallRootError> {
450 let workspace_root = workspace_root()
451 .map_err(|source| InstallRootError::new(InstallRootPhase::WorkspaceDiscovery, source))?;
452 let icp_root = match &options.icp_root {
453 Some(path) => path.canonicalize().map_err(|source| {
454 InstallRootError::new(InstallRootPhase::IcpProjectDiscovery, source)
455 })?,
456 None => icp_root().map_err(|source| {
457 InstallRootError::new(InstallRootPhase::IcpProjectDiscovery, source)
458 })?,
459 };
460 Ok((workspace_root, icp_root))
461}
462
463fn plan_current_fleet_install(
464 icp_root: &Path,
465 environment: &str,
466 fleet_name: &str,
467 app_id: &str,
468 config_path: &Path,
469 finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
470 input: ResolvedFleetInstallInput,
471) -> Result<PlannedCurrentFleetInstall, InstallRootError> {
472 let session = plan_current_fleet_install_session(
473 icp_root,
474 environment,
475 fleet_name,
476 app_id,
477 finalized_release_build,
478 )?;
479 let plan = persist_current_fleet_install_plan(
480 icp_root,
481 config_path,
482 session.fleet.clone(),
483 finalized_release_build,
484 input,
485 )
486 .map_err(InstallRootError::in_phase(InstallRootPhase::Planning))?;
487 Ok(PlannedCurrentFleetInstall { session, plan })
488}
489
490struct PlannedCurrentFleetInstall {
491 session: fleet_install_session::FleetInstallSession,
492 plan: PersistedFleetInstallPlan,
493}
494
495impl PlannedCurrentFleetInstall {
496 const fn fleet(&self) -> canic_core::ids::FleetKey {
497 self.session.fleet.fleet
498 }
499}
500
501fn require_finalized_release_build(
502 finalized: Option<crate::release_build::FinalizedReleaseBuild>,
503) -> Result<crate::release_build::FinalizedReleaseBuild, InstallRootError> {
504 finalized.ok_or_else(|| {
505 InstallRootError::new(
506 InstallRootPhase::Manifest,
507 ReleaseBuildPlanError::MissingFinalizedAuthority,
508 )
509 })
510}
511
512fn resolve_current_fleet_install_input(
513 icp_root: &Path,
514 environment: &str,
515 options: &InstallRootOptions,
516) -> Result<ResolvedFleetInstallInput, Box<dyn std::error::Error>> {
517 let input_path = options
518 .fleet_install_input_path
519 .as_ref()
520 .ok_or(MissingFleetInstallInputError)?;
521 let input_path = if input_path.is_absolute() {
522 input_path.clone()
523 } else {
524 icp_root.join(input_path)
525 };
526 load_and_resolve_fleet_install_input(icp_root, environment, &input_path).map_err(Into::into)
527}
528
529fn persist_current_fleet_install_plan(
530 icp_root: &Path,
531 config_path: &Path,
532 fleet: canic_core::ids::FleetBinding,
533 finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
534 input: ResolvedFleetInstallInput,
535) -> Result<PersistedFleetInstallPlan, Box<dyn std::error::Error>> {
536 let config = AppConfigSnapshot::load(config_path)?;
537 compile_and_persist_fleet_install_plan(FleetInstallPlanRequest {
538 root: icp_root,
539 config: config.model(),
540 fleet,
541 release_build_id: finalized_release_build.record.release_build_id,
542 coordinator: input.coordinator,
543 fleet_subnet_roots: input.fleet_subnet_roots,
544 })
545 .map_err(Into::into)
546}
547
548fn current_install_config_path(
549 icp_root: &Path,
550 options: &InstallRootOptions,
551) -> Result<PathBuf, InstallRootError> {
552 resolve_install_config_path(
553 icp_root,
554 options.config_path.as_deref(),
555 options.interactive_config_selection,
556 )
557 .map_err(InstallRootError::in_phase(InstallRootPhase::Configuration))
558}
559
560fn plan_current_fleet_install_session(
561 icp_root: &Path,
562 environment: &str,
563 fleet_name: &str,
564 app_id: &str,
565 finalized_release_build: &crate::release_build::FinalizedReleaseBuild,
566) -> Result<fleet_install_session::FleetInstallSession, InstallRootError> {
567 let canonical_network_id = resolve_canonical_network_id_from_root(icp_root, environment)
568 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))?;
569 let fleet_name = fleet_name
570 .parse()
571 .map_err(|source| InstallRootError::new(InstallRootPhase::Identity, source))?;
572 fleet_install_session::plan_fleet_install_session(
573 fleet_install_session::PlanFleetInstallSessionRequest {
574 root: icp_root,
575 canonical_network_id,
576 fleet_name,
577 app: app_id.into(),
578 finalized_release_build,
579 },
580 )
581 .map_err(|source| InstallRootError::new(InstallRootPhase::Activation, source))
582}
583
584fn print_install_identity(app: &str, fleet_name: &str) {
585 println!("Installing Fleet {fleet_name}");
586 println!("Source App {app}");
587 println!();
588}
589
590fn persist_current_pre_root_receipts(
591 receipt_scope: InstallReceiptScope<'_>,
592 prepared_receipts: &[DeploymentReceiptV1],
593 build_phase: CompletedInstallPhase,
594 manifest_phase: CompletedInstallPhase,
595) -> Result<(), InstallRootError> {
596 persist_pre_root_receipts(
597 receipt_scope,
598 prepared_receipts,
599 build_phase,
600 manifest_phase,
601 )
602 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))
603}
604
605fn install_current_fleet_coordinator(
606 icp_root: &Path,
607 environment: &str,
608 local_replica: Option<&crate::icp::LocalReplicaTarget>,
609 config_path: &Path,
610 plan: &PersistedFleetInstallPlan,
611) -> Result<(coordinator_install::VerifiedFleetCoordinator, Duration), InstallRootError> {
612 let started = Instant::now();
613 let coordinator = install_and_verify_fleet_coordinator(
614 icp_root,
615 environment,
616 local_replica,
617 config_path,
618 plan,
619 )
620 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
621 Ok((coordinator, started.elapsed()))
622}
623
624fn install_current_fleet_subnet_roots(
625 icp_root: &Path,
626 environment: &str,
627 local_replica: Option<&crate::icp::LocalReplicaTarget>,
628 config_path: &Path,
629 planned: &PlannedCurrentFleetInstall,
630 coordinator: canic_core::cdk::types::Principal,
631) -> Result<Duration, InstallRootError> {
632 let started = Instant::now();
633 install_and_verify_fleet_subnet_roots(
634 icp_root,
635 environment,
636 local_replica,
637 config_path,
638 &planned.plan,
639 coordinator,
640 planned.session.operation_id,
641 )
642 .map_err(InstallRootError::in_phase(InstallRootPhase::Activation))?;
643 Ok(started.elapsed())
644}
645
646fn persist_pre_root_receipts(
647 receipt_scope: InstallReceiptScope<'_>,
648 prepared_receipts: &[DeploymentReceiptV1],
649 build_phase: CompletedInstallPhase,
650 manifest_phase: CompletedInstallPhase,
651) -> Result<(), Box<dyn std::error::Error>> {
652 for receipt in prepared_receipts {
653 receipt_scope.write_receipt(receipt)?;
654 }
655 write_completed_install_phase_receipt(receipt_scope, build_phase)?;
656 write_completed_install_phase_receipt(receipt_scope, manifest_phase)?;
657 Ok(())
658}
659
660fn current_install_build_inputs(
661 workspace_root: &std::path::Path,
662 icp_root: &std::path::Path,
663 config_path: &std::path::Path,
664 options: &InstallRootOptions,
665) -> Result<
666 (
667 crate::canister_build::WorkspaceBuildContext,
668 build_snapshot::ValidatedInstallSnapshot,
669 ),
670 Box<dyn std::error::Error>,
671> {
672 let mut context = resolve_install_build_context(
673 workspace_root,
674 icp_root,
675 config_path,
676 &options.environment,
677 &options.root_build_target,
678 options.build_profile,
679 )?;
680 if options.deployment_plan_override.is_some() {
681 let snapshot = resolve_install_snapshot(&context, &options.root_build_target, true)?;
682 return Ok((context, snapshot));
683 }
684
685 let config = AppConfigSnapshot::load(config_path)?;
686 let release_build = current_install_release_build(
687 icp_root,
688 &options.environment,
689 &options.fleet_name,
690 config.app_id(),
691 )?;
692 context = context.with_release_build_id(release_build.record.release_build_id);
693 let mut snapshot = resolve_install_snapshot(&context, &options.root_build_target, false)?;
694 snapshot.release_build = Some(release_build);
695 Ok((context, snapshot))
696}
697
698fn current_install_release_build(
699 icp_root: &Path,
700 environment: &str,
701 fleet_name: &str,
702 app_id: &str,
703) -> Result<PlannedReleaseBuild, Box<dyn std::error::Error>> {
704 let canonical_network_id = resolve_canonical_network_id_from_root(icp_root, environment)?;
705 let fleet_name = fleet_name.parse()?;
706 let app = app_id.into();
707 if let Some(finalized) = fleet_install_session::recover_fleet_install_session_release_build(
708 icp_root,
709 canonical_network_id,
710 &fleet_name,
711 &app,
712 )? {
713 return Ok(PlannedReleaseBuild {
714 record: finalized.record,
715 path: finalized.path,
716 });
717 }
718
719 plan_release_build(icp_root).map_err(Into::into)
720}