mirror of
https://seed.radicle.dev/rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE.git
synced 2026-10-31 00:41:00 +08:00
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:
+25
-338
@@ -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
@@ -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),
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
Reference in New Issue
Block a user