diff options
author | Lars Wirzenius <liw@liw.fi> | 2024-01-16 18:13:44 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2024-01-16 19:20:27 +0200 |
commit | 211bb3a7d18f5e2e1ad98ef661f036eddd4e652f (patch) | |
tree | 1d5b66c248742e3b2ad712a195d1096da5010cdc | |
parent | 52ba6a9081ee0b6e80a4db7a9548fe255d51e431 (diff) | |
download | radicle-native-ci-211bb3a7d18f5e2e1ad98ef661f036eddd4e652f.tar.gz |
refactor: use engine in main
Also introduce exit codes: 0 for success, 1 for problem in the
repository under test, 2 for engine problem.
Signed-off-by: Lars Wirzenius <liw@liw.fi>
-rw-r--r-- | src/bin/radicle-native-ci.rs | 363 | ||||
-rw-r--r-- | src/engine.rs | 31 | ||||
-rw-r--r-- | src/lib.rs | 1 | ||||
-rw-r--r-- | src/msg.rs | 6 | ||||
-rw-r--r-- | src/run.rs | 6 | ||||
-rw-r--r-- | src/runcmd.rs | 56 |
6 files changed, 43 insertions, 420 deletions
diff --git a/src/bin/radicle-native-ci.rs b/src/bin/radicle-native-ci.rs index 729514c..f4e688f 100644 --- a/src/bin/radicle-native-ci.rs +++ b/src/bin/radicle-native-ci.rs @@ -12,352 +12,39 @@ //! cargo test --locked --workspace //! ``` -use std::{ - error::Error, - path::{Path, PathBuf}, -}; +use std::{error::Error, process::exit}; -use uuid::Uuid; +use radicle_native_ci::engine::{Engine, EngineError}; -use radicle::prelude::Profile; -use radicle_ci_broker::msg::{Id, Oid, Request, RunId, RunResult}; - -use radicle_native_ci::{ - config::{Config, ConfigError}, - logfile::{AdminLog, LogError}, - msg::{ - read_request, write_errored, write_failed, write_succeeded, write_triggered, - NativeMessageError, - }, - report, - runcmd::{runcmd, RunCmdError}, - runinfo::{RunInfo, RunInfoBuilder, RunInfoError}, - runlog::{RunLog, RunLogError}, - runspec::{RunSpec, RunSpecError}, -}; - -/// Path to the repository's CI run specification. This is relative to -/// the root of the repository. -const RUNSPEC_PATH: &str = ".radicle/native.yaml"; +// Exit codes for the program. +const EXIT_OK: i32 = 0; +const EXIT_FAILURE: i32 = 1; +const EXIT_ERROR: i32 = 2; /// The main program. fn main() { - if let Err(e) = fallible_main() { - eprintln!("ERROR: {}", e); - let mut e = e.source(); - while let Some(source) = e { - eprintln!("caused by: {}", source); - e = source.source(); + let code = match fallible_main() { + Ok(success) => { + if success { + EXIT_OK + } else { + EXIT_FAILURE + } } - std::process::exit(1); - } -} - -fn fallible_main() -> Result<(), NativeError> { - let config = Config::load_via_env()?; - let mut adminlog = config.open_log()?; - - let mut builder = RunInfo::builder(); - - let result = fallible_main_inner(&config, &mut adminlog, &mut builder); - let ri = builder.build()?; - - match &ri.result { - RunResult::Success => write_succeeded()?, - RunResult::Failure => write_failed()?, - RunResult::Error(s) => write_errored(s)?, - _ => write_errored(&format!("unknown result {}", ri.result))?, - } - - ri.write()?; - - adminlog.writeln(&format!("update report page in {}", config.state.display()))?; - if let Err(e) = report::build_report(&config.state) { - adminlog.writeln(&format!("report generation failed: {}", e))?; - } - adminlog.writeln(&format!("radicle-native-ci ends: {:?}", result))?; - result -} - -fn fallible_main_inner( - config: &Config, - adminlog: &mut AdminLog, - builder: &mut RunInfoBuilder, -) -> Result<(), NativeError> { - let (run_id, run_dir) = mkdir_run(config)?; - let run_id = RunId::from(format!("{}", run_id).as_str()); - adminlog.writeln(&format!("run directory {}", run_dir.display()))?; - - let src = run_dir.join("src"); - let run_log = run_dir.join("log.html"); - let run_info_file = run_dir.join("run.yaml"); - - let profile = Profile::load().map_err(NativeError::LoadProfile)?; - let storage = profile.storage.path(); - - let req = read_request()?; - adminlog.writeln(&format!("request: {:#?}", req))?; - - builder.run_id(run_id.clone()); - builder.log(&config.state, run_log.clone()); - builder.run_info(run_info_file.clone()); - - let run_log = RunLog::new(&run_log); - - if let Request::Trigger { repo, commit } = req { - builder.repo(repo); - builder.commit(commit); - let mut runner = RunnerBuilder::default() - .run_id(run_id) - .storage(storage) - .repo(repo) - .commit(commit) - .src(&src) - .adminlog(adminlog) - .run_log(run_log) - .timeout(config.timeout) - .builder(builder) - .build()?; - let result = runner.run(); - if let Err(e) = result { - adminlog.writeln(&format!("CI failed: {:?}", e))?; - builder.result(RunResult::Error(format!("{}", e))); - return Err(e); + Err(e) => { + eprintln!("ERROR: {}", e); + let mut e = e.source(); + while let Some(source) = e { + eprintln!("caused by: {}", source); + e = source.source(); + } + EXIT_ERROR } - adminlog.writeln("CI run exited zero")?; - builder.result(RunResult::Success); - } else { - builder.result(RunResult::Error("first request was not Trigger".into())); }; - - adminlog.writeln("radicle-native-ci ends successfully")?; - Ok(()) -} - -/// Create a per-run directory. -fn mkdir_run(config: &Config) -> Result<(Uuid, PathBuf), NativeError> { - let state = &config.state; - if !state.exists() { - std::fs::create_dir_all(state).map_err(|e| NativeError::CreateState(state.into(), e))?; - } - - let run_id = Uuid::new_v4(); - let run_dir = state.join(run_id.to_string()); - std::fs::create_dir(&run_dir).map_err(|e| NativeError::CreateRunDir(run_dir.clone(), e))?; - Ok((run_id, run_dir)) + exit(code); } -#[derive(Debug)] -struct Runner<'a> { - run_id: RunId, - storage: PathBuf, - repo: Id, - commit: Oid, - src: PathBuf, - adminlog: &'a mut AdminLog, - run_log: RunLog, - timeout: Option<usize>, - builder: &'a mut RunInfoBuilder, -} - -impl<'a> Runner<'a> { - fn git_clone(&mut self, repo_path: &Path) -> Result<(), NativeError> { - self.adminlog.writeln("clone repository")?; - runcmd( - &mut self.run_log, - &[ - "git", - "clone", - repo_path.to_str().unwrap(), - self.src.to_str().unwrap(), - ], - Path::new("."), - )?; - Ok(()) - } - - fn git_checkout(&mut self) -> Result<(), NativeError> { - self.adminlog.writeln("check out commit")?; - runcmd( - &mut self.run_log, - &["git", "checkout", &self.commit.to_string()], - &self.src, - )?; - Ok(()) - } - - /// Perform the CI run. - fn run(&mut self) -> Result<(), NativeError> { - self.adminlog - .writeln(&format!("CI run on {}, {}", self.repo, self.commit))?; - - self.run_log.title("Log from Radicle native CI"); - self.run_log.rid(self.repo); - self.run_log.commit(self.commit); - - write_triggered(&self.run_id)?; - - let repo_path = self.storage.join(self.repo.canonical()); - - self.git_clone(&repo_path)?; - self.git_checkout()?; - - let runspec = RunSpec::from_file(&self.src.join(RUNSPEC_PATH))?; - self.adminlog - .writeln(&format!("CI run spec: {:#?}", runspec))?; - - self.adminlog.writeln("run shell snippet in repository")?; - let snippet = format!("set -xeuo pipefail\n{}", &runspec.shell); - let runcmd_result = if let Some(timeout) = self.timeout { - let timeout = format!("{}", timeout); - runcmd( - &mut self.run_log, - &["timeout", &timeout, "bash", "-c", &snippet], - &self.src, - ) - } else { - runcmd(&mut self.run_log, &["bash", "-c", &snippet], &self.src) - }; - - let result = if runcmd_result.is_ok() { - RunResult::Success - } else if let Err(RunCmdError::CommandFailed(exit, argv)) = &runcmd_result { - let msg = format!("command failed: exit: {}, argv: {:?}", exit, argv); - RunResult::Error(msg) - } else { - RunResult::Failure - }; - - std::fs::remove_dir_all(&self.src) - .map_err(|e| NativeError::RemoveDir(self.src.clone(), e))?; - - self.builder.result(result); - - if let Err(e) = self.run_log.write() { - self.adminlog - .writeln(&format!("failed to write run log: {}", e))?; - } - - if let Err(e) = runcmd_result { - Err(e.into()) - } else { - Ok(()) - } - } -} - -#[derive(Debug, Default)] -struct RunnerBuilder<'a> { - run_id: Option<RunId>, - storage: Option<PathBuf>, - repo: Option<Id>, - commit: Option<Oid>, - src: Option<PathBuf>, - adminlog: Option<&'a mut AdminLog>, - run_log: Option<RunLog>, - timeout: Option<usize>, - builder: Option<&'a mut RunInfoBuilder>, -} - -impl<'a> RunnerBuilder<'a> { - fn run_id(mut self, run_id: RunId) -> Self { - self.run_id = Some(run_id); - self - } - - fn storage(mut self, path: &Path) -> Self { - self.storage = Some(path.into()); - self - } - - fn repo(mut self, id: Id) -> Self { - self.repo = Some(id); - self - } - - fn commit(mut self, oid: Oid) -> Self { - self.commit = Some(oid); - self - } - - fn src(mut self, path: &Path) -> Self { - self.src = Some(path.into()); - self - } - - fn adminlog(mut self, log: &'a mut AdminLog) -> Self { - self.adminlog = Some(log); - self - } - - fn run_log(mut self, run_log: RunLog) -> Self { - self.run_log = Some(run_log); - self - } - - fn timeout(mut self, timeout: Option<usize>) -> Self { - self.timeout = timeout; - self - } - - fn builder(mut self, builder: &'a mut RunInfoBuilder) -> Self { - self.builder = Some(builder); - self - } - - fn build(self) -> Result<Runner<'a>, NativeError> { - Ok(Runner { - run_id: self.run_id.ok_or(NativeError::Unset("run_id"))?, - storage: self.storage.ok_or(NativeError::Unset("storage"))?, - repo: self.repo.ok_or(NativeError::Unset("repo"))?, - commit: self.commit.ok_or(NativeError::Unset("commit"))?, - src: self.src.ok_or(NativeError::Unset("src"))?, - adminlog: self.adminlog.ok_or(NativeError::Unset("log"))?, - run_log: self.run_log.ok_or(NativeError::Unset("run_log"))?, - timeout: self.timeout, - builder: self.builder.ok_or(NativeError::Unset("builder"))?, - }) - } -} - -#[derive(Debug, thiserror::Error)] -enum NativeError { - #[error("failed to create per-run parent directory {0}")] - CreateState(PathBuf, #[source] std::io::Error), - - #[error("failed to create per-run directory {0}")] - CreateRunDir(PathBuf, #[source] std::io::Error), - - #[error("failed to load Radicle profile")] - LoadProfile(#[source] radicle::profile::Error), - - #[error("failed to remove {0}")] - RemoveDir(PathBuf, #[source] std::io::Error), - - #[error("programming error: failed to set field {0}")] - Unset(&'static str), - - #[error(transparent)] - Config(#[from] ConfigError), - - #[error(transparent)] - Log(#[from] LogError), - - #[error(transparent)] - Message(#[from] NativeMessageError), - - #[error(transparent)] - Report(#[from] report::ReportError), - - #[error(transparent)] - RunCmd(#[from] RunCmdError), - - #[error(transparent)] - RunInfo(#[from] RunInfoError), - - #[error(transparent)] - RunLog(#[from] RunLogError), - - #[error(transparent)] - RunSpec(#[from] RunSpecError), +fn fallible_main() -> Result<bool, EngineError> { + let mut engine = Engine::new()?; + engine.run() } diff --git a/src/engine.rs b/src/engine.rs index 9531ae3..2444631 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -14,7 +14,6 @@ use crate::{ }, report, run::{Run, RunError}, - runcmd::RunCmdError, runinfo::{RunInfo, RunInfoBuilder, RunInfoError}, runlog::RunLogError, runspec::RunSpecError, @@ -54,7 +53,7 @@ impl Engine { /// from stdin, write responses to stdout. Update node admin log /// with any problems that aren't inherent in the git repository /// (those go into the run log). - pub fn run(&mut self) -> Result<(), EngineError> { + pub fn run(&mut self) -> Result<bool, EngineError> { let req = match self.setup() { Ok(req) => req, Err(e) => { @@ -64,15 +63,20 @@ impl Engine { }; // Check that we got the right kind of request. + let mut success = false; match req { Request::Trigger { repo, commit } => { - if let Err(e) = self.run_helper(repo, commit) { - // If the run helper return an error, something - // went wrong in that is not due to the repository - // under test. So we don't put it in the run log, - // but the admin log and return it to the caller. - self.adminlog.writeln(&format!("Error running CI: {}", e))?; - return Err(e); + match self.run_helper(repo, commit) { + Ok(true) => success = true, + Ok(false) => (), + Err(e) => { + // If the run helper return an error, something + // went wrong in that is not due to the repository + // under test. So we don't put it in the run log, + // but the admin log and return it to the caller. + self.adminlog.writeln(&format!("Error running CI: {}", e))?; + return Err(e); + } } } _ => { @@ -89,7 +93,7 @@ impl Engine { return Err(e); } - Ok(()) + Ok(success) } // Set up CI to run. If something goes wrong, return the error, @@ -134,7 +138,7 @@ impl Engine { // Execute the CI run. Log any problems to a log for this run, and // persist that. Update the run info builder as needed. - fn run_helper(&mut self, rid: Id, commit: Oid) -> Result<(), EngineError> { + fn run_helper(&mut self, rid: Id, commit: Oid) -> Result<bool, EngineError> { // Pick a run id and create a directory for files related to // the run. let (run_id, run_dir) = mkdir_run(&self.config)?; @@ -166,7 +170,7 @@ impl Engine { }; self.run_info_builder.result(result); - Ok(()) + Ok(run_log.all_commands_succeeded()) } /// Report results to caller (via stdout) and to users (via report @@ -219,9 +223,6 @@ pub enum EngineError { Report(#[from] report::ReportError), #[error(transparent)] - RunCmd(#[from] RunCmdError), - - #[error(transparent)] RunInfo(#[from] RunInfoError), #[error(transparent)] @@ -4,7 +4,6 @@ pub mod logfile; pub mod msg; pub mod report; pub mod run; -pub mod runcmd; pub mod runinfo; pub mod runlog; pub mod runspec; @@ -3,8 +3,7 @@ use std::path::PathBuf; use radicle_ci_broker::msg::{MessageError, Request, Response, RunId, RunResult}; use crate::{ - config::ConfigError, logfile::LogError, report, runcmd::RunCmdError, runinfo::RunInfoError, - runspec::RunSpecError, + config::ConfigError, logfile::LogError, report, runinfo::RunInfoError, runspec::RunSpecError, }; /// Read a request from stdin. @@ -82,9 +81,6 @@ pub enum NativeMessageError { Report(#[from] report::ReportError), #[error(transparent)] - RunCmd(#[from] RunCmdError), - - #[error(transparent)] RunInfo(#[from] RunInfoError), #[error(transparent)] @@ -8,7 +8,6 @@ use radicle_ci_broker::msg::{Id, Oid, RunId}; use crate::{ msg::NativeMessageError, report, - runcmd::RunCmdError, runinfo::RunInfoError, runlog::{RunLog, RunLogError}, runspec::{RunSpec, RunSpecError}, @@ -187,7 +186,7 @@ impl Run { self.run_log.runcmd( argv, &cwd.canonicalize() - .map_err(|e| RunCmdError::Canonicalize(cwd.into(), e))?, + .map_err(|e| RunError::Canonicalize(cwd.into(), e))?, exit, &output.stdout, &output.stderr, @@ -222,9 +221,6 @@ pub enum RunError { Report(#[from] report::ReportError), #[error(transparent)] - RunCmd(#[from] RunCmdError), - - #[error(transparent)] RunInfo(#[from] RunInfoError), #[error(transparent)] diff --git a/src/runcmd.rs b/src/runcmd.rs deleted file mode 100644 index 831c72a..0000000 --- a/src/runcmd.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - process::Command, -}; - -use radicle_ci_broker::msg::{MessageError, Response}; - -use crate::runlog::{RunLog, RunLogError}; - -/// Run a command in a directory. -pub fn runcmd(run_log: &mut RunLog, argv: &[&str], cwd: &Path) -> Result<(), RunCmdError> { - assert!(!argv.is_empty()); - let argv0 = argv[0]; - let output = Command::new(argv0) - .args(&argv[1..]) - .current_dir(cwd) - .output() - .map_err(|e| RunCmdError::Command(argv.iter().map(|s| s.to_string()).collect(), e))?; - - let exit = output.status; - - run_log.runcmd( - argv, - &cwd.canonicalize() - .map_err(|e| RunCmdError::Canonicalize(cwd.into(), e))?, - exit.code().unwrap(), - &output.stdout, - &output.stderr, - ); - - if !exit.success() { - return Err(RunCmdError::CommandFailed( - exit.code().unwrap(), - argv.iter().map(|s| s.to_string()).collect(), - )); - } - Ok(()) -} - -#[derive(Debug, thiserror::Error)] -pub enum RunCmdError { - #[error("failed to write response to stdout: {0:?}")] - WriteResponse(Response, #[source] MessageError), - - #[error(transparent)] - RunLog(#[from] RunLogError), - - #[error("failed to run command {0:?}")] - Command(Vec<String>, #[source] std::io::Error), - - #[error("command failed with exit code {0}: {1:?}")] - CommandFailed(i32, Vec<String>), - - #[error("failed to make pathname absolute: {0}")] - Canonicalize(PathBuf, #[source] std::io::Error), -} |