/ src / bin / cmd / plan.rs
plan.rs
  1  use std::path::PathBuf;
  2  
  3  use clap::Parser;
  4  use serde::Serialize;
  5  
  6  use ambient_ci::{
  7      plan::{construct_all_plans, construct_runnable_plan, RunnablePlan},
  8      project::{ProjectError, Projects, State},
  9      runlog::RunLog,
 10  };
 11  
 12  use super::{AmbientError, Config, Leaf};
 13  
 14  /// Prepare image.
 15  #[derive(Debug, Parser)]
 16  pub struct Plan {
 17      /// Project specification file. May contain any number of projects.
 18      #[clap(long)]
 19      projects: Option<PathBuf>,
 20  
 21      /// Which project to show plan for?
 22      project: String,
 23  
 24      /// rsync target for publishing artifacts with rsync program.
 25      #[clap(long, aliases = ["rsync", "target"])]
 26      rsync_target: Option<String>,
 27  
 28      /// dput target for publishing .deb package.
 29      #[clap(long, alias = "dput")]
 30      dput_target: Option<String>,
 31  
 32      /// Output as YAML. Default is JSON.
 33      #[clap(long)]
 34      yaml: bool,
 35  
 36      /// Output runnable plan for `ambient-exeucte-plan`.
 37      #[clap(long)]
 38      runnable: bool,
 39  }
 40  
 41  impl Leaf for Plan {
 42      fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
 43          let projects = if let Some(filename) = &self.projects {
 44              filename.to_path_buf()
 45          } else {
 46              config.projects().into()
 47          };
 48  
 49          let projects =
 50              Projects::from_file(&projects).map_err(|err| PlanError::FromFile(projects, err))?;
 51          let project = projects
 52              .get(&self.project)
 53              .ok_or(PlanError::NoSuchProject(self.project.clone()))?;
 54  
 55          config.lint(&projects)?;
 56  
 57          if self.runnable {
 58              let runnable = construct_runnable_plan(project.plan()).map_err(PlanError::Runnable)?;
 59              let text = if self.yaml {
 60                  serde_norway::to_string(&runnable).map_err(PlanError::ToYaml)?
 61              } else {
 62                  serde_json::to_string_pretty(&runnable).map_err(PlanError::ToJson)?
 63              };
 64              println!("{text}");
 65          } else {
 66              let mut config = config.clone();
 67              if let Some(s) = &self.dput_target {
 68                  config.set_dput_target(s);
 69              };
 70              if let Some(s) = &self.rsync_target {
 71                  config.set_rsync_target(s);
 72              };
 73  
 74              let state =
 75                  State::from_file(config.state(), &self.project).map_err(PlanError::State)?;
 76  
 77              let (pre_plan, plan, post_plan) =
 78                  construct_all_plans(&config, &self.project, project, &state)?;
 79              let plans = Plans {
 80                  pre_plan,
 81                  plan,
 82                  post_plan,
 83              };
 84  
 85              let text = if self.yaml {
 86                  serde_norway::to_string(&plans).map_err(PlanError::ToYaml)?
 87              } else {
 88                  serde_json::to_string_pretty(&plans).map_err(PlanError::ToJson)?
 89              };
 90              println!("{text}");
 91          }
 92  
 93          Ok(())
 94      }
 95  }
 96  
 97  #[derive(Serialize)]
 98  struct Plans {
 99      pre_plan: RunnablePlan,
100      plan: RunnablePlan,
101      post_plan: RunnablePlan,
102  }
103  
104  #[derive(Debug, thiserror::Error)]
105  pub enum PlanError {
106      #[error("failed to load project list from file {0}")]
107      FromFile(PathBuf, #[source] ProjectError),
108  
109      #[error("failed to serialize plans to JSON")]
110      ToJson(#[source] serde_json::Error),
111  
112      #[error("failed to serialize plans to YAML")]
113      ToYaml(#[source] serde_norway::Error),
114  
115      #[error("can't find project {0}")]
116      NoSuchProject(String),
117  
118      #[error("can't construct runnable plan")]
119      Runnable(#[source] ambient_ci::plan::PlanError),
120  
121      #[error("can't load project state")]
122      State(#[source] ambient_ci::project::ProjectError),
123  }