mirror of
https://seed.radicle.dev/rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE.git
synced 2026-10-27 15:20:39 +08:00
Reduce code in the native CI adapter by using library code from the Radicle CI broker. Signed-off-by: Lars Wirzenius <liw@liw.fi>
307 lines
10 KiB
Rust
307 lines
10 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use uuid::Uuid;
|
|
|
|
use radicle::prelude::Profile;
|
|
use radicle_ci_broker::msg::{
|
|
helper::{read_request, write_failed, write_succeeded, write_triggered, NativeMessageError},
|
|
EventCommonFields, Oid, Patch, PatchEvent, PushEvent, RepoId, Repository, Request, RunId,
|
|
RunResult,
|
|
};
|
|
|
|
use crate::{
|
|
config::{Config, ConfigError},
|
|
logfile::{AdminLog, LogError},
|
|
run::{Run, RunError},
|
|
runlog::RunLogError,
|
|
runspec::RunSpecError,
|
|
};
|
|
|
|
/// Run CI for a project once.
|
|
#[derive(Debug)]
|
|
pub struct Engine {
|
|
config: Config,
|
|
adminlog: AdminLog,
|
|
result: Option<RunResult>,
|
|
}
|
|
|
|
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.
|
|
#[allow(clippy::result_large_err)]
|
|
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,
|
|
result: None,
|
|
})
|
|
}
|
|
|
|
/// Return config that has been loaded for the engine.
|
|
pub fn config(&self) -> &Config {
|
|
&self.config
|
|
}
|
|
|
|
/// 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).
|
|
#[allow(clippy::result_large_err)]
|
|
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 {
|
|
common:
|
|
EventCommonFields {
|
|
repository: Repository { name, .. },
|
|
..
|
|
},
|
|
push: Some(PushEvent { branch, .. }),
|
|
..
|
|
} => {
|
|
let repo = req.repo();
|
|
let commit = req.commit().map_err(EngineError::BrokerMessage)?;
|
|
|
|
match self.run_helper(repo, name, req.clone(), commit, Some(branch), None) {
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
Request::Trigger {
|
|
common:
|
|
EventCommonFields {
|
|
repository: Repository { name, .. },
|
|
..
|
|
},
|
|
patch:
|
|
Some(PatchEvent {
|
|
patch: Patch { id, title, .. },
|
|
..
|
|
}),
|
|
..
|
|
} => {
|
|
let repo = req.repo();
|
|
let commit = req.commit().map_err(EngineError::BrokerMessage)?;
|
|
self.adminlog
|
|
.writeln(&format!("run CI for {repo} commit {commit}"))?;
|
|
|
|
match self.run_helper(repo, name, req.clone(), commit, None, Some((*id, title))) {
|
|
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.
|
|
#[allow(clippy::result_large_err)]
|
|
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.
|
|
#[allow(clippy::result_large_err)]
|
|
fn finish(&mut self) -> Result<(), EngineError> {
|
|
// Write response message indicating the run has finished.
|
|
match &self.result {
|
|
Some(RunResult::Success) => write_succeeded()?,
|
|
Some(RunResult::Failure) => write_failed()?,
|
|
_ => panic!("do not know how to handle {:#?}", self.result),
|
|
}
|
|
|
|
// 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.
|
|
#[allow(clippy::result_large_err)]
|
|
fn run_helper(
|
|
&mut self,
|
|
rid: RepoId,
|
|
name: &str,
|
|
req: Request,
|
|
commit: Oid,
|
|
branch: Option<&str>,
|
|
patch: Option<(Oid, &str)>,
|
|
) -> 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.
|
|
let run_log_filename = run_dir.join("log.html");
|
|
|
|
// 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.
|
|
if let Some(url) = &self.config.base_url {
|
|
let url = if url.ends_with('/') {
|
|
format!("{url}{}/log.html", run_id)
|
|
} else {
|
|
format!("{url}/{}/log.html", run_id)
|
|
};
|
|
write_triggered(&run_id, Some(&url))?;
|
|
} else {
|
|
write_triggered(&run_id, None)?;
|
|
}
|
|
|
|
// Create and set up the run.
|
|
let mut run = Run::new(run_id, &run_dir, &run_log_filename)?;
|
|
run.set_repository(rid, name);
|
|
run.set_request(req);
|
|
run.set_commit(commit);
|
|
run.set_storage(storage);
|
|
|
|
// Actually run. Examine the run log to decide if the run
|
|
// succeeded or failed.
|
|
let result = run.run();
|
|
if let Ok(mut run_log) = result {
|
|
if let Some(branch) = branch {
|
|
run_log.branch(branch);
|
|
}
|
|
if let Some((patch, title)) = patch {
|
|
run_log.patch(patch, title);
|
|
}
|
|
self.result = if run_log.all_commands_succeeded() {
|
|
Some(RunResult::Success)
|
|
} else {
|
|
Some(RunResult::Failure)
|
|
};
|
|
let all = run_log.all_commands_succeeded();
|
|
Ok(all)
|
|
} else {
|
|
self.result = Some(RunResult::Failure);
|
|
Ok(false)
|
|
}
|
|
}
|
|
|
|
/// Report results to caller (via stdout) and to users (via report
|
|
/// on web page).
|
|
#[allow(clippy::result_large_err)]
|
|
pub fn report(&mut self) -> Result<(), EngineError> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Create a per-run directory.
|
|
#[allow(clippy::result_large_err)]
|
|
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)]
|
|
#[allow(clippy::large_enum_variant)]
|
|
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("programming error: failed to set field {0}")]
|
|
Unset(&'static str),
|
|
|
|
#[error(transparent)]
|
|
Config(#[from] ConfigError),
|
|
|
|
#[error(transparent)]
|
|
Log(#[from] LogError),
|
|
|
|
#[error(transparent)]
|
|
BrokerMessage(#[from] radicle_ci_broker::msg::MessageError),
|
|
|
|
#[error(transparent)]
|
|
Message(#[from] NativeMessageError),
|
|
|
|
#[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),
|
|
}
|