Skip to main content

canic_host/install_root/
mod.rs

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///
111/// InstallRootBlockKind
112///
113/// Machine-readable reason that a fresh root install stopped before mutation.
114///
115
116#[derive(Clone, Copy, Debug, Eq, PartialEq)]
117pub enum InstallRootBlockKind {
118    DeploymentExecutionPreflight,
119    DeploymentTruth,
120}
121
122///
123/// InstallRootBlockedError
124///
125/// Typed install block retained through the host/CLI error boundary.
126///
127
128#[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/// Stable phase in which a root install failed.
147#[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/// Typed public failure for the root-install workflow.
177#[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    /// Preserve a concrete cause while assigning it to a stable install phase.
187    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
211/// Discover installable Canic config choices under the current workspace.
212pub 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
227// Execute fresh Fleet planning and the Coordinator-first installation workflow.
228pub 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}