summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2024-01-16 18:13:44 +0200
committerLars Wirzenius <liw@liw.fi>2024-01-16 19:20:27 +0200
commit211bb3a7d18f5e2e1ad98ef661f036eddd4e652f (patch)
tree1d5b66c248742e3b2ad712a195d1096da5010cdc
parent52ba6a9081ee0b6e80a4db7a9548fe255d51e431 (diff)
downloadradicle-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.rs363
-rw-r--r--src/engine.rs31
-rw-r--r--src/lib.rs1
-rw-r--r--src/msg.rs6
-rw-r--r--src/run.rs6
-rw-r--r--src/runcmd.rs56
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)]
diff --git a/src/lib.rs b/src/lib.rs
index 67b3312..1ad51d6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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;
diff --git a/src/msg.rs b/src/msg.rs
index 6d1c90f..06b98c6 100644
--- a/src/msg.rs
+++ b/src/msg.rs
@@ -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)]
diff --git a/src/run.rs b/src/run.rs
index 1a0ae10..ce5eb77 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -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),
-}