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>
This commit is contained in:
Lars Wirzenius
2024-01-16 19:20:27 +02:00
parent 52ba6a9081
commit 211bb3a7d1
6 changed files with 43 additions and 420 deletions
+25 -338
View File
@@ -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(())
exit(code);
}
/// 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))
}
#[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()
}
+16 -15
View File
@@ -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
@@ -218,9 +222,6 @@ pub enum EngineError {
#[error(transparent)]
Report(#[from] report::ReportError),
#[error(transparent)]
RunCmd(#[from] RunCmdError),
#[error(transparent)]
RunInfo(#[from] RunInfoError),
-1
View File
@@ -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;
+1 -5
View File
@@ -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.
@@ -81,9 +80,6 @@ pub enum NativeMessageError {
#[error(transparent)]
Report(#[from] report::ReportError),
#[error(transparent)]
RunCmd(#[from] RunCmdError),
#[error(transparent)]
RunInfo(#[from] RunInfoError),
+1 -5
View File
@@ -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,
@@ -221,9 +220,6 @@ pub enum RunError {
#[error(transparent)]
Report(#[from] report::ReportError),
#[error(transparent)]
RunCmd(#[from] RunCmdError),
#[error(transparent)]
RunInfo(#[from] RunInfoError),
-56
View File
@@ -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),
}