ambient-ci 0.14.0

A continuous integration engine
Documentation
use std::path::PathBuf;

use clap::Parser;
use serde::Serialize;

use ambient_ci::{
    plan::{construct_all_plans, construct_runnable_plan, RunnablePlan},
    project::{ProjectError, Projects, State},
    runlog::RunLog,
};

use super::{AmbientError, Config, Leaf};

/// Prepare image.
#[derive(Debug, Parser)]
pub struct Plan {
    /// Project specification file. May contain any number of projects.
    #[clap(long)]
    projects: Option<PathBuf>,

    /// Which project to show plan for?
    project: String,

    /// rsync target for publishing artifacts with rsync program.
    #[clap(long, aliases = ["rsync", "target"])]
    rsync_target: Option<String>,

    /// dput target for publishing .deb package.
    #[clap(long, alias = "dput")]
    dput_target: Option<String>,

    /// Output as YAML. Default is JSON.
    #[clap(long)]
    yaml: bool,

    /// Output runnable plan for `ambient-exeucte-plan`.
    #[clap(long)]
    runnable: bool,
}

impl Leaf for Plan {
    fn run(&self, config: &Config, _runlog: &mut RunLog) -> Result<(), AmbientError> {
        let projects = if let Some(filename) = &self.projects {
            filename.to_path_buf()
        } else {
            config.projects().into()
        };

        let projects =
            Projects::from_file(&projects).map_err(|err| PlanError::FromFile(projects, err))?;
        let project = projects
            .get(&self.project)
            .ok_or(PlanError::NoSuchProject(self.project.clone()))?;

        config.lint(&projects)?;

        if self.runnable {
            let runnable = construct_runnable_plan(project.plan()).map_err(PlanError::Runnable)?;
            let text = if self.yaml {
                serde_norway::to_string(&runnable).map_err(PlanError::ToYaml)?
            } else {
                serde_json::to_string_pretty(&runnable).map_err(PlanError::ToJson)?
            };
            println!("{text}");
        } else {
            let mut config = config.clone();
            if let Some(s) = &self.dput_target {
                config.set_dput_target(s);
            };
            if let Some(s) = &self.rsync_target {
                config.set_rsync_target(s);
            };

            let state =
                State::from_file(config.state(), &self.project).map_err(PlanError::State)?;

            let (pre_plan, plan, post_plan) =
                construct_all_plans(&config, &self.project, project, &state)?;
            let plans = Plans {
                pre_plan,
                plan,
                post_plan,
            };

            let text = if self.yaml {
                serde_norway::to_string(&plans).map_err(PlanError::ToYaml)?
            } else {
                serde_json::to_string_pretty(&plans).map_err(PlanError::ToJson)?
            };
            println!("{text}");
        }

        Ok(())
    }
}

#[derive(Serialize)]
struct Plans {
    pre_plan: RunnablePlan,
    plan: RunnablePlan,
    post_plan: RunnablePlan,
}

#[derive(Debug, thiserror::Error)]
pub enum PlanError {
    #[error("failed to load project list from file {0}")]
    FromFile(PathBuf, #[source] ProjectError),

    #[error("failed to serialize plans to JSON")]
    ToJson(#[source] serde_json::Error),

    #[error("failed to serialize plans to YAML")]
    ToYaml(#[source] serde_norway::Error),

    #[error("can't find project {0}")]
    NoSuchProject(String),

    #[error("can't construct runnable plan")]
    Runnable(#[source] ambient_ci::plan::PlanError),

    #[error("can't load project state")]
    State(#[source] ambient_ci::project::ProjectError),
}