mirror of
https://seed.radicle.dev/rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE.git
synced 2026-11-10 05:30:44 +08:00
244 lines
8.0 KiB
Rust
244 lines
8.0 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use uuid::Uuid;
|
|
|
|
use radicle::prelude::Profile;
|
|
use radicle_ci_broker::msg::{Id, Oid, Request, RunId, RunResult};
|
|
|
|
use crate::{
|
|
config::{Config, ConfigError},
|
|
logfile::{AdminLog, LogError},
|
|
msg::{
|
|
read_request, write_errored, write_failed, write_succeeded, write_triggered,
|
|
NativeMessageError,
|
|
},
|
|
report,
|
|
run::{Run, RunError},
|
|
runinfo::{RunInfo, RunInfoBuilder, RunInfoError},
|
|
runlog::RunLogError,
|
|
runspec::RunSpecError,
|
|
};
|
|
|
|
/// Run CI for a project once.
|
|
#[derive(Debug)]
|
|
pub struct Engine {
|
|
config: Config,
|
|
adminlog: AdminLog,
|
|
run_info_builder: RunInfoBuilder,
|
|
}
|
|
|
|
impl Engine {
|
|
/// Create a new engine and prepare for actually running CI. This
|
|
/// may fail, for various reasons, such as the configuration file
|
|
/// not existing. The caller should handle the error in some
|
|
/// suitable way, such as report that to its stderr, and exit
|
|
/// non-zero.
|
|
pub fn new() -> Result<Self, EngineError> {
|
|
// Get config, open admin log for writing. If either of these
|
|
// fails, we can't write about the problem to the admin log,
|
|
// but we can return an error to the caller.
|
|
let config = Config::load_via_env()?;
|
|
let adminlog = config.open_log()?;
|
|
|
|
// From here on, it's plain sailing.
|
|
|
|
Ok(Self {
|
|
config,
|
|
adminlog,
|
|
run_info_builder: RunInfo::builder(),
|
|
})
|
|
}
|
|
|
|
/// Set up and run CI on a project once: read the trigger request
|
|
/// 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<bool, EngineError> {
|
|
let req = match self.setup() {
|
|
Ok(req) => req,
|
|
Err(e) => {
|
|
self.adminlog.writeln(&format!("Error setting up: {}", e))?;
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
// Check that we got the right kind of request.
|
|
let mut success = false;
|
|
match req {
|
|
Request::Trigger { repo, commit } => {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
_ => {
|
|
// Protocol error. Log in admin log and report to caller.
|
|
self.adminlog
|
|
.writeln("First request was not a message to trigger a run.")?;
|
|
return Err(EngineError::NotTrigger(req));
|
|
}
|
|
}
|
|
|
|
if let Err(e) = self.finish() {
|
|
self.adminlog
|
|
.writeln(&format!("Error finishing up: {}", e))?;
|
|
return Err(e);
|
|
}
|
|
|
|
Ok(success)
|
|
}
|
|
|
|
// Set up CI to run. If something goes wrong, return the error,
|
|
// and assume the caller logs it to the admin log.
|
|
fn setup(&mut self) -> Result<Request, EngineError> {
|
|
// Write something to the admin log to indicate we start.
|
|
self.adminlog.writeln("Native CI run starts")?;
|
|
|
|
// Read request and log it.
|
|
let req = read_request()?;
|
|
self.adminlog.writeln(&format!("request: {:#?}", req))?;
|
|
|
|
Ok(req)
|
|
}
|
|
|
|
// Finish up after a CI run. If something goes wrong, return the
|
|
// error, and assume the caller logs it to the admin log.
|
|
fn finish(&mut self) -> Result<(), EngineError> {
|
|
// Write response message indicating the run has finished.
|
|
let ri = self.run_info_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))?,
|
|
}
|
|
|
|
// Persist the run info. If this fails, it's a node admin problem.
|
|
if let Err(e) = ri.write() {
|
|
self.adminlog
|
|
.writeln(&format!("failed to write run info: {}", e))?;
|
|
}
|
|
|
|
// Generate a report page of all CI runs.
|
|
report::build_report(&self.config.state)?;
|
|
|
|
// Log that we've reached the end successfully.
|
|
self.adminlog.writeln("Native CI ends successfully")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// 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<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)?;
|
|
let run_id = RunId::from(format!("{}", run_id).as_str());
|
|
|
|
// Set the file where the run info should be written, now that
|
|
// we have the run directory.
|
|
self.run_info_builder.run_info(run_dir.join("run.yaml"));
|
|
|
|
// Get node git storage. We do this before responding to the
|
|
// trigger, so that if this fails, we haven't said that a run
|
|
// has started.
|
|
let profile = Profile::load().map_err(EngineError::LoadProfile)?;
|
|
let storage = profile.storage.path();
|
|
|
|
// Write response to indicate run has been triggered.
|
|
write_triggered(&run_id)?;
|
|
|
|
// Create and set up the run.
|
|
let mut run = Run::new(&run_dir, run_id)?;
|
|
run.set_repository(rid);
|
|
run.set_commit(commit);
|
|
run.set_storage(storage);
|
|
run.set_timeout(self.config.timeout());
|
|
|
|
// Actually run. Examine the run log to decide if the run
|
|
// succeeded or failed.
|
|
let run_log = run.run()?;
|
|
let result = if run_log.all_commands_succeeded() {
|
|
RunResult::Success
|
|
} else {
|
|
RunResult::Failure
|
|
};
|
|
self.run_info_builder.result(result);
|
|
|
|
Ok(run_log.all_commands_succeeded())
|
|
}
|
|
|
|
/// Report results to caller (via stdout) and to users (via report
|
|
/// on web page).
|
|
pub fn report(&mut self) -> Result<(), EngineError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Create a per-run directory.
|
|
fn mkdir_run(config: &Config) -> Result<(Uuid, PathBuf), EngineError> {
|
|
let state = &config.state;
|
|
if !state.exists() {
|
|
std::fs::create_dir_all(state).map_err(|e| EngineError::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| EngineError::CreateRunDir(run_dir.clone(), e))?;
|
|
Ok((run_id, run_dir))
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum EngineError {
|
|
#[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)]
|
|
RunInfo(#[from] RunInfoError),
|
|
|
|
#[error(transparent)]
|
|
RunLog(#[from] RunLogError),
|
|
|
|
#[error(transparent)]
|
|
RunSpec(#[from] RunSpecError),
|
|
|
|
#[error(transparent)]
|
|
Run(#[from] RunError),
|
|
|
|
#[error("request message was not a trigger message: {0:?}")]
|
|
NotTrigger(Request),
|
|
}
|