use colored::*;
use serde_yaml::Value;
use std::collections::HashMap;
use std::env;
use std::ffi::OsString;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::exit;
#[derive(Debug, Clone)]
struct Config {
shell: String,
enable_colors: bool,
artifact_dir: String,
cache_dir: String,
}
impl Config {
fn default() -> Config {
Config {
shell: "bash".to_string(),
enable_colors: true,
artifact_dir: ".simple-ci".to_string(),
cache_dir: ".simple-ci/cache".to_string(),
}
}
fn load() -> Config {
let config_path = Self::get_config_path();
if Path::new(&config_path).exists() {
match fs::read_to_string(&config_path) {
Ok(content) => {
if let Ok(yaml) = serde_yaml::from_str::<serde_yaml::Value>(&content) {
return Self::from_yaml(yaml);
}
}
Err(_) => {}
}
}
Config::default()
}
fn get_config_path() -> String {
if Path::new(".simple-ci/config.yml").exists() {
return ".simple-ci/config.yml".to_string();
}
if let Ok(home) = env::var("HOME") {
let home_config = format!("{}/.simple-ci/config.yml", home);
if Path::new(&home_config).exists() {
return home_config;
}
}
".simple-ci/config.yml".to_string()
}
fn from_yaml(yaml: Value) -> Config {
let mut config = Config::default();
if let Some(shell) = yaml.get("shell").and_then(|v| v.as_str()) {
config.shell = shell.to_string();
}
if let Some(colors) = yaml.get("enable_colors").and_then(|v| v.as_bool()) {
config.enable_colors = colors;
}
if let Some(artifact_dir) = yaml.get("artifact_dir").and_then(|v| v.as_str()) {
config.artifact_dir = artifact_dir.to_string();
}
if let Some(cache_dir) = yaml.get("cache_dir").and_then(|v| v.as_str()) {
config.cache_dir = cache_dir.to_string();
}
config
}
fn save(&self, path: &str) -> Result<(), String> {
let yaml_content = format!(
"---\nshell: {}\nenable_colors: {}\nartifact_dir: {}\ncache_dir: {}\n",
self.shell, self.enable_colors, self.artifact_dir, self.cache_dir
);
if let Some(parent) = Path::new(path).parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::write(path, yaml_content).map_err(|e| e.to_string())
}
fn to_yaml_string(&self) -> String {
format!(
"shell: {}\nenable_colors: {}\nartifact_dir: {}\ncache_dir: {}\n",
self.shell, self.enable_colors, self.artifact_dir, self.cache_dir
)
}
}
#[derive(Debug)]
struct Pipeline {
pipeline_type: PipelineType,
stages: Vec<Stage>,
jobs: Vec<Job>,
script: Script,
variables: HashMap<String, String>,
artifacts: Vec<String>,
docker: Option<DockerConfig>,
config: Config,
}
impl Pipeline {
fn new(pipeline_file: &str) -> Pipeline {
let pipeline_data = get_pipeline_data(pipeline_file);
let config = Config::load();
match get_pipeline_type(pipeline_data.clone()) {
PipelineType::Script => {
let script_data = get_script(pipeline_data.clone()).unwrap();
let new_pipeline = Pipeline {
pipeline_type: PipelineType::Script,
stages: vec![], jobs: vec![], script: Script::new(script_data), variables: get_variables(pipeline_data.clone()),
artifacts: get_artifact_paths(pipeline_data.clone()),
docker: get_docker_config(pipeline_data),
config: config,
};
return new_pipeline;
}
PipelineType::Jobs => {
let mut pipeline_jobs: Vec<Job> = vec![];
let pipeline_variables = get_variables(pipeline_data.clone());
let pipeline_docker = get_docker_config(pipeline_data.clone());
let jobs_data = get_jobs(pipeline_data.clone()).unwrap();
let mut a = 0;
loop {
if jobs_data.get(a).is_some() {
let job_name = jobs_data.get(a).unwrap().as_str().unwrap().to_owned();
let job_script = if get_job_or_stage(
pipeline_data.clone(),
job_name.clone(),
)
.is_some()
{
get_job_or_stage(pipeline_data.clone(), job_name.clone()).unwrap()
} else {
break;
};
pipeline_jobs.push(Job::new(
job_name.clone(),
job_script.clone(),
pipeline_variables.clone(),
pipeline_docker.clone(),
config.clone(),
));
} else {
break;
}
a += 1;
}
let new_pipeline = Pipeline {
pipeline_type: PipelineType::Jobs,
stages: vec![], jobs: pipeline_jobs,
script: Script { data: vec![] }, variables: pipeline_variables,
artifacts: vec![],
docker: pipeline_docker,
config: config,
};
return new_pipeline;
}
PipelineType::Stages => {
let mut pipeline_stages: Vec<Stage> = vec![];
let pipeline_variables = get_variables(pipeline_data.clone());
let pipeline_docker = get_docker_config(pipeline_data.clone());
let stages_data = get_stages(pipeline_data.clone()).unwrap();
let mut a = 0;
loop {
if stages_data.get(a).is_some() {
let stage_name = stages_data.get(a).unwrap().as_str().unwrap().to_owned();
pipeline_stages.push(Stage::new(
stage_name,
pipeline_data.clone(),
pipeline_variables.clone(),
pipeline_docker.clone(),
config.clone(),
));
} else {
break;
}
a += 1;
}
let new_pipeline = Pipeline {
pipeline_type: PipelineType::Stages,
stages: pipeline_stages,
jobs: vec![], script: Script { data: vec![] }, variables: pipeline_variables,
artifacts: vec![],
docker: pipeline_docker,
config: config,
};
return new_pipeline;
}
PipelineType::Null => panic!("Error in pipeline format!"),
}
}
fn exec_pipeline(&self) -> bool {
let timestamp: String = create_artifact_dir();
let stage_begin_spacer = "###############";
let stage_end_spacer = "####################################";
match self.pipeline_type {
PipelineType::Script => {
println!(
"{} {} {}",
stage_begin_spacer.green(),
"main".blue(),
stage_begin_spacer.green()
);
let success = self.script.exec(
self.variables.clone(),
timestamp.as_str(),
"main",
&self.artifacts,
&None,
&self.docker,
self.config.shell.as_str(),
);
println!("{}", stage_end_spacer.green());
return success;
}
PipelineType::Jobs => {
let mut success = true;
println!(
"{} {} {}",
stage_begin_spacer.green(),
"main".blue(),
stage_begin_spacer.green()
);
for a in 0..self.jobs.len() {
let job_success = self.jobs[a].exec(timestamp.as_str());
if !job_success {
success = false
}
}
println!("{}", stage_end_spacer.green());
return success;
}
PipelineType::Stages => {
for a in 0..self.stages.len() {
println!(
"{} {} {}",
stage_begin_spacer.green(),
self.stages[a].get_name().to_string().blue(),
stage_begin_spacer.green()
);
let stage_success = self.stages[a].exec(timestamp.as_str());
println!("{}", "###############################\n".green());
if !stage_success {
return false;
}
}
return true;
}
PipelineType::Null => panic!("Error in pipeline format!"),
}
}
}
#[derive(Debug)]
struct Script {
data: Vec<String>,
}
impl Script {
fn new(script_data: Value) -> Script {
let mut a = 0;
let mut commands: Vec<String> = vec![];
loop {
let next_command = get_command(script_data.clone(), a);
if next_command.is_some() {
commands.push(next_command.unwrap());
} else {
break;
}
a += 1;
}
let new_script = Script { data: commands };
return new_script;
}
fn exec(
&self,
variables: HashMap<String, String>,
timestamp: &str,
job_name: &str,
artifacts: &Vec<String>,
cache: &Option<CacheConfig>,
docker: &Option<DockerConfig>,
shell: &str,
) -> bool {
use std::time::Instant;
let start_time = Instant::now();
let start_timestamp = chrono::offset::Utc::now()
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string();
let mut indicators = vec![];
if docker.is_some() {
indicators.push("🐳".to_string());
}
if !artifacts.is_empty() {
indicators.push(format!("📦{}", artifacts.len()));
}
if let Some(cache_config) = cache {
if !cache_config.down.is_empty() {
indicators.push(format!("⬇️{}", cache_config.down.len()));
}
if !cache_config.up.is_empty() {
indicators.push(format!("⬆️{}", cache_config.up.len()));
}
}
let context = if indicators.is_empty() {
String::new()
} else {
format!(" [{}]", indicators.join(" "))
};
print!(
"Job: \"{}\"{}{} ",
job_name.blue(),
context.yellow(),
"...".blink()
);
std::io::stdout().flush().unwrap();
let job_work_dir = format!(
".simple-ci/run_{}/work_{}",
timestamp,
job_name.replace(" ", "_")
);
fs::create_dir_all(&job_work_dir).unwrap();
if let Some(cache_config) = cache {
for path in &cache_config.down {
let cache_file = format!(".simple-ci/cache/{}", path);
let dest_file = if docker.is_some() {
format!("{}/{}", job_work_dir, path)
} else {
path.to_string()
};
if Path::new(&cache_file).exists() {
if let Some(parent) = Path::new(&dest_file).parent() {
fs::create_dir_all(parent).ok();
}
fs::copy(&cache_file, &dest_file).ok();
}
}
}
let mut script_string = "".to_owned(); script_string += "set -ebuxo pipefail\n";
for (key, value) in variables {
script_string += key.as_str();
script_string += "=";
script_string += value.as_str();
script_string += "\n";
}
for command in self.data.iter() {
script_string += "\n";
script_string += command;
}
script_string += "\n";
let script_file: String =
".simple-ci/run_".to_string() + timestamp + "/" + job_name + ".sh";
write_to_file("####\n", script_file.as_str());
write_to_file(script_string.as_str(), script_file.as_str());
let res = if let Some(docker_config) = docker {
exec_in_docker(script_string, docker_config, &job_work_dir, shell)
} else {
exec_command_in_dir(script_string, &job_work_dir, shell)
};
if res.0 == true {
if let Some(cache_config) = cache {
fs::create_dir_all(".simple-ci/cache").ok();
for path in &cache_config.up {
let src_file = if docker.is_some() {
format!("{}/{}", job_work_dir, path)
} else {
path.to_string()
};
let cache_file = format!(".simple-ci/cache/{}", path);
if Path::new(&src_file).exists() {
if let Some(parent) = Path::new(&cache_file).parent() {
fs::create_dir_all(parent).ok();
}
fs::copy(&src_file, &cache_file).ok();
if docker.is_none() {
fs::remove_file(&src_file).ok();
}
}
}
}
if !artifacts.is_empty() {
let artifact_dir = format!(
".simple-ci/run_{}/{}",
timestamp,
job_name.replace(" ", "_")
);
fs::create_dir_all(&artifact_dir).ok();
for artifact_path in artifacts {
let src = if docker.is_some() {
format!("{}/{}", job_work_dir, artifact_path)
} else {
artifact_path.to_string()
};
let dst = format!("{}/{}", artifact_dir, artifact_path);
if Path::new(&src).exists() {
if let Some(parent) = Path::new(&dst).parent() {
fs::create_dir_all(parent).ok();
}
fs::copy(&src, &dst).ok();
if docker.is_none() {
fs::remove_file(&src).ok();
}
}
}
}
if docker.is_none() {
if let Some(cache_config) = cache {
for path in &cache_config.down {
let file_path = Path::new(path);
if file_path.exists() {
fs::remove_file(file_path).ok();
}
}
}
}
let duration = start_time.elapsed();
let duration_str = if duration.as_secs() > 0 {
format!("{}s", duration.as_secs())
} else {
format!("{}ms", duration.as_millis())
};
let log_file: String =
".simple-ci/run_".to_string() + timestamp + "/" + job_name + ".log";
write_to_file(&format!("#### Job: {}\n", job_name), log_file.as_str());
write_to_file(
&format!("#### Started: {}\n", start_timestamp),
log_file.as_str(),
);
write_to_file(
&format!("#### Duration: {}\n", duration_str),
log_file.as_str(),
);
write_to_file(&format!("#### Status: SUCCESS\n####\n"), log_file.as_str());
write_to_file(res.1.as_str(), log_file.as_str());
println!(
"{} {}",
"OK".blink().on_green(),
format!("({})", duration_str).dimmed()
);
fs::remove_dir_all(&job_work_dir).ok();
return true;
} else {
let duration = start_time.elapsed();
let duration_str = if duration.as_secs() > 0 {
format!("{}s", duration.as_secs())
} else {
format!("{}ms", duration.as_millis())
};
let error_log_file: String =
".simple-ci/run_".to_string() + timestamp + "/" + job_name + "_ERROR.log";
write_to_file(
&format!("#### Job: {}\n", job_name),
error_log_file.as_str(),
);
write_to_file(
&format!("#### Started: {}\n", start_timestamp),
error_log_file.as_str(),
);
write_to_file(
&format!("#### Duration: {}\n", duration_str),
error_log_file.as_str(),
);
write_to_file(
&format!("#### Status: FAILED\n####\n"),
error_log_file.as_str(),
);
write_to_file(res.2.as_str(), error_log_file.as_str());
println!(
"{} {}",
"Error".blink().on_red(),
format!("({})", duration_str).dimmed()
);
if !res.2.is_empty() {
let error_lines: Vec<&str> = res.2.lines().collect();
let preview = if error_lines.len() > 3 {
format!(
" {} (see log for full output)",
error_lines[error_lines.len() - 1].trim()
)
} else {
format!(" {}", res.2.trim())
};
eprintln!("{}", preview.red());
}
fs::remove_dir_all(&job_work_dir).ok();
return false;
}
}
}
#[derive(Debug, Clone)]
struct DockerConfig {
image: String,
path: String,
}
#[derive(Debug, Clone)]
struct CacheConfig {
up: Vec<String>,
down: Vec<String>,
}
#[derive(Debug)]
struct Job {
name: String,
scripts: Script,
variables: HashMap<String, String>,
artifacts: Vec<String>,
cache: Option<CacheConfig>,
docker: Option<DockerConfig>,
config: Config,
shell_override: Option<String>,
}
impl Job {
fn new(
job_name: String,
job_data: Value,
herited_variables: HashMap<String, String>,
herited_docker: Option<DockerConfig>,
config: Config,
) -> Job {
let mut all_vars = herited_variables;
for (key, value) in get_variables(job_data.clone()).iter() {
all_vars.insert(key.to_owned(), value.to_owned());
}
let artifacts = get_artifact_paths(job_data.clone());
let cache = get_cache_config(job_data.clone());
let docker = get_docker_config(job_data.clone()).or(herited_docker);
let shell_override = get_job_shell(job_data.clone());
return Job {
name: job_name,
scripts: Script::new(job_data.get("script").unwrap().to_owned()),
variables: all_vars,
artifacts,
cache,
docker,
config,
shell_override,
};
}
fn get_effective_shell(&self) -> &str {
if let Some(shell) = &self.shell_override {
shell.as_str()
} else {
self.config.shell.as_str()
}
}
fn exec(&self, timestamp: &str) -> bool {
let shell = self.get_effective_shell();
let success = self.scripts.exec(
self.variables.clone(),
timestamp,
self.name.as_str(),
&self.artifacts,
&self.cache,
&self.docker,
shell,
);
return success;
}
}
#[derive(Debug)]
struct Stage {
name: String,
jobs: Vec<Job>,
#[allow(dead_code)]
docker: Option<DockerConfig>,
#[allow(dead_code)]
config: Config,
}
impl Stage {
fn new(
stage_name: String,
pipeline_data: Value,
herited_variables: HashMap<String, String>,
herited_docker: Option<DockerConfig>,
config: Config,
) -> Stage {
let mut stage_jobs: Vec<Job> = vec![];
let stage_data = if get_job_or_stage(pipeline_data.clone(), stage_name.clone()).is_some() {
get_job_or_stage(pipeline_data.clone(), stage_name.clone()).unwrap()
} else {
panic!(
"The stage '{}' doesn't exists in pipeline",
stage_name.as_str().red()
);
};
let mut all_vars = herited_variables;
for (key, value) in get_variables(stage_data.clone()).iter() {
all_vars.insert(key.to_owned(), value.to_owned());
}
let stage_docker = get_docker_config(stage_data.clone()).or(herited_docker.clone());
let jobs_data = if get_jobs(stage_data.clone()).is_some() {
get_jobs(stage_data.clone()).unwrap()
} else {
panic!("Jobs not found for stage '{}", stage_name.as_str().red())
};
let mut a = 0;
loop {
if jobs_data.get(a).is_some() {
let job_name = jobs_data.get(a).unwrap().as_str().unwrap().to_owned();
let job_script =
if get_job_or_stage(pipeline_data.clone(), job_name.clone()).is_some() {
get_job_or_stage(pipeline_data.clone(), job_name.clone()).unwrap()
} else {
a += 1;
continue;
};
stage_jobs.push(Job::new(
job_name.clone(),
job_script.clone(),
all_vars.clone(),
stage_docker.clone(),
config.clone(),
));
} else {
break;
}
a += 1;
}
return Stage {
name: stage_name,
jobs: stage_jobs,
docker: stage_docker,
config,
};
}
fn get_name(&self) -> String {
return self.name.clone();
}
fn exec(&self, timestamp: &str) -> bool {
let mut success = true;
for job in self.jobs.iter() {
let job_success = job.exec(timestamp);
if !job_success {
success = false;
}
}
return success;
}
}
fn get_variables<'t>(data: Value) -> HashMap<String, String> {
let variables = data.get("variables");
if variables.is_some() {
if variables.unwrap().is_mapping() {
let mut map = HashMap::new();
for var in variables.unwrap().to_owned().as_mapping().unwrap().iter() {
let key = var.0.as_str().unwrap().to_owned();
let value = var.1.as_str().unwrap().to_owned();
map.insert(key, value);
}
return map;
} else {
println!(
"{}",
"Variables data is incorrect! (not a dictionnay)".red()
);
exit(1)
}
} else {
return HashMap::new();
}
}
fn get_script<'t>(data: Value) -> Option<Value> {
let script = data.get("script");
match script {
Some(_) => return Option::Some(script.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_command<'t>(script: Value, i: usize) -> Option<std::string::String> {
let command = script.get(i);
if command.is_some() {
return Option::Some(command.unwrap().as_str().unwrap().to_owned());
} else {
return Option::None;
}
}
fn get_jobs<'t>(data: Value) -> Option<Value> {
let jobs = data.get("jobs");
match jobs {
Some(_) => return Option::Some(jobs.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_stages<'t>(pipeline: Value) -> Option<Value> {
let stages = pipeline.get("stages");
match stages {
Some(_) => return Option::Some(stages.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_job_or_stage<'t>(pipeline: Value, job_or_stage_name: String) -> Option<Value> {
let stages = pipeline.get(job_or_stage_name);
match stages {
Some(_) => return Option::Some(stages.unwrap().to_owned()),
None => return Option::None,
}
}
fn get_pipeline_data<'t>(file: &str) -> Value {
let content = fs::read_to_string(file).expect("Could not read the pipeline file");
let pipeline: Value = serde_yaml::from_str(&content).unwrap();
return pipeline;
}
#[derive(Debug)]
enum PipelineType {
Null,
Stages,
Jobs,
Script,
}
fn get_pipeline_type(pipeline_data: Value) -> PipelineType {
let stages = get_stages(pipeline_data.clone());
let jobs = get_jobs(pipeline_data.clone());
let script = get_script(pipeline_data.clone());
let pipeline_type_found: PipelineType;
if stages.is_some() {
pipeline_type_found = PipelineType::Stages;
} else if jobs.is_some() {
pipeline_type_found = PipelineType::Jobs;
} else if script.is_some() {
pipeline_type_found = PipelineType::Script;
} else {
pipeline_type_found = PipelineType::Null
}
return pipeline_type_found;
}
fn create_artifact_dir() -> String {
let tstmp: String = chrono::offset::Utc::now()
.format("%Y.%m.%d.%H.%M.%S")
.to_string();
fs::create_dir_all(".simple-ci/run_".to_string() + tstmp.as_str()).unwrap();
return tstmp;
}
fn write_to_file(text: &str, file_name: &str) {
let mut file = fs::OpenOptions::new()
.create(true)
.append(true)
.open(file_name)
.unwrap();
file.write_all(text.as_bytes()).unwrap();
}
fn get_artifact_paths(data: Value) -> Vec<String> {
let artifacts = data.get("artifacts");
if let Some(artifacts_val) = artifacts {
if let Some(paths) = artifacts_val.get("paths") {
if let Some(paths_seq) = paths.as_sequence() {
return paths_seq
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_owned()))
.collect();
}
}
}
vec![]
}
fn get_cache_config(data: Value) -> Option<CacheConfig> {
let cache = data.get("cache");
if let Some(cache_val) = cache {
let mut up_paths = vec![];
let mut down_paths = vec![];
if let Some(up) = cache_val.get("up") {
if let Some(paths) = up.get("paths") {
if let Some(paths_seq) = paths.as_sequence() {
up_paths = paths_seq
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_owned()))
.collect();
}
}
}
if let Some(down) = cache_val.get("down") {
if let Some(paths) = down.get("paths") {
if let Some(paths_seq) = paths.as_sequence() {
down_paths = paths_seq
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_owned()))
.collect();
}
}
}
if !up_paths.is_empty() || !down_paths.is_empty() {
return Some(CacheConfig {
up: up_paths,
down: down_paths,
});
}
}
None
}
fn get_job_shell(data: Value) -> Option<String> {
data.get("shell")
.and_then(|v| v.as_str().map(|s| s.to_owned()))
}
fn get_docker_config(data: Value) -> Option<DockerConfig> {
let docker = data.get("inside_docker");
if let Some(docker_val) = docker {
let image = docker_val
.get("image")
.and_then(|v| v.as_str())
.unwrap_or("ubuntu")
.to_owned();
let path = docker_val
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("/")
.to_owned();
return Some(DockerConfig { image, path });
}
None
}
fn exec_command_in_dir(
command: std::string::String,
_work_dir: &str,
shell: &str,
) -> (bool, std::string::String, std::string::String) {
let command_args = OsString::from(command.as_str());
let mut command_base = std::process::Command::new(shell);
let command_executed = command_base
.arg("-c")
.arg(command_args)
.output()
.expect(&format!("Error executing {} shell", shell));
let command_success = command_executed.status.success();
let command_stdout = std::str::from_utf8(&command_executed.stdout[..])
.unwrap()
.to_owned();
let command_stderr = std::str::from_utf8(&command_executed.stderr[..])
.unwrap()
.to_owned();
return (command_success, command_stdout, command_stderr);
}
fn exec_in_docker(
command: std::string::String,
docker_config: &DockerConfig,
work_dir: &str,
_shell: &str,
) -> (bool, std::string::String, std::string::String) {
let abs_work_dir = fs::canonicalize(work_dir).unwrap();
let abs_work_dir_str = abs_work_dir.to_str().unwrap();
let docker_path = if docker_config.path == "/" {
"/workspace"
} else {
&docker_config.path
};
let docker_cmd = format!(
"docker run --rm -v {}:{} -w {} {} bash -c {}",
abs_work_dir_str,
docker_path,
docker_path,
docker_config.image,
shell_escape(&command)
);
let mut command_base = std::process::Command::new("bash");
let command_executed = command_base
.arg("-c")
.arg(&docker_cmd)
.output()
.expect("Error executing docker command");
let command_success = command_executed.status.success();
let command_stdout = std::str::from_utf8(&command_executed.stdout[..])
.unwrap()
.to_owned();
let command_stderr = std::str::from_utf8(&command_executed.stderr[..])
.unwrap()
.to_owned();
return (command_success, command_stdout, command_stderr);
}
fn shell_escape(s: &str) -> String {
format!("'{}'", s.replace("'", "'\\''"))
}
pub fn main_exec(myfile: &str) {
use std::time::Instant;
let pipeline_start = Instant::now();
let main_pipeline = Pipeline::new(myfile);
let success = main_pipeline.exec_pipeline();
let duration = pipeline_start.elapsed();
let duration_str = if duration.as_secs() > 60 {
format!("{}m {}s", duration.as_secs() / 60, duration.as_secs() % 60)
} else if duration.as_secs() > 0 {
format!("{}s", duration.as_secs())
} else {
format!("{}ms", duration.as_millis())
};
println!();
if success {
println!(
"{} {}",
"Pipeline passed".green(),
format!("in {}", duration_str).dimmed()
)
} else {
println!(
"{} {}",
"Pipeline failed".red(),
format!("after {}", duration_str).dimmed()
);
exit(1);
}
}
pub fn main_start() {
let hook_path = ".git/hooks/post-commit";
let hook_content = "#!/bin/bash\nsimpleci exec\n";
if !Path::new(".git").exists() {
eprintln!("{}", "Error: Not a git repository!".red());
exit(1);
}
fs::create_dir_all(".git/hooks").unwrap_or_else(|_| {
eprintln!("{}", "Error: Could not create hooks directory!".red());
exit(1);
});
fs::write(hook_path, hook_content).unwrap_or_else(|_| {
eprintln!("{}", "Error: Could not create git hook!".red());
exit(1);
});
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(hook_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(hook_path, perms).unwrap();
}
println!("{}", "Git hook created successfully!".green());
}
pub fn main_stop() {
let hook_path = ".git/hooks/post-commit";
if Path::new(hook_path).exists() {
fs::remove_file(hook_path).unwrap_or_else(|_| {
eprintln!("{}", "Error: Could not remove git hook!".red());
exit(1);
});
println!("{}", "Git hook removed successfully!".green());
} else {
println!("{}", "No git hook found.".yellow());
}
}
pub fn main_init() {
main_start();
let ci_file = ".simple-ci.yml";
if !Path::new(ci_file).exists() {
let default_content = r#"---
script:
- echo "Hello from SimpleCI!"
- echo "Edit .simple-ci.yml to customize your pipeline"
"#;
fs::write(ci_file, default_content).unwrap_or_else(|_| {
eprintln!("{}", "Error: Could not create .simple-ci.yml!".red());
exit(1);
});
println!("{}", "Created default .simple-ci.yml file!".green());
} else {
println!(
"{}",
".simple-ci.yml already exists, not overwriting.".yellow()
);
}
fs::create_dir_all(".simple-ci").ok();
println!("{}", "Initialization complete!".green());
}
pub fn main_clean() {
let ci_dir = ".simple-ci";
if Path::new(ci_dir).exists() {
fs::remove_dir_all(ci_dir).unwrap_or_else(|_| {
eprintln!("{}", "Error: Could not remove .simple-ci directory!".red());
exit(1);
});
println!("{}", "Cleaned artifacts directory successfully!".green());
} else {
println!("{}", "No artifacts directory found.".yellow());
}
}
pub fn main_config(subcommand: Option<&str>, key: Option<&str>, value: Option<&str>) {
match subcommand {
Some("set") => {
if key.is_none() || value.is_none() {
eprintln!(
"{}",
"Error: config set requires KEY and VALUE arguments".red()
);
exit(1);
}
config_set(key.unwrap(), value.unwrap());
}
Some("get") => {
if key.is_none() {
eprintln!("{}", "Error: config get requires KEY argument".red());
exit(1);
}
config_get(key.unwrap());
}
Some("show") => {
config_show();
}
Some(cmd) => {
eprintln!(
"{}",
format!("Error: Unknown config command '{}'", cmd).red()
);
eprintln!("Valid commands: set, get, show");
exit(1);
}
None => {
config_show();
}
}
}
fn config_set(key: &str, value: &str) {
let mut config = Config::load();
let config_path = Config::get_config_path();
match key {
"shell" => {
if value != "bash" && value != "fish" && value != "zsh" && value != "sh" {
eprintln!(
"{}",
format!(
"Error: Invalid shell '{}'. Must be one of: bash, fish, zsh, sh",
value
)
.red()
);
exit(1);
}
config.shell = value.to_string();
println!("{} shell = {}", "Set".green(), value.blue());
}
"enable_colors" => {
let bool_value = match value.to_lowercase().as_str() {
"true" | "yes" | "1" => true,
"false" | "no" | "0" => false,
_ => {
eprintln!(
"{}",
format!("Error: Invalid value '{}'. Must be true/false", value).red()
);
exit(1);
}
};
config.enable_colors = bool_value;
println!("{} enable_colors = {}", "Set".green(), bool_value);
}
"artifact_dir" => {
config.artifact_dir = value.to_string();
println!("{} artifact_dir = {}", "Set".green(), value.blue());
}
"cache_dir" => {
config.cache_dir = value.to_string();
println!("{} cache_dir = {}", "Set".green(), value.blue());
}
_ => {
eprintln!("{}", format!("Error: Unknown config key '{}'", key).red());
eprintln!("Valid keys: shell, enable_colors, artifact_dir, cache_dir");
exit(1);
}
}
if let Err(e) = config.save(&config_path) {
eprintln!("{}", format!("Error saving config: {}", e).red());
exit(1);
}
}
fn config_get(key: &str) {
let config = Config::load();
match key {
"shell" => println!("{} = {}", "shell".blue(), config.shell),
"enable_colors" => println!("{} = {}", "enable_colors".blue(), config.enable_colors),
"artifact_dir" => println!("{} = {}", "artifact_dir".blue(), config.artifact_dir),
"cache_dir" => println!("{} = {}", "cache_dir".blue(), config.cache_dir),
_ => {
eprintln!("{}", format!("Error: Unknown config key '{}'", key).red());
eprintln!("Valid keys: shell, enable_colors, artifact_dir, cache_dir");
exit(1);
}
}
}
fn config_show() {
let config = Config::load();
let config_path = Config::get_config_path();
println!("{}", "SimpleCI Configuration".green().bold());
println!("{}: {}", "Config file".dimmed(), config_path);
println!();
println!("{}", config.to_yaml_string());
}