my-ci 0.0.6

Minimalist Local CICD
mod build;
mod cli;
mod config;
mod events;
mod graph;
mod gui;
mod history;
mod init;
mod oci;
mod run;
mod telemetry;
mod ui_assets;

use std::collections::HashSet;

use anyhow::{Context, Result};
use clap::Parser;
use my_ci_macros::trace;
use tracing::{debug, info};

use crate::build::build_workflow;
use crate::cli::{Cli, Commands};
use crate::config::{get_workflow, load_config};
use crate::graph::{resolve_build_plan, topological_order};
use crate::gui::serve_gui;
use crate::init::scaffold_init;
use crate::oci::{
    OciRuntime, RuntimeChoice, connect_oci, describe_oci_target, select_oci_provider,
};
use crate::run::run_workflow;

#[tokio::main]
async fn main() -> Result<()> {
    telemetry::init_tracing();
    let cli = Cli::parse();
    run_cli(cli).await
}

#[trace(skip(cli), err, fields(config = %cli.config.display(), command = ?cli.command, runtime = ?cli.runtime))]
async fn run_cli(cli: Cli) -> Result<()> {
    debug!("parsed CLI arguments");
    if let Commands::Init { path, force } = &cli.command {
        info!(path = %path.display(), force, "scaffolding bundled workflow template");
        return scaffold_init(path, *force);
    }

    let config = load_config(&cli.config)?;
    let project_name = if config.name.trim().is_empty() {
        "my-ci"
    } else {
        config.name.trim()
    };
    info!(
        project = %project_name,
        workflow_count = config.workflow.len(),
        "loaded workflow config"
    );

    if let Commands::List = &cli.command {
        debug!("listing workflows without connecting to a runtime");
        for wf in &config.workflow {
            println!("{}", wf.name);
        }
        return Ok(());
    }

    match cli.command {
        Commands::Build { workflow } => {
            let oci_runtime = connect_selected_runtime(cli.runtime)?;
            if let Some(name) = workflow {
                debug!(workflow = %name, "resolving targeted build plan");
                for target in resolve_build_plan(&config, &name)? {
                    let wf = get_workflow(&config, &target)?;
                    build_workflow(&oci_runtime, &config, wf).await?;
                }
            } else {
                debug!("resolving full build plan");
                for name in topological_order(&config)? {
                    let wf = get_workflow(&config, &name)?;
                    build_workflow(&oci_runtime, &config, wf).await?;
                }
            }
        }
        Commands::Run { workflow } => {
            let oci_runtime = connect_selected_runtime(cli.runtime)?;
            let targets = match workflow {
                Some(name) => vec![name],
                None => topological_order(&config)?,
            };
            debug!(targets = ?targets, "resolved run targets");
            let mut seen: HashSet<String> = HashSet::new();
            let mut build_order: Vec<String> = Vec::new();
            for target in &targets {
                for dep in resolve_build_plan(&config, target)? {
                    if seen.insert(dep.clone()) {
                        build_order.push(dep);
                    }
                }
            }
            debug!(build_order = ?build_order, "deduplicated build order before run");
            for dep in &build_order {
                let wf = get_workflow(&config, dep)?;
                build_workflow(&oci_runtime, &config, wf).await?;
            }
            for target in &targets {
                let wf = get_workflow(&config, target)?;
                if wf.command.is_some() {
                    run_workflow(&oci_runtime, &config, wf).await?;
                }
            }
        }
        Commands::Gui { host, port } => {
            info!(%host, port, default_runtime = ?cli.runtime, "starting GUI");
            serve_gui(host, port, config, cli.runtime).await?;
        }
        Commands::List => unreachable!("list handled before runtime connect"),
        Commands::Init { .. } => unreachable!("init handled before config load"),
    }

    Ok(())
}

fn connect_selected_runtime(runtime: RuntimeChoice) -> Result<OciRuntime> {
    let provider = select_oci_provider(runtime);
    info!(?runtime, provider = ?provider, "selected OCI runtime provider");

    let oci_runtime = connect_oci(provider)
        .with_context(|| format!("failed to connect to {}", describe_oci_target(provider)))?;
    info!(
        target = describe_oci_target(provider),
        "connected to OCI runtime"
    );
    Ok(oci_runtime)
}