1use crate::canister_build::{
2 CanisterBuildProfile, build_current_workspace_canister_artifact,
3 current_workspace_build_context_once,
4};
5use crate::deployment_truth::{
6 ArtifactPromotionExecutionReceiptRequest, ArtifactPromotionExecutionReceiptV1,
7 ArtifactPromotionPlanV1, ArtifactPromotionProvenanceReportRequest, ArtifactTransportV1,
8 CurrentCliDeploymentExecutor, DeploymentCheckV1, DeploymentCommandResultV1,
9 DeploymentExecutionContextV1, DeploymentExecutionPreflightV1, DeploymentExecutionStatusV1,
10 DeploymentExecutor, DeploymentExecutorCapabilityV1, DeploymentPlanV1, DeploymentReceiptV1,
11 DeploymentRootVerificationEvidenceStatusV1, DeploymentRootVerificationReceiptV1,
12 DeploymentRootVerificationReportV1, DeploymentRootVerificationRequestV1,
13 DeploymentRootVerificationSourceV1, DeploymentRootVerificationStateTransitionV1,
14 DeploymentRootVerificationStateV1, LocalDeploymentCheckRequest, LocalInventoryRequest,
15 ObservationStatusV1, SafetyFindingV1, StagingReceiptV1, artifact_gate_phase_receipt,
16 artifact_gate_role_phase_receipts, artifact_promotion_execution_receipt,
17 artifact_promotion_provenance_report, check_local_deployment,
18 collect_local_deployment_inventory, compare_plan_to_inventory,
19 deployment_execution_preflight_from_check, deployment_receipt_from_check_with_status,
20 deployment_root_verification_receipt_digest, deployment_root_verification_report_from_check,
21 missing_executor_capabilities, phase_receipt, safety_report_from_diff,
22 staging_receipt_evidence, validate_deployment_execution_preflight_for_check,
23 validate_deployment_root_verification_receipt, validate_deployment_root_verification_report,
24};
25use crate::format::wasm_size_label;
26use crate::icp::{self, CANIC_ICP_LOCAL_NETWORK_URL_ENV, CANIC_ICP_LOCAL_ROOT_KEY_ENV};
27use crate::release_set::{
28 LOCAL_ROOT_MIN_READY_CYCLES, RootReleaseSetManifest, configured_fleet_name,
29 configured_install_targets, configured_local_root_create_cycles,
30 emit_root_release_set_manifest_with_config, icp_query_on_network, icp_root,
31 load_root_release_set_manifest, resolve_artifact_root, resume_root_bootstrap,
32 stage_root_release_set, workspace_root,
33};
34use crate::replica_query;
35use crate::response_parse::parse_cycle_balance_response;
36use crate::table::{ColumnAlign, render_separator, render_table, render_table_row, table_widths};
37use canic_core::cdk::utils::hash::wasm_hash_hex;
38use canic_core::{
39 CANIC_WASM_CHUNK_BYTES,
40 cdk::{types::Principal, utils::hash::wasm_hash},
41 protocol,
42};
43use config_selection::resolve_install_config_path;
44use serde_json::Value as JsonValue;
45use sha2::{Digest, Sha256};
46use std::{
47 env,
48 ffi::OsString,
49 fs,
50 path::{Path, PathBuf},
51 process::Command,
52 time::{Duration, Instant, SystemTime, UNIX_EPOCH},
53};
54
55mod config_selection;
56mod readiness;
57mod state;
58
59pub use config_selection::{
60 current_canic_project_root, discover_canic_config_choices, discover_canic_project_root_from,
61 discover_project_canic_config_choices, project_fleet_roots,
62};
63use readiness::wait_for_root_ready;
64use state::{
65 INSTALL_STATE_SCHEMA_VERSION, deployment_install_state_path, read_deployment_install_state,
66 validate_network_name, validate_state_name, write_install_state,
67};
68pub use state::{
69 InstallState, RootVerificationStatus, read_named_deployment_install_state,
70 read_named_deployment_install_state_from_root,
71};
72
73#[cfg(test)]
74mod tests;
75
76#[cfg(test)]
77use config_selection::config_selection_error;
78#[cfg(test)]
79use readiness::{parse_bootstrap_status_value, parse_root_ready_value};
80#[cfg(test)]
81use state::legacy_fleet_install_state_path;
82
83#[derive(Clone, Debug)]
88pub struct InstallRootOptions {
89 pub root_canister: String,
90 pub root_build_target: String,
91 pub network: String,
92 pub deployment_name: Option<String>,
93 pub icp_root: Option<PathBuf>,
94 pub build_profile: Option<CanisterBuildProfile>,
95 pub ready_timeout_seconds: u64,
96 pub config_path: Option<String>,
97 pub expected_fleet: Option<String>,
98 pub interactive_config_selection: bool,
99 pub deployment_plan_override: Option<DeploymentPlanV1>,
100 pub artifact_promotion_plan_override: Option<ArtifactPromotionPlanV1>,
101}
102
103#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct RegisterDeploymentStateOptions {
109 pub deployment_name: String,
110 pub fleet_template: String,
111 pub root_canister_id: String,
112 pub network: String,
113 pub allow_unverified: bool,
114 pub icp_root: Option<PathBuf>,
115 pub workspace_root: Option<PathBuf>,
116}
117
118#[derive(Clone, Debug, Eq, PartialEq)]
122pub struct VerifyDeploymentRootOptions {
123 pub deployment_name: String,
124 pub network: String,
125 pub deployment_check: DeploymentCheckV1,
126 pub verified_at_unix_secs: Option<u64>,
127 pub icp_root: Option<PathBuf>,
128}
129
130#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
135struct InstallTimingSummary {
136 create_canisters: Duration,
137 build_all: Duration,
138 emit_manifest: Duration,
139 install_root: Duration,
140 fund_root: Duration,
141 stage_release_set: Duration,
142 resume_bootstrap: Duration,
143 wait_ready: Duration,
144 finalize_root_funding: Duration,
145}
146
147const CURRENT_INSTALL_REQUIRED_CAPABILITIES: &[DeploymentExecutorCapabilityV1] = &[
148 DeploymentExecutorCapabilityV1::CreateCanister,
149 DeploymentExecutorCapabilityV1::InstallCode,
150 DeploymentExecutorCapabilityV1::Call,
151 DeploymentExecutorCapabilityV1::Query,
152 DeploymentExecutorCapabilityV1::StageArtifact,
153];
154
155pub fn discover_current_canic_config_choices() -> Result<Vec<PathBuf>, Box<dyn std::error::Error>> {
157 let project_root = current_canic_project_root()?;
158 let choices = config_selection::discover_workspace_canic_config_choices(&project_root)?;
159 if !choices.is_empty() {
160 return Ok(choices);
161 }
162
163 if let Ok(icp_root) = icp_root()
164 && icp_root != project_root
165 {
166 return config_selection::discover_workspace_canic_config_choices(&icp_root);
167 }
168
169 Ok(choices)
170}
171
172pub fn install_root(options: InstallRootOptions) -> Result<(), Box<dyn std::error::Error>> {
174 let workspace_root = workspace_root()?;
175 let icp_root = match &options.icp_root {
176 Some(path) => path.canonicalize()?,
177 None => icp_root()?,
178 };
179 let config_path = resolve_install_config_path(
180 &icp_root,
181 options.config_path.as_deref(),
182 options.interactive_config_selection,
183 )?;
184 let _install_env = BuildEnvGuard::apply(&options.network, &config_path, &icp_root);
185 let (fleet_name, deployment_name) = resolve_install_identity(&options, &config_path)?;
186 let total_started_at = Instant::now();
187 let mut timings = InstallTimingSummary::default();
188 let network = options.network.as_str();
189 let execution_context = current_install_execution_context(&workspace_root, &icp_root, network);
190
191 println!("Installing deployment {deployment_name}");
192 println!("Fleet template {fleet_name}");
193 println!();
194 let prepared = prepare_install_deployment_truth(
195 &options,
196 &workspace_root,
197 &icp_root,
198 &config_path,
199 &deployment_name,
200 &execution_context,
201 )?;
202 timings.create_canisters = prepared.timings.create_canisters;
203 timings.build_all = prepared.timings.build_all;
204
205 let (manifest_path, emit_manifest_duration) = emit_manifest_with_deployment_truth_receipt(
206 &workspace_root,
207 &icp_root,
208 &options,
209 &config_path,
210 &deployment_name,
211 &prepared.deployment_truth_check,
212 &execution_context,
213 )?;
214 timings.emit_manifest = emit_manifest_duration;
215 let activation_timings = run_root_activation_phases(
216 InstallReceiptScope {
217 icp_root: &icp_root,
218 network,
219 deployment_name: &deployment_name,
220 check: &prepared.deployment_truth_check,
221 execution_context: Some(&execution_context),
222 },
223 &options,
224 &prepared.root_canister_id,
225 &manifest_path,
226 total_started_at,
227 )?;
228 timings.install_root = activation_timings.install_root;
229 timings.fund_root = activation_timings.fund_root;
230 timings.stage_release_set = activation_timings.stage_release_set;
231 timings.resume_bootstrap = activation_timings.resume_bootstrap;
232 timings.wait_ready = activation_timings.wait_ready;
233 timings.finalize_root_funding = activation_timings.finalize_root_funding;
234
235 print_install_timing_summary(&timings, total_started_at.elapsed());
236 let state = build_install_state(
237 &options,
238 &workspace_root,
239 &icp_root,
240 &config_path,
241 &manifest_path,
242 (&deployment_name, &fleet_name),
243 &prepared.root_canister_id,
244 )?;
245 let state_path = write_install_state_with_deployment_truth_receipt(
246 InstallReceiptScope {
247 icp_root: &icp_root,
248 network,
249 deployment_name: &deployment_name,
250 check: &prepared.deployment_truth_check,
251 execution_context: Some(&execution_context),
252 },
253 &options.network,
254 &state,
255 )?;
256 write_artifact_promotion_execution_receipt_for_install(
257 &options,
258 &icp_root,
259 network,
260 &deployment_name,
261 &prepared.deployment_truth_check,
262 &execution_context,
263 )?;
264 print_install_result_summary(
265 &options.network,
266 &state.deployment_name,
267 &state.fleet_template,
268 &state_path,
269 );
270 Ok(())
271}
272
273fn resolve_install_identity(
274 options: &InstallRootOptions,
275 config_path: &Path,
276) -> Result<(String, String), Box<dyn std::error::Error>> {
277 let fleet_name = configured_fleet_name(config_path)?;
278 validate_expected_fleet_name(options.expected_fleet.as_deref(), &fleet_name, config_path)?;
279 validate_state_name(&fleet_name)?;
280 let deployment_name = options
281 .deployment_name
282 .clone()
283 .unwrap_or_else(|| fleet_name.clone());
284 validate_state_name(&deployment_name)?;
285 Ok((fleet_name, deployment_name))
286}
287
288pub fn register_deployment_state(
294 options: RegisterDeploymentStateOptions,
295) -> Result<PathBuf, Box<dyn std::error::Error>> {
296 validate_state_name(&options.deployment_name)?;
297 validate_state_name(&options.fleet_template)?;
298 validate_network_name(&options.network)?;
299 if !options.allow_unverified {
300 return Err(
301 "deployment registration requires explicit unverified-root acknowledgement; pass --allow-unverified"
302 .into(),
303 );
304 }
305 Principal::from_text(&options.root_canister_id).map_err(|err| {
306 format!(
307 "invalid root principal for deployment {}: {err}",
308 options.deployment_name
309 )
310 })?;
311
312 let workspace_root = match options.workspace_root {
313 Some(path) => path,
314 None => workspace_root()?,
315 };
316 let icp_root = match options.icp_root {
317 Some(path) => path,
318 None => icp_root()?,
319 };
320 let artifact_root = resolve_artifact_root(&icp_root, &options.network).unwrap_or_else(|_| {
321 icp_root
322 .join(".icp")
323 .join(&options.network)
324 .join("canisters")
325 });
326 let release_set_manifest_path =
327 crate::release_set::root_release_set_manifest_path(&artifact_root)
328 .unwrap_or_else(|_| artifact_root.join("root").join("root.release-set.json"));
329 let timestamp = current_unix_secs()?;
330 let state = InstallState {
331 schema_version: INSTALL_STATE_SCHEMA_VERSION,
332 deployment_name: options.deployment_name,
333 fleet_template: options.fleet_template.clone(),
334 created_at_unix_secs: timestamp,
335 updated_at_unix_secs: timestamp,
336 network: options.network.clone(),
337 root_target: options.root_canister_id.clone(),
338 root_canister_id: options.root_canister_id,
339 root_verification: RootVerificationStatus::NotVerified,
340 root_build_target: "root".to_string(),
341 workspace_root: workspace_root.display().to_string(),
342 icp_root: icp_root.display().to_string(),
343 config_path: workspace_root
344 .join("fleets")
345 .join(&options.fleet_template)
346 .join("canic.toml")
347 .display()
348 .to_string(),
349 release_set_manifest_path: release_set_manifest_path.display().to_string(),
350 };
351
352 write_install_state(&icp_root, &options.network, &state)
353}
354
355pub fn verify_registered_deployment_root(
358 options: VerifyDeploymentRootOptions,
359) -> Result<DeploymentRootVerificationReceiptV1, Box<dyn std::error::Error>> {
360 validate_state_name(&options.deployment_name)?;
361 validate_network_name(&options.network)?;
362 let verified_at_unix_secs = match options.verified_at_unix_secs {
363 Some(value) => value,
364 None => current_unix_secs()?,
365 };
366 let icp_root = match options.icp_root {
367 Some(path) => path,
368 None => icp_root()?,
369 };
370 let state_path =
371 deployment_install_state_path(&icp_root, &options.network, &options.deployment_name);
372 let state =
373 read_deployment_install_state(&icp_root, &options.network, &options.deployment_name)?
374 .ok_or_else(|| {
375 format!(
376 "no local deployment state exists for {}; run canic deploy register first",
377 options.deployment_name
378 )
379 })?;
380 let state_fleet_template = state.fleet_template.clone();
381 let state_root_canister_id = state.root_canister_id.clone();
382 let local_state_digest_before = file_sha256_hex(&state_path)?;
383 let previous_root_verification = deployment_root_verification_state(&state.root_verification);
384 let report =
385 deployment_root_verification_report_from_check(DeploymentRootVerificationRequestV1 {
386 report_id: format!(
387 "local:{}:{}:root-verification-report",
388 options.network, options.deployment_name
389 ),
390 requested_at: format!("unix:{verified_at_unix_secs}"),
391 deployment_name: options.deployment_name.clone(),
392 network: options.network.clone(),
393 expected_fleet_template: state.fleet_template.clone(),
394 expected_root_principal: state.root_canister_id.clone(),
395 current_root_verification: previous_root_verification,
396 source: DeploymentRootVerificationSourceV1::DeploymentTruthCheck,
397 deployment_check: options.deployment_check,
398 });
399 validate_deployment_root_verification_report(&report)?;
400 if report.evidence_status != DeploymentRootVerificationEvidenceStatusV1::EvidenceSatisfied {
401 return Err(format!(
402 "deployment root verification failed for {}: {} blocker(s)",
403 options.deployment_name,
404 report.blockers.len()
405 )
406 .into());
407 }
408 let state_transition = verified_root_state_transition(previous_root_verification);
409 let local_state_digest_after = match previous_root_verification {
410 DeploymentRootVerificationStateV1::NotVerified => {
411 let mut verified_state = state;
412 verified_state.root_verification = RootVerificationStatus::Verified;
413 verified_state.updated_at_unix_secs = verified_at_unix_secs;
414 write_verified_root_state_if_unchanged(
415 &icp_root,
416 &options.network,
417 &verified_state,
418 &local_state_digest_before,
419 )?
420 }
421 DeploymentRootVerificationStateV1::Verified => file_sha256_hex(&state_path)?,
422 };
423
424 root_verification_receipt_from_report(RootVerificationReceiptInput {
425 deployment_name: options.deployment_name,
426 network: options.network,
427 fleet_template: state_fleet_template,
428 root_principal: state_root_canister_id,
429 previous_root_verification,
430 state_transition,
431 report,
432 verified_at_unix_secs,
433 local_state_path: state_path.display().to_string(),
434 local_state_digest_before,
435 local_state_digest_after,
436 })
437}
438
439struct RootVerificationReceiptInput {
440 deployment_name: String,
441 network: String,
442 fleet_template: String,
443 root_principal: String,
444 previous_root_verification: DeploymentRootVerificationStateV1,
445 state_transition: DeploymentRootVerificationStateTransitionV1,
446 report: DeploymentRootVerificationReportV1,
447 verified_at_unix_secs: u64,
448 local_state_path: String,
449 local_state_digest_before: String,
450 local_state_digest_after: String,
451}
452
453fn root_verification_receipt_from_report(
454 input: RootVerificationReceiptInput,
455) -> Result<DeploymentRootVerificationReceiptV1, Box<dyn std::error::Error>> {
456 let source_root_observation_source = input.report.observed_root_observation_source.ok_or(
457 "deployment root verification report did not preserve observed root source evidence",
458 )?;
459 let source_observed_root_canister_id =
460 input.report.observed_root_canister_id.clone().ok_or(
461 "deployment root verification report did not preserve observed root canister id",
462 )?;
463
464 let mut receipt = DeploymentRootVerificationReceiptV1 {
465 schema_version: crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
466 receipt_id: format!(
467 "local:{}:{}:root-verification-receipt",
468 input.network, input.deployment_name
469 ),
470 receipt_digest: String::new(),
471 deployment_name: input.deployment_name,
472 network: input.network,
473 fleet_template: input.fleet_template,
474 root_principal: input.root_principal,
475 previous_root_verification: input.previous_root_verification,
476 new_root_verification: DeploymentRootVerificationStateV1::Verified,
477 state_transition: input.state_transition,
478 source_report_id: input.report.report_id,
479 source_report_digest: input.report.report_digest,
480 source_report_requested_at: input.report.requested_at,
481 source_report_source: input.report.source,
482 source_report_evidence_status: input.report.evidence_status,
483 source_report_current_root_verification: input.report.current_root_verification,
484 source_report_state_transition: input.report.state_transition,
485 source_root_observation_source,
486 source_observed_root_canister_id,
487 source_check_id: input.report.source_check_id,
488 source_check_digest: input.report.source_check_digest,
489 source_deployment_plan_id: input.report.source_deployment_plan_id,
490 source_deployment_plan_digest: input.report.source_deployment_plan_digest,
491 source_inventory_id: input.report.source_inventory_id,
492 source_inventory_digest: input.report.source_inventory_digest,
493 verified_at_unix_secs: input.verified_at_unix_secs,
494 local_state_path: input.local_state_path,
495 local_state_digest_before: input.local_state_digest_before,
496 local_state_digest_after: input.local_state_digest_after,
497 warnings: input.report.warnings,
498 };
499 receipt.receipt_digest = deployment_root_verification_receipt_digest(&receipt);
500 validate_deployment_root_verification_receipt(&receipt)?;
501 Ok(receipt)
502}
503
504struct PreparedInstallTruth {
505 root_canister_id: String,
506 deployment_truth_check: DeploymentCheckV1,
507 timings: InstallTimingSummary,
508}
509
510struct CurrentInstallTruthInputs {
511 workspace_root: PathBuf,
512 icp_root: PathBuf,
513 config_path: PathBuf,
514 deployment_name: String,
515}
516
517fn prepare_install_deployment_truth(
518 options: &InstallRootOptions,
519 workspace_root: &Path,
520 icp_root: &Path,
521 config_path: &Path,
522 deployment_name: &str,
523 execution_context: &DeploymentExecutionContextV1,
524) -> Result<PreparedInstallTruth, Box<dyn std::error::Error>> {
525 let mut timings = InstallTimingSummary::default();
526 ensure_current_install_executor_capabilities(execution_context)?;
527 ensure_icp_environment_ready(icp_root, &options.network)?;
528 let (root_canister_id, create_phase, create_duration) =
529 resolve_root_canister_with_phase(options, icp_root, config_path)?;
530 timings.create_canisters = create_duration;
531
532 let (build_phase, build_duration) =
533 build_install_targets_with_phase(options, icp_root, config_path)?;
534 timings.build_all = build_duration;
535
536 let deployment_truth_check = run_install_deployment_truth_safety_gate(
537 options,
538 workspace_root,
539 icp_root,
540 config_path,
541 deployment_name,
542 execution_context,
543 )?;
544 let receipt_scope = InstallReceiptScope {
545 icp_root,
546 network: &options.network,
547 deployment_name,
548 check: &deployment_truth_check,
549 execution_context: Some(execution_context),
550 };
551 write_completed_install_phase_receipt(receipt_scope, create_phase)?;
552 write_completed_install_phase_receipt(receipt_scope, build_phase)?;
553
554 Ok(PreparedInstallTruth {
555 root_canister_id,
556 deployment_truth_check,
557 timings,
558 })
559}
560
561fn resolve_root_canister_with_phase(
562 options: &InstallRootOptions,
563 icp_root: &Path,
564 config_path: &Path,
565) -> Result<(String, CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
566 let operation = ResolveRootCanisterOperation::new(
567 icp_root,
568 &options.network,
569 &options.root_canister,
570 config_path,
571 );
572 let started_at = current_unix_timestamp_label()?;
573 let started = Instant::now();
574 let root_canister_id = operation.execute()?;
575 let duration = started.elapsed();
576 let phase = CompletedInstallPhase {
577 phase: "resolve_root_canister",
578 attempted_action: "resolve or create root canister id",
579 started_at,
580 finished_at: Some(current_unix_timestamp_label()?),
581 evidence: operation.evidence(&root_canister_id),
582 role_names: Vec::new(),
583 };
584 Ok((root_canister_id, phase, duration))
585}
586
587fn build_install_targets_with_phase(
588 options: &InstallRootOptions,
589 icp_root: &Path,
590 config_path: &Path,
591) -> Result<(CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
592 if let Some(plan) = &options.deployment_plan_override {
593 return validate_plan_artifacts_with_phase(plan, icp_root, &options.network);
594 }
595
596 let build_targets = configured_install_targets(config_path, &options.root_build_target)?;
597 let operation = BuildInstallTargetsOperation::new(
598 &options.network,
599 build_targets,
600 options.build_profile,
601 config_path,
602 icp_root,
603 );
604 let started_at = current_unix_timestamp_label()?;
605 let started = Instant::now();
606 operation.execute()?;
607 let duration = started.elapsed();
608 let phase = CompletedInstallPhase {
609 phase: "build_artifacts",
610 attempted_action: "build configured install targets",
611 started_at,
612 finished_at: Some(current_unix_timestamp_label()?),
613 evidence: operation.evidence(),
614 role_names: operation.role_names(),
615 };
616 Ok((phase, duration))
617}
618
619fn validate_plan_artifacts_with_phase(
620 plan: &DeploymentPlanV1,
621 icp_root: &Path,
622 network: &str,
623) -> Result<(CompletedInstallPhase, Duration), Box<dyn std::error::Error>> {
624 let started_at = current_unix_timestamp_label()?;
625 let started = Instant::now();
626 validate_plan_artifact_paths(plan, icp_root, network)?;
627 let duration = started.elapsed();
628 let role_names = plan
629 .role_artifacts
630 .iter()
631 .map(|artifact| artifact.role.clone())
632 .collect::<Vec<_>>();
633 let phase = CompletedInstallPhase {
634 phase: "materialize_artifacts",
635 attempted_action: "validate supplied deployment plan artifacts",
636 started_at,
637 finished_at: Some(current_unix_timestamp_label()?),
638 evidence: vec![format!("deployment_plan:{}", plan.plan_id)],
639 role_names,
640 };
641 Ok((phase, duration))
642}
643
644fn validate_plan_artifact_paths(
645 plan: &DeploymentPlanV1,
646 icp_root: &Path,
647 network: &str,
648) -> Result<(), Box<dyn std::error::Error>> {
649 let artifact_root = resolve_artifact_root(icp_root, network)?;
650 let root_artifact = plan
651 .role_artifacts
652 .iter()
653 .find(|artifact| artifact.role == "root")
654 .ok_or_else(|| "deployment plan is missing root role artifact".to_string())?;
655 let root_wasm = plan_role_wasm_path(icp_root, &artifact_root, root_artifact);
656 if !root_wasm.is_file() {
657 return Err(format!(
658 "deployment plan root wasm artifact does not exist: {}",
659 root_wasm.display()
660 )
661 .into());
662 }
663
664 for artifact in plan_release_role_artifacts(plan) {
665 let wasm_gz = plan_role_wasm_gz_path(icp_root, &artifact_root, artifact);
666 if !wasm_gz.is_file() {
667 return Err(format!(
668 "deployment plan role {} wasm.gz artifact does not exist: {}",
669 artifact.role,
670 wasm_gz.display()
671 )
672 .into());
673 }
674 }
675 Ok(())
676}
677
678fn emit_manifest_with_deployment_truth_receipt(
679 workspace_root: &Path,
680 icp_root: &Path,
681 options: &InstallRootOptions,
682 config_path: &Path,
683 deployment_name: &str,
684 deployment_truth_check: &DeploymentCheckV1,
685 execution_context: &DeploymentExecutionContextV1,
686) -> Result<(PathBuf, Duration), Box<dyn std::error::Error>> {
687 let operation =
688 EmitRootManifestOperation::new(workspace_root, icp_root, &options.network, config_path);
689 let emit_manifest_started_at_label = current_unix_timestamp_label()?;
690 let emit_manifest_started_at = Instant::now();
691 let manifest_path = if let Some(plan) = &options.deployment_plan_override {
692 emit_root_release_set_manifest_from_plan(icp_root, &options.network, plan)?
693 } else {
694 operation.execute()?
695 };
696 let emit_manifest_duration = emit_manifest_started_at.elapsed();
697 let emit_manifest_receipt = receipt_with_execution_context(
698 install_deployment_truth_phase_receipt(
699 deployment_truth_check,
700 "emit_manifest",
701 emit_manifest_started_at_label,
702 Some(current_unix_timestamp_label()?),
703 "emit root release-set manifest",
704 crate::deployment_truth::ObservationStatusV1::Observed,
705 EmitRootManifestOperation::evidence(&manifest_path),
706 ),
707 execution_context,
708 );
709 let emit_manifest_receipt_path = write_install_deployment_truth_receipt(
710 icp_root,
711 &options.network,
712 deployment_name,
713 &emit_manifest_receipt,
714 )?;
715 println!(
716 "Deployment truth receipt JSON: {}",
717 emit_manifest_receipt_path.display()
718 );
719 Ok((manifest_path, emit_manifest_duration))
720}
721
722fn emit_root_release_set_manifest_from_plan(
723 icp_root: &Path,
724 network: &str,
725 plan: &DeploymentPlanV1,
726) -> Result<PathBuf, Box<dyn std::error::Error>> {
727 let artifact_root = resolve_artifact_root(icp_root, network)?;
728 let manifest_path = crate::release_set::root_release_set_manifest_path(&artifact_root)?;
729 let entries = plan_release_role_artifacts(plan)
730 .map(|artifact| release_set_entry_from_plan_artifact(icp_root, &artifact_root, artifact))
731 .collect::<Result<Vec<_>, _>>()?;
732 let manifest = RootReleaseSetManifest {
733 release_version: plan
734 .deployment_identity
735 .canic_version
736 .clone()
737 .unwrap_or_else(|| plan.plan_id.clone()),
738 entries,
739 };
740
741 fs::write(&manifest_path, serde_json::to_vec_pretty(&manifest)?)?;
742 Ok(manifest_path)
743}
744
745fn release_set_entry_from_plan_artifact(
746 icp_root: &Path,
747 artifact_root: &Path,
748 artifact: &crate::deployment_truth::RoleArtifactV1,
749) -> Result<crate::release_set::ReleaseSetEntry, Box<dyn std::error::Error>> {
750 let artifact_path = plan_role_wasm_gz_path(icp_root, artifact_root, artifact);
751 let artifact_relative_path = artifact_path
752 .strip_prefix(icp_root)
753 .map_err(|_| {
754 format!(
755 "deployment plan artifact {} is not under ICP root {}",
756 artifact_path.display(),
757 icp_root.display()
758 )
759 })?
760 .to_string_lossy()
761 .to_string();
762 let wasm_module = fs::read(&artifact_path)?;
763 let chunk_hashes = wasm_module
764 .chunks(CANIC_WASM_CHUNK_BYTES)
765 .map(wasm_hash_hex)
766 .collect::<Vec<_>>();
767
768 Ok(crate::release_set::ReleaseSetEntry {
769 role: artifact.role.clone(),
770 template_id: format!("embedded:{}", artifact.role),
771 artifact_relative_path,
772 payload_size_bytes: wasm_module.len() as u64,
773 payload_sha256_hex: wasm_hash_hex(&wasm_module),
774 chunk_size_bytes: CANIC_WASM_CHUNK_BYTES as u64,
775 chunk_sha256_hex: chunk_hashes,
776 })
777}
778
779fn plan_release_role_artifacts(
780 plan: &DeploymentPlanV1,
781) -> impl Iterator<Item = &crate::deployment_truth::RoleArtifactV1> {
782 plan.role_artifacts
783 .iter()
784 .filter(|artifact| !matches!(artifact.role.as_str(), "root" | "wasm_store"))
785}
786
787fn plan_role_wasm_path(
788 icp_root: &Path,
789 artifact_root: &Path,
790 artifact: &crate::deployment_truth::RoleArtifactV1,
791) -> PathBuf {
792 artifact.wasm_path.as_ref().map_or_else(
793 || {
794 artifact_root
795 .join(&artifact.role)
796 .join(format!("{}.wasm", artifact.role))
797 },
798 |path| plan_artifact_path(icp_root, path),
799 )
800}
801
802fn plan_role_wasm_gz_path(
803 icp_root: &Path,
804 artifact_root: &Path,
805 artifact: &crate::deployment_truth::RoleArtifactV1,
806) -> PathBuf {
807 artifact.wasm_gz_path.as_ref().map_or_else(
808 || {
809 artifact_root
810 .join(&artifact.role)
811 .join(format!("{}.wasm.gz", artifact.role))
812 },
813 |path| plan_artifact_path(icp_root, path),
814 )
815}
816
817fn plan_artifact_path(icp_root: &Path, path: &str) -> PathBuf {
818 let path = PathBuf::from(path);
819 if path.is_absolute() {
820 path
821 } else {
822 icp_root.join(path)
823 }
824}
825
826fn run_root_activation_phases(
827 receipt_scope: InstallReceiptScope<'_>,
828 options: &InstallRootOptions,
829 root_canister_id: &str,
830 manifest_path: &Path,
831 total_started_at: Instant,
832) -> Result<InstallTimingSummary, Box<dyn std::error::Error>> {
833 let mut timings = InstallTimingSummary::default();
834 let root_wasm = root_wasm_for_install_plan(
835 receipt_scope.icp_root,
836 receipt_scope.network,
837 &options.root_build_target,
838 options.deployment_plan_override.as_ref(),
839 )?;
840 let install_operation = InstallRootWasmOperation::new(
841 receipt_scope.icp_root,
842 receipt_scope.network,
843 root_canister_id,
844 root_wasm,
845 );
846 timings.install_root = receipt_scope.run_operation(&install_operation)?;
847 let pre_bootstrap_funding = EnsureRootCyclesOperation::new(
848 receipt_scope.icp_root,
849 receipt_scope.network,
850 root_canister_id,
851 "fund_root_pre_bootstrap",
852 "ensure local root minimum cycles before bootstrap",
853 "pre-bootstrap",
854 );
855 timings.fund_root = receipt_scope.run_operation(&pre_bootstrap_funding)?;
856 let manifest = load_root_release_set_manifest(manifest_path)?;
857 let stage_operation = StageReleaseSetOperation::new(
858 receipt_scope.icp_root,
859 receipt_scope.network,
860 root_canister_id,
861 manifest_path,
862 manifest,
863 );
864 timings.stage_release_set = receipt_scope.run_operation(&stage_operation)?;
865 let resume_operation = ResumeBootstrapOperation::new(receipt_scope.network, root_canister_id);
866 timings.resume_bootstrap = receipt_scope.run_operation(&resume_operation)?;
867 let wait_ready_operation = WaitRootReadyOperation::new(
868 receipt_scope.network,
869 root_canister_id,
870 options.ready_timeout_seconds,
871 );
872 let wait_ready_result = receipt_scope.run_operation(&wait_ready_operation);
873 match wait_ready_result {
874 Ok(duration) => timings.wait_ready = duration,
875 Err(err) => {
876 print_install_timing_summary(&timings, total_started_at.elapsed());
877 return Err(err);
878 }
879 }
880 let post_ready_funding = EnsureRootCyclesOperation::new(
881 receipt_scope.icp_root,
882 receipt_scope.network,
883 root_canister_id,
884 "fund_root_post_ready",
885 "ensure local root minimum cycles after ready",
886 "post-ready",
887 );
888 timings.finalize_root_funding = receipt_scope.run_operation(&post_ready_funding)?;
889 Ok(timings)
890}
891
892fn root_wasm_for_install_plan(
893 icp_root: &Path,
894 network: &str,
895 root_build_target: &str,
896 plan: Option<&DeploymentPlanV1>,
897) -> Result<PathBuf, Box<dyn std::error::Error>> {
898 let artifact_root = resolve_artifact_root(icp_root, network)?;
899 if let Some(plan) = plan {
900 let root_artifact = plan
901 .role_artifacts
902 .iter()
903 .find(|artifact| artifact.role == "root")
904 .ok_or_else(|| "deployment plan is missing root role artifact".to_string())?;
905 return Ok(plan_role_wasm_path(icp_root, &artifact_root, root_artifact));
906 }
907
908 Ok(artifact_root
909 .join(root_build_target)
910 .join(format!("{root_build_target}.wasm")))
911}
912
913#[derive(Clone, Copy)]
914struct InstallReceiptScope<'a> {
915 icp_root: &'a Path,
916 network: &'a str,
917 deployment_name: &'a str,
918 check: &'a DeploymentCheckV1,
919 execution_context: Option<&'a DeploymentExecutionContextV1>,
920}
921
922struct CompletedInstallPhase {
923 phase: &'static str,
924 attempted_action: &'static str,
925 started_at: String,
926 finished_at: Option<String>,
927 evidence: Vec<String>,
928 role_names: Vec<String>,
929}
930
931trait InstallPhaseOperation {
932 fn phase(&self) -> &'static str;
933 fn attempted_action(&self) -> &'static str;
934 fn evidence(&self) -> Vec<String>;
935 fn execute(&self) -> Result<(), Box<dyn std::error::Error>>;
936}
937
938struct ResolveRootCanisterOperation<'a> {
939 icp_root: &'a Path,
940 network: &'a str,
941 root_canister: &'a str,
942 config_path: &'a Path,
943}
944
945impl<'a> ResolveRootCanisterOperation<'a> {
946 const fn new(
947 icp_root: &'a Path,
948 network: &'a str,
949 root_canister: &'a str,
950 config_path: &'a Path,
951 ) -> Self {
952 Self {
953 icp_root,
954 network,
955 root_canister,
956 config_path,
957 }
958 }
959
960 fn evidence(&self, root_canister_id: &str) -> Vec<String> {
961 vec![
962 format!("root_target:{}", self.root_canister),
963 format!("root_canister:{root_canister_id}"),
964 ]
965 }
966
967 fn execute(&self) -> Result<String, Box<dyn std::error::Error>> {
968 ensure_root_canister_id(
969 self.icp_root,
970 self.network,
971 self.root_canister,
972 self.config_path,
973 )
974 }
975}
976
977struct BuildInstallTargetsOperation<'a> {
978 network: &'a str,
979 build_targets: Vec<String>,
980 build_profile: Option<CanisterBuildProfile>,
981 config_path: &'a Path,
982 icp_root: &'a Path,
983}
984
985impl<'a> BuildInstallTargetsOperation<'a> {
986 const fn new(
987 network: &'a str,
988 build_targets: Vec<String>,
989 build_profile: Option<CanisterBuildProfile>,
990 config_path: &'a Path,
991 icp_root: &'a Path,
992 ) -> Self {
993 Self {
994 network,
995 build_targets,
996 build_profile,
997 config_path,
998 icp_root,
999 }
1000 }
1001
1002 fn evidence(&self) -> Vec<String> {
1003 self.build_targets
1004 .iter()
1005 .map(|target| format!("build_target:{target}"))
1006 .collect()
1007 }
1008
1009 fn role_names(&self) -> Vec<String> {
1010 self.build_targets.clone()
1011 }
1012
1013 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
1014 run_canic_build_targets(
1015 self.network,
1016 &self.build_targets,
1017 self.build_profile,
1018 self.config_path,
1019 self.icp_root,
1020 )
1021 }
1022}
1023
1024struct EmitRootManifestOperation<'a> {
1025 workspace_root: &'a Path,
1026 icp_root: &'a Path,
1027 network: &'a str,
1028 config_path: &'a Path,
1029}
1030
1031impl<'a> EmitRootManifestOperation<'a> {
1032 const fn new(
1033 workspace_root: &'a Path,
1034 icp_root: &'a Path,
1035 network: &'a str,
1036 config_path: &'a Path,
1037 ) -> Self {
1038 Self {
1039 workspace_root,
1040 icp_root,
1041 network,
1042 config_path,
1043 }
1044 }
1045
1046 fn evidence(manifest_path: &Path) -> Vec<String> {
1047 vec![format!("manifest_path:{}", manifest_path.display())]
1048 }
1049
1050 fn execute(&self) -> Result<PathBuf, Box<dyn std::error::Error>> {
1051 emit_root_release_set_manifest_with_config(
1052 self.workspace_root,
1053 self.icp_root,
1054 self.network,
1055 self.config_path,
1056 )
1057 }
1058}
1059
1060struct InstallRootWasmOperation<'a> {
1061 icp_root: &'a Path,
1062 network: &'a str,
1063 root_canister_id: &'a str,
1064 root_wasm: PathBuf,
1065}
1066
1067impl<'a> InstallRootWasmOperation<'a> {
1068 const fn new(
1069 icp_root: &'a Path,
1070 network: &'a str,
1071 root_canister_id: &'a str,
1072 root_wasm: PathBuf,
1073 ) -> Self {
1074 Self {
1075 icp_root,
1076 network,
1077 root_canister_id,
1078 root_wasm,
1079 }
1080 }
1081}
1082
1083impl InstallPhaseOperation for InstallRootWasmOperation<'_> {
1084 fn phase(&self) -> &'static str {
1085 "install_root"
1086 }
1087
1088 fn attempted_action(&self) -> &'static str {
1089 "install root wasm"
1090 }
1091
1092 fn evidence(&self) -> Vec<String> {
1093 vec![
1094 format!("root_canister:{}", self.root_canister_id),
1095 format!("root_wasm:{}", self.root_wasm.display()),
1096 ]
1097 }
1098
1099 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
1100 reinstall_root_wasm(
1101 self.icp_root,
1102 self.network,
1103 self.root_canister_id,
1104 &self.root_wasm,
1105 )
1106 }
1107}
1108
1109struct EnsureRootCyclesOperation<'a> {
1110 icp_root: &'a Path,
1111 network: &'a str,
1112 root_canister_id: &'a str,
1113 phase: &'static str,
1114 attempted_action: &'static str,
1115 phase_label: &'a str,
1116}
1117
1118impl<'a> EnsureRootCyclesOperation<'a> {
1119 const fn new(
1120 icp_root: &'a Path,
1121 network: &'a str,
1122 root_canister_id: &'a str,
1123 phase: &'static str,
1124 attempted_action: &'static str,
1125 phase_label: &'a str,
1126 ) -> Self {
1127 Self {
1128 icp_root,
1129 network,
1130 root_canister_id,
1131 phase,
1132 attempted_action,
1133 phase_label,
1134 }
1135 }
1136}
1137
1138impl InstallPhaseOperation for EnsureRootCyclesOperation<'_> {
1139 fn phase(&self) -> &'static str {
1140 self.phase
1141 }
1142
1143 fn attempted_action(&self) -> &'static str {
1144 self.attempted_action
1145 }
1146
1147 fn evidence(&self) -> Vec<String> {
1148 vec![
1149 format!("root_canister:{}", self.root_canister_id),
1150 format!("minimum_cycles:{LOCAL_ROOT_MIN_READY_CYCLES}"),
1151 format!("funding_phase:{}", self.phase_label),
1152 ]
1153 }
1154
1155 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
1156 ensure_local_root_min_cycles(
1157 self.icp_root,
1158 self.network,
1159 self.root_canister_id,
1160 self.phase_label,
1161 )
1162 }
1163}
1164
1165struct ResumeBootstrapOperation<'a> {
1166 network: &'a str,
1167 root_canister_id: &'a str,
1168}
1169
1170impl<'a> ResumeBootstrapOperation<'a> {
1171 const fn new(network: &'a str, root_canister_id: &'a str) -> Self {
1172 Self {
1173 network,
1174 root_canister_id,
1175 }
1176 }
1177}
1178
1179impl InstallPhaseOperation for ResumeBootstrapOperation<'_> {
1180 fn phase(&self) -> &'static str {
1181 "resume_bootstrap"
1182 }
1183
1184 fn attempted_action(&self) -> &'static str {
1185 "resume root bootstrap"
1186 }
1187
1188 fn evidence(&self) -> Vec<String> {
1189 vec![format!("root_canister:{}", self.root_canister_id)]
1190 }
1191
1192 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
1193 resume_root_bootstrap(self.network, self.root_canister_id)
1194 }
1195}
1196
1197struct WaitRootReadyOperation<'a> {
1198 network: &'a str,
1199 root_canister_id: &'a str,
1200 timeout_seconds: u64,
1201}
1202
1203impl<'a> WaitRootReadyOperation<'a> {
1204 const fn new(network: &'a str, root_canister_id: &'a str, timeout_seconds: u64) -> Self {
1205 Self {
1206 network,
1207 root_canister_id,
1208 timeout_seconds,
1209 }
1210 }
1211}
1212
1213impl InstallPhaseOperation for WaitRootReadyOperation<'_> {
1214 fn phase(&self) -> &'static str {
1215 "wait_ready"
1216 }
1217
1218 fn attempted_action(&self) -> &'static str {
1219 "wait for root bootstrap readiness"
1220 }
1221
1222 fn evidence(&self) -> Vec<String> {
1223 vec![
1224 format!("root_canister:{}", self.root_canister_id),
1225 format!("timeout_seconds:{}", self.timeout_seconds),
1226 ]
1227 }
1228
1229 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
1230 wait_for_root_ready(self.network, self.root_canister_id, self.timeout_seconds)
1231 }
1232}
1233
1234fn write_completed_install_phase_receipt(
1235 receipt_scope: InstallReceiptScope<'_>,
1236 completed: CompletedInstallPhase,
1237) -> Result<PathBuf, Box<dyn std::error::Error>> {
1238 let role_phase_receipts = completed
1239 .role_names
1240 .iter()
1241 .filter_map(|role| {
1242 completed_phase_role_receipt(
1243 receipt_scope.check,
1244 completed.phase,
1245 role,
1246 crate::deployment_truth::RolePhaseResultV1::Applied,
1247 None,
1248 )
1249 })
1250 .collect();
1251 let receipt =
1252 receipt_scope.with_execution_context(install_deployment_truth_phase_receipt_with_result(
1253 receipt_scope.check,
1254 PhaseReceiptInput {
1255 phase: completed.phase,
1256 started_at: completed.started_at,
1257 finished_at: completed.finished_at,
1258 attempted_action: completed.attempted_action,
1259 status: crate::deployment_truth::ObservationStatusV1::Observed,
1260 evidence: completed.evidence,
1261 role_phase_receipts,
1262 operation_status: DeploymentExecutionStatusV1::Complete,
1263 command_result: DeploymentCommandResultV1::Succeeded,
1264 },
1265 ));
1266 receipt_scope.write_receipt(&receipt)
1267}
1268
1269fn completed_phase_role_receipt(
1270 check: &DeploymentCheckV1,
1271 phase: &str,
1272 role: &str,
1273 result: crate::deployment_truth::RolePhaseResultV1,
1274 error: Option<String>,
1275) -> Option<crate::deployment_truth::RolePhaseReceiptV1> {
1276 let planned = check
1277 .plan
1278 .role_artifacts
1279 .iter()
1280 .find(|artifact| artifact.role == role)?;
1281 let observed = check
1282 .inventory
1283 .observed_artifacts
1284 .iter()
1285 .find(|artifact| artifact.role == role);
1286 let artifact_digest = observed
1287 .and_then(|artifact| artifact.file_sha256.clone())
1288 .or_else(|| observed.and_then(|artifact| artifact.payload_sha256.clone()))
1289 .or_else(|| planned.observed_wasm_gz_file_sha256.clone())
1290 .or_else(|| planned.wasm_gz_sha256.clone());
1291
1292 Some(crate::deployment_truth::RolePhaseReceiptV1 {
1293 role: role.to_string(),
1294 phase: phase.to_string(),
1295 result,
1296 previous_module_hash: None,
1297 target_module_hash: planned.installed_module_hash.clone(),
1298 observed_module_hash_after: None,
1299 artifact_digest,
1300 canonical_embedded_config_sha256: planned.canonical_embedded_config_sha256.clone(),
1301 error,
1302 })
1303}
1304
1305fn write_install_state_with_deployment_truth_receipt(
1306 receipt_scope: InstallReceiptScope<'_>,
1307 network: &str,
1308 state: &InstallState,
1309) -> Result<PathBuf, Box<dyn std::error::Error>> {
1310 let started_at = current_unix_timestamp_label()?;
1311 let state_path = write_install_state(receipt_scope.icp_root, network, state)?;
1312 let completed = CompletedInstallPhase {
1313 phase: "write_install_state",
1314 attempted_action: "write local install state",
1315 started_at,
1316 finished_at: Some(current_unix_timestamp_label()?),
1317 evidence: vec![
1318 format!("install_state:{}", state_path.display()),
1319 format!("deployment:{}", state.deployment_name),
1320 format!("fleet_template:{}", state.fleet_template),
1321 format!("root_canister:{}", state.root_canister_id),
1322 ],
1323 role_names: Vec::new(),
1324 };
1325 write_completed_install_phase_receipt(receipt_scope, completed)?;
1326 Ok(state_path)
1327}
1328
1329fn write_artifact_promotion_execution_receipt_for_install(
1330 options: &InstallRootOptions,
1331 icp_root: &Path,
1332 network: &str,
1333 deployment_name: &str,
1334 check: &DeploymentCheckV1,
1335 execution_context: &DeploymentExecutionContextV1,
1336) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
1337 let Some(promotion_plan) = &options.artifact_promotion_plan_override else {
1338 return Ok(None);
1339 };
1340 let deployment_receipt =
1341 promotion_install_deployment_receipt(check, execution_context, promotion_plan)?;
1342 let provenance_report =
1343 artifact_promotion_provenance_report(ArtifactPromotionProvenanceReportRequest {
1344 report_id: format!("{}:execution-provenance", promotion_plan.plan_id),
1345 artifact_promotion_plan: promotion_plan.clone(),
1346 wasm_store_identity_report: None,
1347 wasm_store_catalog_verification: None,
1348 materialization_identity_report: None,
1349 })?;
1350 let receipt = artifact_promotion_execution_receipt(ArtifactPromotionExecutionReceiptRequest {
1351 receipt_id: format!("{}:execution-receipt", promotion_plan.plan_id),
1352 provenance_report,
1353 deployment_receipt,
1354 })?;
1355 let path =
1356 write_artifact_promotion_execution_receipt(icp_root, network, deployment_name, &receipt)?;
1357 println!(
1358 "Artifact promotion execution receipt JSON: {}",
1359 path.display()
1360 );
1361 Ok(Some(path))
1362}
1363
1364fn promotion_install_deployment_receipt(
1365 check: &DeploymentCheckV1,
1366 execution_context: &DeploymentExecutionContextV1,
1367 promotion_plan: &ArtifactPromotionPlanV1,
1368) -> Result<DeploymentReceiptV1, Box<dyn std::error::Error>> {
1369 let started_at = current_unix_timestamp_label()?;
1370 let finished_at = current_unix_timestamp_label()?;
1371 let phase = phase_receipt(
1372 "promoted_plan_install",
1373 started_at.clone(),
1374 Some(finished_at.clone()),
1375 "execute promoted deployment plan through current install runner",
1376 ObservationStatusV1::Observed,
1377 vec![
1378 format!("artifact_promotion_plan:{}", promotion_plan.plan_id),
1379 format!(
1380 "artifact_promotion_plan_digest:{}",
1381 promotion_plan.artifact_promotion_plan_digest
1382 ),
1383 format!(
1384 "promotion_plan_lineage_digest:{}",
1385 promotion_plan.promotion_plan_lineage_digest
1386 ),
1387 ],
1388 );
1389 let role_phase_receipts = promotion_plan
1390 .transform
1391 .roles
1392 .iter()
1393 .map(|role| {
1394 completed_phase_role_receipt(
1395 check,
1396 "promoted_plan_install",
1397 &role.role,
1398 crate::deployment_truth::RolePhaseResultV1::Applied,
1399 None,
1400 )
1401 .ok_or_else(|| {
1402 format!(
1403 "promoted role {} is missing from deployment plan artifacts",
1404 role.role
1405 )
1406 })
1407 })
1408 .collect::<Result<Vec<_>, _>>()?;
1409 Ok(receipt_with_execution_context(
1410 deployment_receipt_from_check_with_status(
1411 check,
1412 format!("{}:promoted_plan_install", check.check_id),
1413 DeploymentExecutionStatusV1::Complete,
1414 started_at,
1415 Some(finished_at),
1416 vec![phase],
1417 role_phase_receipts,
1418 DeploymentCommandResultV1::Succeeded,
1419 ),
1420 execution_context,
1421 ))
1422}
1423
1424impl InstallReceiptScope<'_> {
1425 fn run_operation(
1426 self,
1427 operation: &impl InstallPhaseOperation,
1428 ) -> Result<Duration, Box<dyn std::error::Error>> {
1429 self.run_phase(
1430 operation.phase(),
1431 operation.attempted_action(),
1432 operation.evidence(),
1433 || operation.execute(),
1434 )
1435 }
1436
1437 fn run_phase(
1438 self,
1439 phase: &str,
1440 attempted_action: &str,
1441 evidence: Vec<String>,
1442 run: impl FnOnce() -> Result<(), Box<dyn std::error::Error>>,
1443 ) -> Result<Duration, Box<dyn std::error::Error>> {
1444 let started_at = current_unix_timestamp_label()?;
1445 let started = Instant::now();
1446 match run() {
1447 Ok(()) => {
1448 let duration = started.elapsed();
1449 let receipt = self.with_execution_context(install_deployment_truth_phase_receipt(
1450 self.check,
1451 phase,
1452 started_at,
1453 Some(current_unix_timestamp_label()?),
1454 attempted_action,
1455 crate::deployment_truth::ObservationStatusV1::Observed,
1456 evidence,
1457 ));
1458 self.write_receipt(&receipt)?;
1459 Ok(duration)
1460 }
1461 Err(err) => {
1462 self.try_write_failed_phase_receipt(
1463 phase,
1464 started_at,
1465 attempted_action,
1466 evidence,
1467 err.as_ref(),
1468 );
1469 Err(err)
1470 }
1471 }
1472 }
1473
1474 fn with_execution_context(self, receipt: DeploymentReceiptV1) -> DeploymentReceiptV1 {
1475 match self.execution_context {
1476 Some(context) => receipt_with_execution_context(receipt, context),
1477 None => receipt,
1478 }
1479 }
1480
1481 fn write_receipt(
1482 self,
1483 receipt: &DeploymentReceiptV1,
1484 ) -> Result<PathBuf, Box<dyn std::error::Error>> {
1485 let path = write_install_deployment_truth_receipt(
1486 self.icp_root,
1487 self.network,
1488 self.deployment_name,
1489 receipt,
1490 )?;
1491 println!("Deployment truth receipt JSON: {}", path.display());
1492 Ok(path)
1493 }
1494
1495 fn try_write_failed_phase_receipt(
1496 self,
1497 phase: &str,
1498 started_at: String,
1499 attempted_action: &str,
1500 evidence: Vec<String>,
1501 err: &dyn std::error::Error,
1502 ) {
1503 let receipt = install_deployment_truth_phase_receipt_with_result(
1504 self.check,
1505 PhaseReceiptInput {
1506 phase,
1507 started_at,
1508 finished_at: Some(
1509 current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string()),
1510 ),
1511 attempted_action,
1512 status: crate::deployment_truth::ObservationStatusV1::Inconclusive,
1513 evidence,
1514 role_phase_receipts: Vec::new(),
1515 operation_status: DeploymentExecutionStatusV1::FailedAfterMutation,
1516 command_result: DeploymentCommandResultV1::Failed {
1517 code: format!("{phase}_failed"),
1518 message: err.to_string(),
1519 },
1520 },
1521 );
1522 let receipt = self.with_execution_context(receipt);
1523 if let Err(write_err) = self.write_receipt(&receipt) {
1524 eprintln!("Deployment truth receipt JSON write failed: {write_err}");
1525 }
1526 }
1527}
1528
1529pub fn check_install_deployment_truth(
1532 options: &InstallRootOptions,
1533 observed_at: impl Into<String>,
1534) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1535 let inputs = resolve_current_install_truth_inputs(options)?;
1536 current_install_deployment_truth_check_at(
1537 options,
1538 &inputs.workspace_root,
1539 &inputs.icp_root,
1540 &inputs.config_path,
1541 &inputs.deployment_name,
1542 observed_at.into(),
1543 )
1544}
1545
1546pub fn check_install_execution_preflight(
1552 options: &InstallRootOptions,
1553 observed_at: impl Into<String>,
1554) -> Result<DeploymentExecutionPreflightV1, Box<dyn std::error::Error>> {
1555 let inputs = resolve_current_install_truth_inputs(options)?;
1556 let check = current_install_deployment_truth_check_at(
1557 options,
1558 &inputs.workspace_root,
1559 &inputs.icp_root,
1560 &inputs.config_path,
1561 &inputs.deployment_name,
1562 observed_at.into(),
1563 )?;
1564 let execution_context = current_install_execution_context(
1565 &inputs.workspace_root,
1566 &inputs.icp_root,
1567 &options.network,
1568 );
1569 let executor = CurrentCliDeploymentExecutor::new(
1570 execution_context.workspace_root,
1571 execution_context.icp_root,
1572 execution_context.artifact_roots,
1573 );
1574 let preflight = deployment_execution_preflight_from_check(
1575 &check,
1576 &executor,
1577 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1578 );
1579 validate_deployment_execution_preflight_for_check(&check, &preflight)?;
1580 Ok(preflight)
1581}
1582
1583fn resolve_current_install_truth_inputs(
1584 options: &InstallRootOptions,
1585) -> Result<CurrentInstallTruthInputs, Box<dyn std::error::Error>> {
1586 let workspace_root = workspace_root()?;
1587 let icp_root = match &options.icp_root {
1588 Some(path) => path.canonicalize()?,
1589 None => icp_root()?,
1590 };
1591 let state = match options.deployment_name.as_deref() {
1592 Some(deployment) => {
1593 read_named_deployment_install_state_from_root(&icp_root, &options.network, deployment)?
1594 }
1595 None => None,
1596 };
1597 let config_path = match (options.config_path.as_deref(), state.as_ref()) {
1598 (Some(path), _) => resolve_install_config_path(
1599 &icp_root,
1600 Some(path),
1601 options.interactive_config_selection,
1602 )?,
1603 (None, Some(state)) => resolve_install_config_path(
1604 &icp_root,
1605 Some(&state.config_path),
1606 options.interactive_config_selection,
1607 )?,
1608 (None, None) => {
1609 let default_config = options
1610 .deployment_name
1611 .as_ref()
1612 .map(|deployment| default_config_path_for_deployment(deployment));
1613 resolve_install_config_path(
1614 &icp_root,
1615 default_config.as_deref(),
1616 options.interactive_config_selection,
1617 )?
1618 }
1619 };
1620 let fleet_template = configured_fleet_name(&config_path)?;
1621 let expected_fleet = options
1622 .expected_fleet
1623 .as_deref()
1624 .or_else(|| state.as_ref().map(|state| state.fleet_template.as_str()));
1625 validate_expected_fleet_name(expected_fleet, &fleet_template, &config_path)?;
1626 validate_state_name(&fleet_template)?;
1627 let deployment_name = options
1628 .deployment_name
1629 .clone()
1630 .unwrap_or_else(|| fleet_template.clone());
1631 validate_state_name(&deployment_name)?;
1632 Ok(CurrentInstallTruthInputs {
1633 workspace_root,
1634 icp_root,
1635 config_path,
1636 deployment_name,
1637 })
1638}
1639
1640fn default_config_path_for_deployment(deployment: &str) -> String {
1641 format!("fleets/{deployment}/canic.toml")
1642}
1643
1644fn current_install_deployment_truth_check_at(
1645 options: &InstallRootOptions,
1646 workspace_root: &Path,
1647 icp_root: &Path,
1648 config_path: &Path,
1649 deployment_name: &str,
1650 observed_at: String,
1651) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1652 if let Some(plan) = &options.deployment_plan_override {
1653 validate_current_install_plan_override(plan, &options.network, deployment_name)?;
1654 return current_install_deployment_truth_check_for_plan(
1655 plan,
1656 workspace_root,
1657 icp_root,
1658 config_path,
1659 deployment_name,
1660 observed_at,
1661 &options.network,
1662 );
1663 }
1664
1665 let build_profile = options
1666 .build_profile
1667 .unwrap_or_else(CanisterBuildProfile::current)
1668 .target_dir_name()
1669 .to_string();
1670
1671 check_local_deployment(&LocalDeploymentCheckRequest {
1672 deployment_name: deployment_name.to_string(),
1673 network: options.network.clone(),
1674 workspace_root: workspace_root.to_path_buf(),
1675 icp_root: icp_root.to_path_buf(),
1676 config_path: Some(config_path.to_path_buf()),
1677 observed_at,
1678 runtime_variant: options.network.clone(),
1679 build_profile,
1680 })
1681 .map_err(Into::into)
1682}
1683
1684fn current_install_deployment_truth_check_for_plan(
1685 plan: &DeploymentPlanV1,
1686 workspace_root: &Path,
1687 icp_root: &Path,
1688 config_path: &Path,
1689 deployment_name: &str,
1690 observed_at: String,
1691 network: &str,
1692) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1693 let inventory = collect_local_deployment_inventory(&LocalInventoryRequest {
1694 deployment_name: deployment_name.to_string(),
1695 network: network.to_string(),
1696 workspace_root: workspace_root.to_path_buf(),
1697 icp_root: icp_root.to_path_buf(),
1698 config_path: Some(config_path.to_path_buf()),
1699 observed_at,
1700 })?;
1701 let diff = compare_plan_to_inventory(plan, &inventory);
1702 let report = safety_report_from_diff(
1703 format!("local:{network}:{deployment_name}:report"),
1704 Some(format!("local:{network}:{deployment_name}:diff")),
1705 &diff,
1706 );
1707
1708 Ok(DeploymentCheckV1 {
1709 schema_version: crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1710 check_id: format!("local:{network}:{deployment_name}:check"),
1711 plan: plan.clone(),
1712 inventory,
1713 diff,
1714 report,
1715 })
1716}
1717
1718fn validate_current_install_plan_override(
1719 plan: &DeploymentPlanV1,
1720 network: &str,
1721 deployment_name: &str,
1722) -> Result<(), Box<dyn std::error::Error>> {
1723 if plan.schema_version != crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION {
1724 return Err(format!(
1725 "deployment plan schema mismatch: expected {}, found {}",
1726 crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
1727 plan.schema_version
1728 )
1729 .into());
1730 }
1731 if plan.deployment_identity.network != network {
1732 return Err(format!(
1733 "deployment plan network mismatch: install network {network}, plan network {}",
1734 plan.deployment_identity.network
1735 )
1736 .into());
1737 }
1738 if plan.deployment_identity.deployment_name != deployment_name {
1739 return Err(format!(
1740 "deployment plan target mismatch: install deployment {deployment_name}, plan deployment {}",
1741 plan.deployment_identity.deployment_name
1742 )
1743 .into());
1744 }
1745 Ok(())
1746}
1747
1748fn current_install_execution_context(
1749 workspace_root: &Path,
1750 icp_root: &Path,
1751 network: &str,
1752) -> DeploymentExecutionContextV1 {
1753 CurrentCliDeploymentExecutor::new(
1754 Some(workspace_root.display().to_string()),
1755 Some(icp_root.display().to_string()),
1756 current_install_artifact_roots(icp_root, network),
1757 )
1758 .execution_context()
1759}
1760
1761fn ensure_current_install_executor_capabilities(
1762 execution_context: &DeploymentExecutionContextV1,
1763) -> Result<(), Box<dyn std::error::Error>> {
1764 let missing = current_install_executor_missing_capabilities(execution_context);
1765 if missing.is_empty() {
1766 return Ok(());
1767 }
1768
1769 Err(format!(
1770 "current install executor backend {:?} is missing required capabilities: {missing:?}",
1771 execution_context.backend
1772 )
1773 .into())
1774}
1775
1776fn current_install_executor_missing_capabilities(
1777 execution_context: &DeploymentExecutionContextV1,
1778) -> Vec<DeploymentExecutorCapabilityV1> {
1779 missing_executor_capabilities(
1780 &execution_context.backend_capabilities,
1781 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1782 )
1783}
1784
1785fn current_install_artifact_roots(icp_root: &Path, network: &str) -> Vec<String> {
1786 let planned_root = planned_build_artifact_root(icp_root);
1787 let mut roots = vec![planned_root.display().to_string()];
1788 if let Ok(resolved_root) = resolve_artifact_root(icp_root, network)
1789 && resolved_root != planned_root
1790 {
1791 roots.push(resolved_root.display().to_string());
1792 }
1793 roots
1794}
1795
1796fn run_install_deployment_truth_safety_gate(
1797 options: &InstallRootOptions,
1798 workspace_root: &Path,
1799 icp_root: &Path,
1800 config_path: &Path,
1801 deployment_name: &str,
1802 execution_context: &DeploymentExecutionContextV1,
1803) -> Result<DeploymentCheckV1, Box<dyn std::error::Error>> {
1804 let truth_gate_started_at = current_unix_timestamp_label()?;
1805 let deployment_truth_check = current_install_deployment_truth_check_at(
1806 options,
1807 workspace_root,
1808 icp_root,
1809 config_path,
1810 deployment_name,
1811 truth_gate_started_at.clone(),
1812 )?;
1813 let artifact_gate_receipt = artifact_gate_phase_receipt(
1814 &deployment_truth_check,
1815 truth_gate_started_at.clone(),
1816 Some(current_unix_timestamp_label()?),
1817 );
1818 let role_receipts = artifact_gate_role_phase_receipts(&deployment_truth_check);
1819 let deployment_receipt = receipt_with_execution_context(
1820 install_deployment_truth_gate_receipt(
1821 &deployment_truth_check,
1822 truth_gate_started_at,
1823 vec![artifact_gate_receipt],
1824 role_receipts,
1825 ),
1826 execution_context,
1827 );
1828 let receipt_write = write_install_deployment_truth_receipt(
1829 icp_root,
1830 &options.network,
1831 deployment_name,
1832 &deployment_receipt,
1833 );
1834 match &receipt_write {
1835 Ok(path) => println!("Deployment truth receipt JSON: {}", path.display()),
1836 Err(err) => eprintln!("Deployment truth receipt JSON write failed: {err}"),
1837 }
1838 print_install_deployment_truth_gate(&deployment_truth_check, &deployment_receipt);
1839 enforce_install_deployment_truth_gate(&deployment_truth_check)?;
1840 receipt_write?;
1841 write_current_install_execution_preflight_receipt(
1842 icp_root,
1843 &options.network,
1844 deployment_name,
1845 &deployment_truth_check,
1846 execution_context,
1847 )?;
1848 Ok(deployment_truth_check)
1849}
1850
1851fn enforce_install_deployment_truth_gate(
1852 check: &DeploymentCheckV1,
1853) -> Result<(), Box<dyn std::error::Error>> {
1854 let blockers = install_deployment_truth_gate_blockers(check);
1855 if blockers.is_empty() {
1856 return Ok(());
1857 }
1858
1859 let details = blockers
1860 .iter()
1861 .map(|finding| deployment_truth_finding_label(finding))
1862 .collect::<Vec<_>>()
1863 .join("; ");
1864 Err(format!("deployment truth safety gate blocked install: {details}").into())
1865}
1866
1867fn install_deployment_truth_gate_blockers(check: &DeploymentCheckV1) -> Vec<&SafetyFindingV1> {
1868 check.report.hard_failures.iter().collect()
1869}
1870
1871fn print_install_deployment_truth_gate(check: &DeploymentCheckV1, receipt: &DeploymentReceiptV1) {
1872 for line in install_deployment_truth_gate_lines(check, receipt) {
1873 println!("{line}");
1874 }
1875}
1876
1877fn install_deployment_truth_gate_lines(
1878 check: &DeploymentCheckV1,
1879 receipt: &DeploymentReceiptV1,
1880) -> Vec<String> {
1881 let mut lines = vec![
1882 format!("Deployment truth: {}", check.report.summary),
1883 format!(
1884 "Deployment truth receipt: operation={} status={:?}",
1885 receipt.operation_id, receipt.operation_status
1886 ),
1887 ];
1888 for phase_receipt in &receipt.phase_receipts {
1889 lines.push(format!(
1890 "Deployment truth phase receipt: phase={} postcondition={:?}",
1891 phase_receipt.phase, phase_receipt.verified_postcondition.status
1892 ));
1893 }
1894 if !receipt.role_phase_receipts.is_empty() {
1895 lines.push(format!(
1896 "Deployment truth role receipts: {}",
1897 receipt.role_phase_receipts.len()
1898 ));
1899 }
1900 for role_receipt in &receipt.role_phase_receipts {
1901 lines.push(format!(
1902 "Deployment truth role receipt: phase={} role={} result={:?}",
1903 role_receipt.phase, role_receipt.role, role_receipt.result
1904 ));
1905 }
1906
1907 if !check.report.hard_failures.is_empty() {
1908 lines.push(format!(
1909 "Deployment truth hard failures: {}",
1910 check.report.hard_failures.len()
1911 ));
1912 }
1913 for finding in install_deployment_truth_gate_blockers(check) {
1914 lines.push(format!(
1915 "Deployment truth blocker: {}",
1916 deployment_truth_finding_label(finding)
1917 ));
1918 }
1919 if !check.report.warnings.is_empty() {
1920 lines.push(format!(
1921 "Deployment truth warnings: {}",
1922 check.report.warnings.len()
1923 ));
1924 }
1925 for finding in &check.report.warnings {
1926 lines.push(format!(
1927 "Deployment truth warning: {}",
1928 deployment_truth_finding_label(finding)
1929 ));
1930 }
1931 lines
1932}
1933
1934fn install_deployment_truth_gate_receipt(
1935 check: &DeploymentCheckV1,
1936 started_at: String,
1937 phase_receipts: Vec<crate::deployment_truth::PhaseReceiptV1>,
1938 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
1939) -> DeploymentReceiptV1 {
1940 let blockers = install_deployment_truth_gate_blockers(check);
1941 let (operation_status, command_result) = if blockers.is_empty() {
1942 (
1943 DeploymentExecutionStatusV1::Complete,
1944 DeploymentCommandResultV1::Succeeded,
1945 )
1946 } else {
1947 (
1948 DeploymentExecutionStatusV1::FailedBeforeMutation,
1949 DeploymentCommandResultV1::Failed {
1950 code: "deployment_truth_blocked".to_string(),
1951 message: check.report.summary.clone(),
1952 },
1953 )
1954 };
1955 deployment_receipt_from_check_with_status(
1956 check,
1957 format!("{}:materialize_artifacts", check.check_id),
1958 operation_status,
1959 started_at,
1960 Some(current_unix_timestamp_label().unwrap_or_else(|_| "unknown".to_string())),
1961 phase_receipts,
1962 role_phase_receipts,
1963 command_result,
1964 )
1965}
1966
1967fn write_current_install_execution_preflight_receipt(
1968 icp_root: &Path,
1969 network: &str,
1970 deployment_name: &str,
1971 check: &DeploymentCheckV1,
1972 execution_context: &DeploymentExecutionContextV1,
1973) -> Result<PathBuf, Box<dyn std::error::Error>> {
1974 let started_at = current_unix_timestamp_label()?;
1975 let executor = CurrentCliDeploymentExecutor::new(
1976 execution_context.workspace_root.clone(),
1977 execution_context.icp_root.clone(),
1978 execution_context.artifact_roots.clone(),
1979 );
1980 let preflight = deployment_execution_preflight_from_check(
1981 check,
1982 &executor,
1983 CURRENT_INSTALL_REQUIRED_CAPABILITIES,
1984 );
1985 validate_deployment_execution_preflight_for_check(check, &preflight)?;
1986 let blockers = preflight.blockers.clone();
1987 let (operation_status, command_result) = if blockers.is_empty() {
1988 (
1989 DeploymentExecutionStatusV1::Complete,
1990 DeploymentCommandResultV1::Succeeded,
1991 )
1992 } else {
1993 (
1994 DeploymentExecutionStatusV1::FailedBeforeMutation,
1995 DeploymentCommandResultV1::Failed {
1996 code: "execution_preflight_blocked".to_string(),
1997 message: "deployment execution preflight blocked current install".to_string(),
1998 },
1999 )
2000 };
2001 let finished_at = current_unix_timestamp_label()?;
2002 let receipt = receipt_with_execution_context(
2003 deployment_receipt_from_check_with_status(
2004 check,
2005 format!("{}:execution_preflight", check.check_id),
2006 operation_status,
2007 started_at.clone(),
2008 Some(finished_at.clone()),
2009 vec![phase_receipt(
2010 "execution_preflight",
2011 started_at,
2012 Some(finished_at),
2013 "validate deployment plan, authority, and executor capability readiness",
2014 crate::deployment_truth::ObservationStatusV1::Observed,
2015 current_install_execution_preflight_evidence(&preflight),
2016 )],
2017 Vec::new(),
2018 command_result,
2019 ),
2020 execution_context,
2021 );
2022 let path =
2023 write_install_deployment_truth_receipt(icp_root, network, deployment_name, &receipt)?;
2024 println!("Deployment truth receipt JSON: {}", path.display());
2025 if !blockers.is_empty() {
2026 let details = blockers
2027 .iter()
2028 .map(deployment_truth_finding_label)
2029 .collect::<Vec<_>>()
2030 .join("; ");
2031 return Err(format!("deployment execution preflight blocked install: {details}").into());
2032 }
2033 Ok(path)
2034}
2035
2036struct StageReleaseSetOperation<'a> {
2037 icp_root: &'a Path,
2038 network: &'a str,
2039 root_canister_id: &'a str,
2040 manifest_path: &'a Path,
2041 manifest: RootReleaseSetManifest,
2042}
2043
2044impl<'a> StageReleaseSetOperation<'a> {
2045 const fn new(
2046 icp_root: &'a Path,
2047 network: &'a str,
2048 root_canister_id: &'a str,
2049 manifest_path: &'a Path,
2050 manifest: RootReleaseSetManifest,
2051 ) -> Self {
2052 Self {
2053 icp_root,
2054 network,
2055 root_canister_id,
2056 manifest_path,
2057 manifest,
2058 }
2059 }
2060}
2061
2062impl InstallPhaseOperation for StageReleaseSetOperation<'_> {
2063 fn phase(&self) -> &'static str {
2064 "stage_release_set"
2065 }
2066
2067 fn attempted_action(&self) -> &'static str {
2068 "stage root release set"
2069 }
2070
2071 fn evidence(&self) -> Vec<String> {
2072 current_install_staging_evidence(self.root_canister_id, self.manifest_path, &self.manifest)
2073 }
2074
2075 fn execute(&self) -> Result<(), Box<dyn std::error::Error>> {
2076 stage_root_release_set(
2077 self.icp_root,
2078 self.network,
2079 self.root_canister_id,
2080 &self.manifest,
2081 )
2082 }
2083}
2084
2085fn current_install_execution_preflight_evidence(
2086 preflight: &crate::deployment_truth::DeploymentExecutionPreflightV1,
2087) -> Vec<String> {
2088 let mut evidence = vec![
2089 format!("execution_preflight_status:{:?}", preflight.status),
2090 format!("authority_plan:{}", preflight.authority_plan_id),
2091 format!("planned_phases:{}", preflight.planned_phases.len()),
2092 format!(
2093 "required_capabilities:{}",
2094 preflight.required_capabilities.len()
2095 ),
2096 format!(
2097 "missing_capabilities:{}",
2098 preflight.missing_capabilities.len()
2099 ),
2100 format!("blockers:{}", preflight.blockers.len()),
2101 ];
2102 evidence.extend(
2103 preflight
2104 .missing_capabilities
2105 .iter()
2106 .map(|capability| format!("missing_capability:{capability:?}")),
2107 );
2108 evidence.extend(
2109 preflight
2110 .blockers
2111 .iter()
2112 .map(|finding| format!("blocker:{}:{}", finding.code, finding.message)),
2113 );
2114 evidence
2115}
2116
2117fn current_install_staging_evidence(
2118 root_canister_id: &str,
2119 manifest_path: &Path,
2120 manifest: &RootReleaseSetManifest,
2121) -> Vec<String> {
2122 let mut evidence = vec![
2123 format!("root_canister:{root_canister_id}"),
2124 format!("manifest_path:{}", manifest_path.display()),
2125 format!("release_version:{}", manifest.release_version),
2126 ];
2127 let staging_receipts = current_install_staging_receipts(root_canister_id, manifest);
2128 evidence.extend(staging_receipt_evidence(&staging_receipts));
2129 evidence
2130}
2131
2132fn current_install_staging_receipts(
2133 root_canister_id: &str,
2134 manifest: &RootReleaseSetManifest,
2135) -> Vec<StagingReceiptV1> {
2136 manifest
2137 .entries
2138 .iter()
2139 .map(|entry| StagingReceiptV1 {
2140 schema_version: crate::deployment_truth::DEPLOYMENT_TRUTH_SCHEMA_VERSION,
2141 role: entry.role.clone(),
2142 artifact_identity: format!(
2143 "{}:{}:{}",
2144 entry.template_id, manifest.release_version, entry.payload_sha256_hex
2145 ),
2146 transport: ArtifactTransportV1::WasmStore,
2147 wasm_store_locator: Some(format!("root:{root_canister_id}:bootstrap")),
2148 prepared_chunk_hashes: entry.chunk_sha256_hex.clone(),
2149 published_chunk_count: entry.chunk_sha256_hex.len(),
2150 verified_postcondition: crate::deployment_truth::VerifiedPostconditionV1 {
2151 status: ObservationStatusV1::Observed,
2152 evidence: vec![
2153 format!("payload_sha256:{}", entry.payload_sha256_hex),
2154 format!("payload_size_bytes:{}", entry.payload_size_bytes),
2155 format!("chunk_size_bytes:{}", entry.chunk_size_bytes),
2156 format!("chunk_count:{}", entry.chunk_sha256_hex.len()),
2157 ],
2158 },
2159 })
2160 .collect()
2161}
2162
2163fn install_deployment_truth_phase_receipt(
2164 check: &DeploymentCheckV1,
2165 phase: &str,
2166 started_at: String,
2167 finished_at: Option<String>,
2168 attempted_action: &str,
2169 status: crate::deployment_truth::ObservationStatusV1,
2170 evidence: Vec<String>,
2171) -> DeploymentReceiptV1 {
2172 install_deployment_truth_phase_receipt_with_result(
2173 check,
2174 PhaseReceiptInput {
2175 phase,
2176 started_at,
2177 finished_at,
2178 attempted_action,
2179 status,
2180 evidence,
2181 role_phase_receipts: Vec::new(),
2182 operation_status: DeploymentExecutionStatusV1::Complete,
2183 command_result: DeploymentCommandResultV1::Succeeded,
2184 },
2185 )
2186}
2187
2188fn install_deployment_truth_phase_receipt_with_result(
2189 check: &DeploymentCheckV1,
2190 input: PhaseReceiptInput<'_>,
2191) -> DeploymentReceiptV1 {
2192 deployment_receipt_from_check_with_status(
2193 check,
2194 format!("{}:{}", check.check_id, input.phase),
2195 input.operation_status,
2196 input.started_at.clone(),
2197 input.finished_at.clone(),
2198 vec![phase_receipt(
2199 input.phase,
2200 input.started_at,
2201 input.finished_at,
2202 input.attempted_action,
2203 input.status,
2204 input.evidence,
2205 )],
2206 input.role_phase_receipts,
2207 input.command_result,
2208 )
2209}
2210
2211fn receipt_with_execution_context(
2212 mut receipt: DeploymentReceiptV1,
2213 execution_context: &DeploymentExecutionContextV1,
2214) -> DeploymentReceiptV1 {
2215 receipt.execution_context = Some(execution_context.clone());
2216 receipt
2217}
2218
2219struct PhaseReceiptInput<'a> {
2220 phase: &'a str,
2221 started_at: String,
2222 finished_at: Option<String>,
2223 attempted_action: &'a str,
2224 status: crate::deployment_truth::ObservationStatusV1,
2225 evidence: Vec<String>,
2226 role_phase_receipts: Vec<crate::deployment_truth::RolePhaseReceiptV1>,
2227 operation_status: DeploymentExecutionStatusV1,
2228 command_result: DeploymentCommandResultV1,
2229}
2230
2231fn write_install_deployment_truth_receipt(
2232 icp_root: &Path,
2233 network: &str,
2234 deployment_name: &str,
2235 receipt: &DeploymentReceiptV1,
2236) -> Result<PathBuf, Box<dyn std::error::Error>> {
2237 let path = install_deployment_truth_receipt_path(icp_root, network, deployment_name, receipt)?;
2238 if let Some(parent) = path.parent() {
2239 fs::create_dir_all(parent)?;
2240 }
2241 let mut bytes = serde_json::to_vec_pretty(receipt)?;
2242 bytes.push(b'\n');
2243 fs::write(&path, bytes)?;
2244 Ok(path)
2245}
2246
2247fn write_artifact_promotion_execution_receipt(
2248 icp_root: &Path,
2249 network: &str,
2250 deployment_name: &str,
2251 receipt: &ArtifactPromotionExecutionReceiptV1,
2252) -> Result<PathBuf, Box<dyn std::error::Error>> {
2253 let path =
2254 artifact_promotion_execution_receipt_path(icp_root, network, deployment_name, receipt)?;
2255 if let Some(parent) = path.parent() {
2256 fs::create_dir_all(parent)?;
2257 }
2258 let mut bytes = serde_json::to_vec_pretty(receipt)?;
2259 bytes.push(b'\n');
2260 fs::write(&path, bytes)?;
2261 Ok(path)
2262}
2263
2264fn artifact_promotion_execution_receipt_path(
2265 icp_root: &Path,
2266 network: &str,
2267 deployment_name: &str,
2268 receipt: &ArtifactPromotionExecutionReceiptV1,
2269) -> Result<PathBuf, Box<dyn std::error::Error>> {
2270 validate_network_name(network)?;
2271 validate_state_name(deployment_name)?;
2272 let file_stem = format!(
2273 "{}-{}",
2274 safe_deployment_truth_path_label(&receipt.started_at),
2275 safe_deployment_truth_path_label(&receipt.receipt_id)
2276 );
2277 Ok(
2278 artifact_promotion_execution_receipts_dir(icp_root, network, deployment_name)?
2279 .join(format!("{file_stem}.json")),
2280 )
2281}
2282
2283fn artifact_promotion_execution_receipts_dir(
2284 icp_root: &Path,
2285 network: &str,
2286 deployment_name: &str,
2287) -> Result<PathBuf, Box<dyn std::error::Error>> {
2288 validate_network_name(network)?;
2289 validate_state_name(deployment_name)?;
2290 Ok(icp_root
2291 .join(".canic")
2292 .join(network)
2293 .join("artifact-promotion-execution-receipts")
2294 .join(deployment_name))
2295}
2296
2297fn install_deployment_truth_receipt_path(
2298 icp_root: &Path,
2299 network: &str,
2300 deployment_name: &str,
2301 receipt: &DeploymentReceiptV1,
2302) -> Result<PathBuf, Box<dyn std::error::Error>> {
2303 validate_network_name(network)?;
2304 validate_state_name(deployment_name)?;
2305 let file_stem = format!(
2306 "{}-{}",
2307 safe_deployment_truth_path_label(&receipt.started_at),
2308 safe_deployment_truth_path_label(&receipt.operation_id)
2309 );
2310 Ok(
2311 install_deployment_truth_receipts_dir(icp_root, network, deployment_name)?
2312 .join(format!("{file_stem}.json")),
2313 )
2314}
2315
2316pub fn latest_deployment_truth_receipt_path_from_root(
2318 icp_root: &Path,
2319 network: &str,
2320 deployment_name: &str,
2321) -> Result<Option<PathBuf>, Box<dyn std::error::Error>> {
2322 let dir = install_deployment_truth_receipts_dir(icp_root, network, deployment_name)?;
2323 if !dir.is_dir() {
2324 return Ok(None);
2325 }
2326
2327 let mut latest = None;
2328 for entry in fs::read_dir(dir)? {
2329 let path = entry?.path();
2330 if !path.is_file()
2331 || path
2332 .extension()
2333 .is_none_or(|ext| !ext.eq_ignore_ascii_case("json"))
2334 {
2335 continue;
2336 }
2337 if latest.as_ref().is_none_or(|current| path > *current) {
2338 latest = Some(path);
2339 }
2340 }
2341 Ok(latest)
2342}
2343
2344fn install_deployment_truth_receipts_dir(
2345 icp_root: &Path,
2346 network: &str,
2347 deployment_name: &str,
2348) -> Result<PathBuf, Box<dyn std::error::Error>> {
2349 validate_network_name(network)?;
2350 validate_state_name(deployment_name)?;
2351 Ok(icp_root
2352 .join(".canic")
2353 .join(network)
2354 .join("deployment-receipts")
2355 .join(deployment_name))
2356}
2357
2358fn safe_deployment_truth_path_label(value: &str) -> String {
2359 let label = value
2360 .chars()
2361 .map(|ch| {
2362 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
2363 ch
2364 } else {
2365 '_'
2366 }
2367 })
2368 .collect::<String>();
2369 if label.is_empty() {
2370 "unknown".to_string()
2371 } else {
2372 label
2373 }
2374}
2375
2376fn deployment_truth_finding_label(finding: &SafetyFindingV1) -> String {
2377 let subject = finding
2378 .subject
2379 .as_ref()
2380 .map_or_else(|| "<none>".to_string(), Clone::clone);
2381 format!(
2382 "{}:{}:{}: {}",
2383 deployment_truth_finding_source(&finding.code),
2384 finding.code,
2385 subject,
2386 finding.message
2387 )
2388}
2389
2390fn deployment_truth_finding_source(code: &str) -> &'static str {
2391 match code {
2392 "plan_assumption" => "plan",
2393 "observation_gap" => "inventory",
2394 _ => "diff",
2395 }
2396}
2397
2398fn validate_expected_fleet_name(
2399 expected: Option<&str>,
2400 actual: &str,
2401 config_path: &Path,
2402) -> Result<(), Box<dyn std::error::Error>> {
2403 let Some(expected) = expected else {
2404 return Ok(());
2405 };
2406 if expected == actual {
2407 return Ok(());
2408 }
2409 Err(format!(
2410 "install requested fleet {expected}, but {} declares [fleet].name = {actual:?}",
2411 config_path.display()
2412 )
2413 .into())
2414}
2415
2416fn ensure_root_canister_id(
2417 icp_root: &Path,
2418 network: &str,
2419 root_canister: &str,
2420 config_path: &Path,
2421) -> Result<String, Box<dyn std::error::Error>> {
2422 if Principal::from_text(root_canister).is_ok() {
2423 return Ok(root_canister.to_string());
2424 }
2425
2426 match resolve_root_canister_id(icp_root, network, root_canister) {
2427 Ok(canister_id) => return Ok(canister_id),
2428 Err(err) if !is_missing_canister_id_error(&err.to_string()) => return Err(err),
2429 Err(_) => {}
2430 }
2431
2432 let mut create = icp_canister_command_in_network(icp_root);
2433 add_create_root_target(&mut create, root_canister);
2434 add_local_root_create_cycles_arg(&mut create, config_path, network)?;
2435 add_icp_environment_target(&mut create, network);
2436 let output = run_command_stdout(&mut create)?;
2437 if let Some(canister_id) = parse_created_canister_id(&output) {
2438 return Ok(canister_id);
2439 }
2440
2441 resolve_root_canister_id(icp_root, network, root_canister).map_err(|_| {
2442 format!(
2443 "created root canister target '{root_canister}', but ICP CLI still has no canister ID for environment '{network}' under ICP root {}\nExpected project-local state under {}/.icp/{network}. If another foreground replica is reachable, stop it and restart with `canic replica start --background` from this Canic project.",
2444 icp_root.display(),
2445 icp_root.display(),
2446 )
2447 .into()
2448 })
2449}
2450
2451fn parse_created_canister_id(output: &str) -> Option<String> {
2452 if let Ok(value) = serde_json::from_str::<JsonValue>(output) {
2453 return parse_canister_id_json(&value);
2454 }
2455
2456 output
2457 .lines()
2458 .map(str::trim)
2459 .find(|line| Principal::from_text(*line).is_ok())
2460 .map(ToString::to_string)
2461}
2462
2463fn parse_canister_id_json(value: &JsonValue) -> Option<String> {
2464 match value {
2465 JsonValue::String(text) if Principal::from_text(text).is_ok() => Some(text.clone()),
2466 JsonValue::Array(values) => values.iter().find_map(parse_canister_id_json),
2467 JsonValue::Object(object) => ["canister_id", "id", "principal"]
2468 .iter()
2469 .filter_map(|key| object.get(*key))
2470 .find_map(parse_canister_id_json),
2471 _ => None,
2472 }
2473}
2474
2475fn add_create_root_target(command: &mut Command, root_canister: &str) {
2476 if env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV).is_some() {
2477 command.args(["create", "--detached", "--json"]);
2478 } else {
2479 command.args(["create", root_canister, "--json"]);
2480 }
2481}
2482
2483fn is_missing_canister_id_error(message: &str) -> bool {
2484 message.contains("failed to lookup canister ID")
2485 || message.contains("could not find ID for canister")
2486 || message.contains("Canister ID is missing")
2487}
2488
2489fn reinstall_root_wasm(
2490 icp_root: &Path,
2491 network: &str,
2492 root_canister: &str,
2493 root_wasm: &Path,
2494) -> Result<(), Box<dyn std::error::Error>> {
2495 let mut install = icp_canister_command_in_network(icp_root);
2496 install.args(["install", root_canister, "--mode=reinstall", "-y", "--wasm"]);
2497 install.arg(root_wasm);
2498 install.args(["--args", &root_init_args(root_wasm)?]);
2499 add_icp_environment_target(&mut install, network);
2500 run_command(&mut install)
2501}
2502
2503fn root_init_args(root_wasm: &Path) -> Result<String, Box<dyn std::error::Error>> {
2504 let wasm = std::fs::read(root_wasm)?;
2505 Ok(format!(
2506 "(variant {{ PrimeWithModuleHash = {} }})",
2507 idl_blob(&wasm_hash(&wasm))
2508 ))
2509}
2510
2511fn idl_blob(bytes: &[u8]) -> String {
2512 let mut encoded = String::from("blob \"");
2513 for byte in bytes {
2514 use std::fmt::Write as _;
2515 let _ = write!(encoded, "\\{byte:02X}");
2516 }
2517 encoded.push('"');
2518 encoded
2519}
2520
2521fn build_install_state(
2523 options: &InstallRootOptions,
2524 workspace_root: &Path,
2525 icp_root: &Path,
2526 config_path: &Path,
2527 release_set_manifest_path: &Path,
2528 identity: (&str, &str),
2529 root_canister_id: &str,
2530) -> Result<InstallState, Box<dyn std::error::Error>> {
2531 let (deployment_name, fleet_name) = identity;
2532 let timestamp = current_unix_secs()?;
2533 Ok(InstallState {
2534 schema_version: INSTALL_STATE_SCHEMA_VERSION,
2535 deployment_name: deployment_name.to_string(),
2536 fleet_template: fleet_name.to_string(),
2537 created_at_unix_secs: timestamp,
2538 updated_at_unix_secs: timestamp,
2539 network: options.network.clone(),
2540 root_target: options.root_canister.clone(),
2541 root_canister_id: root_canister_id.to_string(),
2542 root_verification: RootVerificationStatus::Verified,
2543 root_build_target: options.root_build_target.clone(),
2544 workspace_root: workspace_root.display().to_string(),
2545 icp_root: icp_root.display().to_string(),
2546 config_path: config_path.display().to_string(),
2547 release_set_manifest_path: release_set_manifest_path.display().to_string(),
2548 })
2549}
2550
2551fn resolve_root_canister_id(
2553 icp_root: &Path,
2554 network: &str,
2555 root_canister: &str,
2556) -> Result<String, Box<dyn std::error::Error>> {
2557 if Principal::from_text(root_canister).is_ok() {
2558 return Ok(root_canister.to_string());
2559 }
2560
2561 let mut command = icp_canister_command_in_network(icp_root);
2562 command.args(["status", root_canister, "--json"]);
2563 add_icp_environment_target(&mut command, network);
2564 let output = run_command_stdout(&mut command)?;
2565 parse_created_canister_id(&output).ok_or_else(|| {
2566 format!("could not parse root canister id from ICP status JSON output: {output}").into()
2567 })
2568}
2569
2570fn current_unix_secs() -> Result<u64, Box<dyn std::error::Error>> {
2572 Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
2573}
2574
2575fn current_unix_timestamp_label() -> Result<String, Box<dyn std::error::Error>> {
2576 Ok(format!("unix:{}", current_unix_secs()?))
2577}
2578
2579const fn deployment_root_verification_state(
2580 status: &RootVerificationStatus,
2581) -> DeploymentRootVerificationStateV1 {
2582 match status {
2583 RootVerificationStatus::Verified => DeploymentRootVerificationStateV1::Verified,
2584 RootVerificationStatus::NotVerified => DeploymentRootVerificationStateV1::NotVerified,
2585 }
2586}
2587
2588const fn verified_root_state_transition(
2589 previous: DeploymentRootVerificationStateV1,
2590) -> DeploymentRootVerificationStateTransitionV1 {
2591 match previous {
2592 DeploymentRootVerificationStateV1::NotVerified => {
2593 DeploymentRootVerificationStateTransitionV1::PromotedNotVerifiedToVerified
2594 }
2595 DeploymentRootVerificationStateV1::Verified => {
2596 DeploymentRootVerificationStateTransitionV1::NoStateChange
2597 }
2598 }
2599}
2600
2601fn write_verified_root_state_if_unchanged(
2602 icp_root: &Path,
2603 network: &str,
2604 state: &InstallState,
2605 expected_digest_before: &str,
2606) -> Result<String, Box<dyn std::error::Error>> {
2607 let path = deployment_install_state_path(icp_root, network, &state.deployment_name);
2608 let current_digest = file_sha256_hex(&path)?;
2609 if current_digest != expected_digest_before {
2610 return Err(format!(
2611 "deployment root verification state changed before write: expected {expected_digest_before}, found {current_digest}"
2612 )
2613 .into());
2614 }
2615 write_install_state(icp_root, network, state)?;
2616 file_sha256_hex(&path)
2617}
2618
2619fn file_sha256_hex(path: &Path) -> Result<String, Box<dyn std::error::Error>> {
2620 Ok(bytes_sha256_hex(&fs::read(path)?))
2621}
2622
2623fn bytes_sha256_hex(bytes: &[u8]) -> String {
2624 let digest = Sha256::digest(bytes);
2625 let mut hex = String::with_capacity(digest.len() * 2);
2626 for byte in digest {
2627 use std::fmt::Write as _;
2628 let _ = write!(&mut hex, "{byte:02x}");
2629 }
2630 hex
2631}
2632
2633fn run_canic_build_targets(
2635 network: &str,
2636 targets: &[String],
2637 build_profile: Option<CanisterBuildProfile>,
2638 config_path: &Path,
2639 icp_root: &Path,
2640) -> Result<(), Box<dyn std::error::Error>> {
2641 let _env = BuildEnvGuard::apply(network, config_path, icp_root);
2642 let profile = build_profile.unwrap_or_else(CanisterBuildProfile::current);
2643 if let Some(context) = current_workspace_build_context_once(profile)? {
2644 for line in context.lines() {
2645 println!("{line}");
2646 }
2647 println!("config: {}", config_path.display());
2648 println!(
2649 "artifacts: {}",
2650 planned_build_artifact_root(icp_root).display()
2651 );
2652 println!();
2653 }
2654
2655 fs::create_dir_all(planned_build_artifact_root(icp_root))?;
2656 println!("Building {} canisters", targets.len());
2657 println!();
2658 let headers = ["CANISTER", "PROGRESS", "WASM", "ELAPSED"];
2659 let planned_rows = targets
2660 .iter()
2661 .map(|target| {
2662 [
2663 target.clone(),
2664 progress_bar(targets.len(), targets.len(), 10),
2665 "000.00 MiB (gz 000.00 MiB)".to_string(),
2666 "0.00s".to_string(),
2667 ]
2668 })
2669 .collect::<Vec<_>>();
2670 let alignments = [
2671 ColumnAlign::Left,
2672 ColumnAlign::Left,
2673 ColumnAlign::Right,
2674 ColumnAlign::Right,
2675 ];
2676 let widths = table_widths(&headers, &planned_rows);
2677 println!("{}", render_table_row(&headers, &widths, &alignments));
2678 println!("{}", render_separator(&widths));
2679
2680 for (index, target) in targets.iter().enumerate() {
2681 let started_at = Instant::now();
2682 let output = build_current_workspace_canister_artifact(target, profile)
2683 .map_err(|err| format!("artifact build failed for {target}: {err}"))?;
2684 let elapsed = started_at.elapsed();
2685 let artifact_size = wasm_artifact_size(&output.wasm_path, &output.wasm_gz_path)?;
2686
2687 let row = [
2688 target.clone(),
2689 progress_bar(index + 1, targets.len(), 10),
2690 artifact_size,
2691 format!("{:.2}s", elapsed.as_secs_f64()),
2692 ];
2693 println!("{}", render_table_row(&row, &widths, &alignments));
2694 }
2695
2696 println!();
2697 Ok(())
2698}
2699
2700fn planned_build_artifact_root(icp_root: &Path) -> PathBuf {
2701 icp_root.join(".icp/local/canisters")
2702}
2703
2704fn wasm_artifact_size(
2705 wasm_path: &Path,
2706 wasm_gz_path: &Path,
2707) -> Result<String, Box<dyn std::error::Error>> {
2708 let wasm_bytes = Some(std::fs::metadata(wasm_path)?.len());
2709 let gzip_bytes = std::fs::metadata(wasm_gz_path)
2710 .ok()
2711 .map(|metadata| metadata.len());
2712 Ok(wasm_size_label(wasm_bytes, gzip_bytes))
2713}
2714
2715struct BuildEnvGuard {
2716 previous_network: Option<OsString>,
2717 previous_config_path: Option<OsString>,
2718 previous_icp_root: Option<OsString>,
2719 previous_local_network_url: Option<OsString>,
2720 previous_local_root_key: Option<OsString>,
2721}
2722
2723impl BuildEnvGuard {
2724 fn apply(network: &str, config_path: &Path, icp_root: &Path) -> Self {
2725 let guard = Self {
2726 previous_network: env::var_os("ICP_ENVIRONMENT"),
2727 previous_config_path: env::var_os("CANIC_CONFIG_PATH"),
2728 previous_icp_root: env::var_os("CANIC_ICP_ROOT"),
2729 previous_local_network_url: env::var_os(CANIC_ICP_LOCAL_NETWORK_URL_ENV),
2730 previous_local_root_key: env::var_os(CANIC_ICP_LOCAL_ROOT_KEY_ENV),
2731 };
2732 set_env("ICP_ENVIRONMENT", network);
2733 set_env("CANIC_CONFIG_PATH", config_path);
2734 set_env("CANIC_ICP_ROOT", icp_root);
2735 if let Some(target) = local_replica_icp_target(network, icp_root) {
2736 set_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV, target.url);
2737 set_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV, target.root_key);
2738 } else {
2739 remove_env(CANIC_ICP_LOCAL_NETWORK_URL_ENV);
2740 remove_env(CANIC_ICP_LOCAL_ROOT_KEY_ENV);
2741 }
2742 guard
2743 }
2744}
2745
2746impl Drop for BuildEnvGuard {
2747 fn drop(&mut self) {
2748 restore_env("ICP_ENVIRONMENT", self.previous_network.take());
2749 restore_env("CANIC_CONFIG_PATH", self.previous_config_path.take());
2750 restore_env("CANIC_ICP_ROOT", self.previous_icp_root.take());
2751 restore_env(
2752 CANIC_ICP_LOCAL_NETWORK_URL_ENV,
2753 self.previous_local_network_url.take(),
2754 );
2755 restore_env(
2756 CANIC_ICP_LOCAL_ROOT_KEY_ENV,
2757 self.previous_local_root_key.take(),
2758 );
2759 }
2760}
2761
2762struct LocalReplicaIcpTarget {
2763 url: String,
2764 root_key: String,
2765}
2766
2767fn local_replica_icp_target(network: &str, icp_root: &Path) -> Option<LocalReplicaIcpTarget> {
2768 if !replica_query::should_use_local_replica_query(Some(network)) {
2769 return None;
2770 }
2771 if icp_ping(icp_root, network).unwrap_or(false) {
2772 return None;
2773 }
2774 let root_key = replica_query::local_replica_root_key_from_root(Some(network), icp_root)
2775 .ok()
2776 .flatten()?;
2777 Some(LocalReplicaIcpTarget {
2778 url: replica_query::local_replica_endpoint_from_root(Some(network), icp_root),
2779 root_key,
2780 })
2781}
2782
2783fn set_env<K, V>(key: K, value: V)
2784where
2785 K: AsRef<std::ffi::OsStr>,
2786 V: AsRef<std::ffi::OsStr>,
2787{
2788 unsafe {
2791 env::set_var(key, value);
2792 }
2793}
2794
2795fn remove_env<K>(key: K)
2796where
2797 K: AsRef<std::ffi::OsStr>,
2798{
2799 unsafe {
2802 env::remove_var(key);
2803 }
2804}
2805
2806fn restore_env(key: &str, value: Option<OsString>) {
2807 if let Some(value) = value {
2809 set_env(key, value);
2810 } else {
2811 remove_env(key);
2812 }
2813}
2814
2815fn add_local_root_create_cycles_arg(
2816 command: &mut Command,
2817 config_path: &Path,
2818 network: &str,
2819) -> Result<(), Box<dyn std::error::Error>> {
2820 if network != "local" {
2821 return Ok(());
2822 }
2823
2824 let cycles = configured_local_root_create_cycles(config_path)?;
2825 command.args(["--cycles", &cycles.to_string()]);
2826 Ok(())
2827}
2828
2829fn ensure_local_root_min_cycles(
2830 icp_root: &Path,
2831 network: &str,
2832 root_canister: &str,
2833 phase: &str,
2834) -> Result<(), Box<dyn std::error::Error>> {
2835 if network != "local" {
2836 return Ok(());
2837 }
2838
2839 let current = query_root_cycle_balance(network, root_canister)?;
2840 if current >= LOCAL_ROOT_MIN_READY_CYCLES {
2841 return Ok(());
2842 }
2843
2844 let amount = LOCAL_ROOT_MIN_READY_CYCLES.saturating_sub(current);
2845 let mut command = icp_canister_command_in_network(icp_root);
2846 command
2847 .args(["top-up", "--amount"])
2848 .arg(amount.to_string())
2849 .arg(root_canister);
2850 add_icp_environment_target(&mut command, network);
2851 run_command(&mut command)?;
2852 println!(
2853 "Local root cycles ({phase}): topped up {} ({} -> {} target)",
2854 crate::format::cycles_tc(amount),
2855 crate::format::cycles_tc(current),
2856 crate::format::cycles_tc(LOCAL_ROOT_MIN_READY_CYCLES)
2857 );
2858 Ok(())
2859}
2860
2861fn query_root_cycle_balance(
2862 network: &str,
2863 root_canister: &str,
2864) -> Result<u128, Box<dyn std::error::Error>> {
2865 let output = icp_query_on_network(
2866 network,
2867 root_canister,
2868 protocol::CANIC_CYCLE_BALANCE,
2869 None,
2870 Some("json"),
2871 )?;
2872 parse_cycle_balance_response(&output).ok_or_else(|| {
2873 format!(
2874 "could not parse {root_canister} {} response: {output}",
2875 protocol::CANIC_CYCLE_BALANCE
2876 )
2877 .into()
2878 })
2879}
2880
2881fn progress_bar(current: usize, total: usize, width: usize) -> String {
2882 if total == 0 || width == 0 {
2883 return "[] 0/0".to_string();
2884 }
2885
2886 let filled = current.saturating_mul(width).div_ceil(total);
2887 let filled = filled.min(width);
2888 format!(
2889 "[{}{}] {current}/{total}",
2890 "#".repeat(filled),
2891 " ".repeat(width - filled)
2892 )
2893}
2894
2895fn ensure_icp_environment_ready(
2897 icp_root: &Path,
2898 network: &str,
2899) -> Result<(), Box<dyn std::error::Error>> {
2900 if icp_ping(icp_root, network)? {
2901 return Ok(());
2902 }
2903 if replica_query::should_use_local_replica_query(Some(network))
2904 && replica_query::local_replica_status_reachable_from_root(Some(network), icp_root)
2905 {
2906 println!(
2907 "Replica reachable via HTTP status endpoint even though ICP CLI reports network '{network}' stopped; continuing from ICP root {}.",
2908 icp_root.display()
2909 );
2910 return Ok(());
2911 }
2912
2913 Err(format!(
2914 "icp environment is not running for network '{network}'\nStart the target replica in another terminal with `canic replica start` and rerun."
2915 )
2916 .into())
2917}
2918
2919fn icp_ping(icp_root: &Path, network: &str) -> Result<bool, Box<dyn std::error::Error>> {
2921 Ok(icp::default_command_in(icp_root)
2922 .args(["network", "ping", network])
2923 .output()?
2924 .status
2925 .success())
2926}
2927
2928fn print_install_timing_summary(timings: &InstallTimingSummary, total: Duration) {
2929 println!("Install timing summary:");
2930 println!("{}", render_install_timing_summary(timings, total));
2931}
2932
2933fn render_install_timing_summary(timings: &InstallTimingSummary, total: Duration) -> String {
2934 let rows = [
2935 timing_row("create_canisters", timings.create_canisters),
2936 timing_row("build_all", timings.build_all),
2937 timing_row("emit_manifest", timings.emit_manifest),
2938 timing_row("install_root", timings.install_root),
2939 timing_row("fund_root", timings.fund_root),
2940 timing_row("stage_release_set", timings.stage_release_set),
2941 timing_row("resume_bootstrap", timings.resume_bootstrap),
2942 timing_row("wait_ready", timings.wait_ready),
2943 timing_row("finalize_root_funding", timings.finalize_root_funding),
2944 timing_row("total", total),
2945 ];
2946 render_table(
2947 &["PHASE", "ELAPSED"],
2948 &rows,
2949 &[ColumnAlign::Left, ColumnAlign::Right],
2950 )
2951}
2952
2953fn timing_row(label: &str, duration: Duration) -> [String; 2] {
2954 [label.to_string(), format!("{:.2}s", duration.as_secs_f64())]
2955}
2956
2957fn print_install_result_summary(
2959 network: &str,
2960 deployment: &str,
2961 fleet_template: &str,
2962 state_path: &Path,
2963) {
2964 println!("Install result:");
2965 println!("{:<14} success", "status");
2966 println!("{:<14} {}", "deployment", deployment);
2967 println!("{:<14} {}", "fleet_template", fleet_template);
2968 println!("{:<14} {}", "install_state", state_path.display());
2969 println!(
2970 "{:<14} canic list {} --network {}",
2971 "smoke_check", deployment, network
2972 );
2973}
2974
2975fn run_command(command: &mut Command) -> Result<(), Box<dyn std::error::Error>> {
2977 icp::run_status(command).map_err(Into::into)
2978}
2979
2980fn run_command_stdout(command: &mut Command) -> Result<String, Box<dyn std::error::Error>> {
2982 icp::run_output(command).map_err(Into::into)
2983}
2984
2985fn icp_command_on_network(network: &str) -> Command {
2988 let mut command = icp::default_command();
2989 command.env("ICP_ENVIRONMENT", network);
2990 command
2991}
2992
2993fn icp_command_in_network(icp_root: &Path, network: &str) -> Command {
2995 let mut command = icp::default_command_in(icp_root);
2996 command.env("ICP_ENVIRONMENT", network);
2997 command
2998}
2999
3000fn icp_canister_command_in_network(icp_root: &Path) -> Command {
3002 let mut command = icp::default_command_in(icp_root);
3003 command.arg("canister");
3004 command
3005}
3006
3007fn add_icp_environment_target(command: &mut Command, network: &str) {
3008 icp::add_target_args(command, Some(network), None);
3009}