1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
mod ci;
mod cli;
mod docs;
mod execution;
mod logs;
mod model_injection;
pub mod models;
mod output;
mod utils;

use crate::cli::{cli_builder, construct_cli};
use crate::execution::{execute_stages, execute_tasks};
use crate::model_injection::{
    inject_pipeline_metadata, inject_task_metadata, inject_template_values,
};
use crate::models::{AllResults, Validate};
use crate::models::{PassFail, TaskResult};
use std::collections::HashMap;
use std::error::Error;

type RoxResult<T> = Result<T, Box<dyn Error>>;

/// Get the filepath argument from the CLI
///
/// This is required because we might need to
/// dynamically populate the CLI based on this arg
fn get_filepath_arg_value() -> String {
    let cli = cli_builder(false);
    // Get the file arg from the CLI if set
    let cli_matches = cli.clone().arg_required_else_help(false).get_matches();
    cli_matches.get_one::<String>("roxfile").unwrap().to_owned()
}

/// Entrypoint for the Crate CLI
pub async fn rox() -> RoxResult<()> {
    let start = std::time::Instant::now();
    let execution_start = chrono::Utc::now().to_rfc3339();

    // NOTE: Due to the dynamically generated nature of the CLI,
    // It is required to parse the CLI matches twice. Once to get
    // the filename arg and once to actually build the CLI.

    // Get the file arg from the CLI if set
    let file_path = get_filepath_arg_value();
    let roxfile = utils::parse_file_contents(utils::load_file(&file_path));
    roxfile.validate()?;
    utils::print_horizontal_rule();

    // Build & Generate the CLI based on the loaded Roxfile
    let tasks = inject_task_metadata(roxfile.tasks, &file_path);
    let pipelines = inject_pipeline_metadata(roxfile.pipelines);
    let docs = roxfile.docs;
    let ci = roxfile.ci;
    let cli = construct_cli(&tasks, &pipelines, &docs, &ci);
    let cli_matches = cli.get_matches();

    // Build Hashmaps for Tasks, Templates and Pipelines
    let template_map: HashMap<String, models::Template> = std::collections::HashMap::from_iter(
        roxfile
            .templates
            .into_iter()
            .flatten()
            .map(|template| (template.name.to_owned(), template)),
    );
    let task_map: HashMap<String, models::Task> = std::collections::HashMap::from_iter(
        tasks
            .into_iter()
            .map(|task| match task.uses.to_owned() {
                Some(task_use) => {
                    inject_template_values(task, template_map.get(&task_use).unwrap())
                }
                None => task,
            })
            .map(|task| (task.name.to_owned(), task)),
    );

    // Deconstruct the CLI commands and get the Pipeline object that was called
    let (_, args) = cli_matches.subcommand().unwrap();
    let subcommand_name = args.subcommand_name().unwrap_or("default");

    // Execute the Task(s)
    let results: Vec<Vec<TaskResult>> = match cli_matches.subcommand_name().unwrap() {
        "docs" => {
            let docs_map: HashMap<String, models::Docs> = std::collections::HashMap::from_iter(
                docs.into_iter()
                    .flatten()
                    .map(|doc| (doc.name.to_owned(), doc)),
            );
            docs::display_docs(docs_map.get(subcommand_name).unwrap());
            std::process::exit(0);
        }
        "logs" => {
            let number = args.get_one::<i8>("number").unwrap();
            logs::display_logs(number);
            std::process::exit(0);
        }
        "ci" => {
            assert!(ci.is_some());
            ci::display_ci_status(ci.unwrap()).await;
            std::process::exit(0);
        }
        "pl" => {
            let pipeline_map: HashMap<String, models::Pipeline> =
                std::collections::HashMap::from_iter(
                    pipelines
                        .into_iter()
                        .flatten()
                        .map(|pipeline| (pipeline.name.to_owned(), pipeline)),
                );
            let parallel = args.get_flag("parallel");
            let execution_results = execute_stages(
                &pipeline_map.get(subcommand_name).unwrap().stages,
                &task_map,
                parallel,
            );
            execution_results
        }
        "task" => {
            let execution_results = vec![execute_tasks(
                vec![subcommand_name.to_string()],
                0,
                &task_map,
                false,
            )];
            execution_results
        }
        _ => unreachable!("Invalid subcommand"),
    };
    let results = AllResults {
        job_name: subcommand_name.to_string(),
        execution_time: execution_start,
        results: results.into_iter().flatten().collect(),
    };

    let log_path = logs::write_logs(&results);
    println!("> Log file written to: {}", log_path);

    output::display_execution_results(&results);
    println!(
        "> Total elapsed time: {}s | {}ms",
        start.elapsed().as_secs(),
        start.elapsed().as_millis(),
    );
    nonzero_exit_if_failure(&results);

    Ok(())
}

/// Throw a non-zero exit if any Task(s) had a failing result
pub fn nonzero_exit_if_failure(results: &AllResults) {
    // TODO: Figure out a way to get this info without looping again
    for result in results.results.iter() {
        if result.result == PassFail::Fail {
            std::process::exit(2)
        }
    }
}