cinderella

[unmaintained] simple CI engine
Log | Files | Refs | README | LICENSE

commit b12fa5d0d51562d6700290d4ca7214d347806f3e
parent 68b85da189c1fa158329deb89b1b9110667f98b5
Author: Stefan Koch <programming@stefan-koch.name>
Date:   Fri,  1 May 2020 10:13:34 +0200

Merge branch 'master' of https://github.com/aufziehvogel/cinderella

Diffstat:
M.cinderella.toml | 3+++
MCargo.lock | 2+-
MCargo.toml | 3++-
MREADME.md | 22++++++++++++++++------
Msrc/config.rs | 3+--
Msrc/execution.rs | 81+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/main.rs | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Mtests/cli.rs | 28++++++++++++++++++++++++++++
8 files changed, 156 insertions(+), 55 deletions(-)

diff --git a/.cinderella.toml b/.cinderella.toml @@ -16,6 +16,9 @@ when = "\"%BRANCH\" == \"master\"" [build-release] commands = [ + # set the tag number as tool version + "sed -i -e 's/^version = .*/version = \"%TAG\"/' Cargo.toml", + "cargo build --release", "cp target/release/cinderella \"/opt/cinderella/cinderella-%TAG\"", ] diff --git a/Cargo.lock b/Cargo.lock @@ -90,7 +90,7 @@ dependencies = [ [[package]] name = "cinderella" -version = "0.1.0" +version = "0.0.1-dev" dependencies = [ "assert_cmd 0.11.1 (registry+https://github.com/rust-lang/crates.io-index)", "duct 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "cinderella" -version = "0.1.0" +# version is automatically set in the build pipeline +version = "0.0.1-dev" authors = ["Stefan Koch <programming@stefan-koch.name>"] edition = "2018" diff --git a/README.md b/README.md @@ -83,6 +83,15 @@ Currently supported variables are: - `%BRANCH`: The name of the branch that is built, if it is a branch - `%TAG`: The name of the tag that is built, if it is a tag +### Environment Variables + +It is possible to use environment variables in your commands, e.g. +`commands = ["echo $HOME"]`. Cinderella will substitute them by their +values before the command gets sent to the operating system. + +This is also true if you use `bash` or other shells in your commands list. +This means that in such cases the plaintext value of the environment +variable will be visible in your shell history. ### Conditions @@ -192,19 +201,20 @@ Open Points This is a list of open points that are subject to implementation: +- allow cinderella to `run` with a non-git folder: Logic should be so that + user passes a URL or a path to `run` (same as now) and Cinderella checks + if this is a URL, a local git repository or a local folder without git and + then runs the correct `Source` and `WorkingCopy` - introduce command shortcuts for commands that are often used but annoying to write in their full form - `"[bash] foo && bar"` for `"bash -c \"foo && bar\""` - `"[python-venv env] pip install -r requirements.txt && ./foo"` for `"bash -c \"source env/bin/activate && pip install -r requirements.txt && ./foo\""` -- substitute important environment variables like `$HOME` so that we do not - have to run a bash in order to use them (or all environment variables?) -- provide a way to set version numbers from a tag with Cinderella easily; i.e. - when a tag 1.2 is created a Cinderella command in the pipeline file should - set the version number at the right places (maybe this is already easily - possible, maybe some adjustments to Cinderella are needed) - keep a status of the last result per repository (to send *OK again* mails) - send a more detailed error message on the build error reason: - return code of the failed command - full log of the executed commands (in a prompt style, command followed by output) +- send a mail with all compiler warnings? (or optionally to be + enabled/disabled in .cinderella.toml?); otherwise developers never see the + warnings diff --git a/src/config.rs b/src/config.rs @@ -59,6 +59,7 @@ pub struct ExecutionConfig { impl ExecutionConfig { // TODO: This approach only works for URLs, not for local paths. + // TODO: Move the name() function to the CodeSource pub fn name(&self) -> String { self.repo_url.split('/').last().unwrap().to_string() } @@ -150,7 +151,5 @@ mod tests { secrets_file, PathBuf::from("/tmp/work-dir/.cinderella/secrets") ); - - } } diff --git a/src/execution.rs b/src/execution.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::io::{BufRead, BufReader}; +use std::env; use evalexpr; use duct::cmd; @@ -25,19 +26,16 @@ struct Command { impl Command { fn command_string(&self) -> String { - let mut command = String::from(&self.command); - - for arg in &self.args { - command.push_str(" "); + let mut parts = self.args.clone(); + parts.insert(0, String::from(&self.command)); + let command = parts.iter().map(|arg| { if arg.contains(" ") { - command.push_str("\""); - command.push_str(&arg); - command.push_str("\""); + format!("\"{}\"", arg) } else { - command.push_str(&arg); + String::from(arg) } - } + }).collect::<Vec<String>>().join(" "); command } @@ -121,33 +119,39 @@ fn execute_pipeline( pipeline: &pipeline::Pipeline, variables: &HashMap<String, String>) -> ExecutionResult { - let mut step_results = Vec::new(); + let res = pipeline.commands.iter() + .try_fold(Vec::<StepResult>::new(), |mut step_results, cmd| { + let result = execute_step(&cmd, variables); + + match result { + StepResult::Success(_, _) => { + step_results.push(result); + Ok(step_results) + }, + StepResult::Error(_, _, _) => { + step_results.push(result); + Err(step_results) + } + } + }); - for cmd in &pipeline.commands { - let cmd = replace_variables(&cmd, &variables); - // TODO: Raise error if some variables remain unsubstituted? - let parts = parser::parse_command(&cmd); + match res { + Ok(step_results) => ExecutionResult::Success(step_results), + Err(step_results) => ExecutionResult::Error(step_results), + } +} - let cmd = Command { - command: String::from(&parts[0]), - args: parts[1..].to_vec(), - }; +fn execute_step(cmd: &str, variables: &HashMap<String, String>) -> StepResult { + let cmd = replace_variables(&cmd, &variables); + let parts = parser::parse_command(&cmd); - let result = cmd.execute(); - match result { - StepResult::Success(_, _) => { - step_results.push(result); - }, - StepResult::Error(_, _, _) => { - step_results.push(result); - return ExecutionResult::Error(step_results); - }, - } - } + let cmd = Command { + command: String::from(&parts[0]), + args: parts[1..].to_vec(), + }; - ExecutionResult::Success(step_results) + cmd.execute() } - fn execute_test(test: &str, variables: &HashMap<String, String>) -> bool { // not possible to use evalexpr Context, because evalexpr only handles // standard variable names without special characters (percentage @@ -171,9 +175,24 @@ fn replace_variables(command: &str, variables: &HashMap<String, String>) res = res.replace(&varname, replacement); } + let res = replace_envvars(&res); + + res +} + +fn replace_envvars(command: &str) -> String +{ + let mut res = String::from(command); + + for (key, value) in env::vars() { + let varname = format!("${}", key.to_uppercase()); + res = res.replace(&varname, &value); + } + res } + #[cfg(test)] mod tests { use super::*; diff --git a/src/main.rs b/src/main.rs @@ -1,17 +1,27 @@ use std::env; use std::path::Path; +use std::process; use rpassword; use getopts::Options; use cinderella::ExecutionConfig; -fn print_usage(program: &str, opts: Options) { - let brief = format!("Usage: {} REPO_URL [options]", program); +fn print_usage(program: &str) { + println!("Usage: {} (run | encrypt | decrypt)", program); +} + +fn print_usage_command(program: &str, argline: &str, opts: Options) { + let brief = format!("Usage: {} {}", program, argline); print!("{}", opts.usage(&brief)); } fn main() { + const NAME: &'static str = env!("CARGO_PKG_NAME"); + const VERSION: &'static str = env!("CARGO_PKG_VERSION"); + println!("{} v{}", NAME, VERSION); + let args: Vec<String> = env::args().collect(); + let program = args[0].clone(); match args.get(1) { Some(command) => { @@ -19,25 +29,41 @@ fn main() { "run" => run(args), "encrypt" => encrypt(args), "decrypt" => decrypt(args), - _ => println!("Unknown command!"), + "--help" | "-h" => print_usage(&program), + _ => { + println!("Unknown command!"); + print_usage(&program); + }, } }, - None => println!("Please provide a command"), + None => { + println!("Please provide a command"); + print_usage(&program); + }, } } -fn parse_password_arg(args: Vec<String>) -> Option<String> { - let mut opts = Options::new(); - opts.optopt("p", "password", "set the password for encryption/decryption", "PASSWORD"); - +fn parse_password_arg(opts: &Options, args: Vec<String>) + -> Result<Option<String>, String> +{ match opts.parse(&args[2..]) { - Ok(m) => m.opt_str("p"), - Err(f) => panic!(f.to_string()), + Ok(m) => Ok(m.opt_str("p")), + Err(f) => Err(f.to_string()), } } fn encrypt(args: Vec<String>) { - let pass = match parse_password_arg(args) { + let mut opts = Options::new(); + opts.optopt("p", "password", "set the password for encryption/decryption", "PASSWORD"); + + let pass_or_none = parse_password_arg(&opts, args) + .unwrap_or_else(|msg| { + println!("{}", msg); + print_usage_command("cinderella", "encrypt [options]", opts); + process::exit(1); + }); + + let pass = match pass_or_none { Some(pass) => pass, None => rpassword::read_password_from_tty(Some("Password: ")).unwrap(), }; @@ -47,7 +73,17 @@ fn encrypt(args: Vec<String>) { } fn decrypt(args: Vec<String>) { - let pass = match parse_password_arg(args) { + let mut opts = Options::new(); + opts.optopt("p", "password", "set the password for encryption/decryption", "PASSWORD"); + + let pass_or_none = parse_password_arg(&opts, args) + .unwrap_or_else(|msg| { + println!("{}", msg); + print_usage_command("cinderella", "decrypt [options]", opts); + process::exit(1); + }); + + let pass = match pass_or_none { Some(pass) => pass, None => rpassword::read_password_from_tty(Some("Password: ")).unwrap(), }; @@ -66,13 +102,17 @@ fn run(args: Vec<String>) { let matches = match opts.parse(&args[2..]) { Ok(m) => { m }, - Err(f) => { panic!(f.to_string()) }, + Err(f) => { + println!("{}", f.to_string()); + print_usage_command(&program, "run [options] REPO", opts); + process::exit(1); + }, }; let repository_url = if !matches.free.is_empty() { matches.free[0].clone() } else { - print_usage(&program, opts); + print_usage(&program); return; }; @@ -83,5 +123,6 @@ fn run(args: Vec<String>) { cinderella_filepath: matches.opt_str("f"), }; + // TODO: Handle error from cinderella:run and display error message + usage cinderella::run(&repo) } diff --git a/tests/cli.rs b/tests/cli.rs @@ -38,3 +38,31 @@ fn test_encrypt_decrypt() { assert_eq!("MY_SECRET = \"secret\"", res.trim()); } + +#[test] +fn test_environment_variables_get_replaced() { + let dir = tempfile::tempdir().unwrap(); + let cinderella_file = dir.path().join(".cinderella.toml"); + + let mut file = File::create(&cinderella_file).unwrap(); + writeln!(file, "[test]\ncommands = [\"echo $MY_ENV_VAR\"]").unwrap(); + + // Create a git repo, TODO: Would be nicer if we could run cinderella + // also on folders without having a git repo + Command::new("git") + .arg("init") + .current_dir(&dir) + .output() + .expect("Creation of git repo failed"); + + let output = Command::cargo_bin("cinderella").unwrap() + .args(vec!["run", "-f", &cinderella_file.to_string_lossy(), "."]) + .current_dir(&dir) + .env("MY_ENV_VAR", "test-env-var") + .output() + .expect("Execution failed"); + + let stdout = String::from_utf8_lossy(&output.stdout); + + assert!(stdout.contains("test-env-var")); +}