| |
use uuid::Uuid;
|
| |
|
| |
use radicle::prelude::Profile;
|
| - |
use radicle_ci_broker::msg::{Id, Oid, Request, Response, RunId, RunResult};
|
| + |
use radicle_ci_broker::msg::{Id, MessageError, Oid, Request, Response, RunId, RunResult};
|
| |
|
| |
/// Path to the repository's CI run specification. This is relative to
|
| |
/// the root of the repository.
|
| |
const RUNSPEC_PATH: &str = ".radicle/native.yaml";
|
| |
|
| |
/// The main program.
|
| - |
fn main() -> anyhow::Result<()> {
|
| + |
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();
|
| + |
}
|
| + |
std::process::exit(1);
|
| + |
}
|
| + |
}
|
| + |
|
| + |
fn fallible_main() -> Result<(), NativeError> {
|
| |
pretty_env_logger::init_custom_env("RADICLE_NATIVE_CI_LOG");
|
| |
info!("radicle-native-ci starts");
|
| |
|
| - |
let config = std::env::var("RADICLE_NATIVE_CI")?;
|
| + |
const ENV: &str = "RADICLE_NATIVE_CI";
|
| + |
let config = std::env::var(ENV).map_err(|e| NativeError::GetEnv(ENV, e))?;
|
| |
let config = Config::read(Path::new(&config))?;
|
| |
debug!("read config: {:#?}", config);
|
| |
let state = &config.state;
|
| |
if !state.exists() {
|
| |
debug!("creating {}", state.display());
|
| - |
std::fs::create_dir_all(state)?;
|
| + |
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());
|
| |
debug!("directory for this run: {}", run_dir.display());
|
| - |
std::fs::create_dir(&run_dir)?;
|
| + |
std::fs::create_dir(&run_dir).map_err(|e| NativeError::CreateRunDir(run_dir.clone(), e))?;
|
| |
|
| |
let src = run_dir.join("src");
|
| |
let log = run_dir.join("log");
|
| |
|
| - |
let profile = Profile::load()?;
|
| + |
let profile = Profile::load().map_err(NativeError::LoadProfile)?;
|
| |
let storage = profile.storage.path();
|
| |
|
| - |
let req = Request::from_reader(std::io::stdin()).unwrap();
|
| + |
let req = Request::from_reader(std::io::stdin()).map_err(NativeError::ReadRequest)?;
|
| |
debug!("request: {:#?}", req);
|
| |
|
| |
if let Request::Trigger { repo, commit } = req {
|
| |
repo: Id,
|
| |
commit: Oid,
|
| |
src: &Path,
|
| - |
log: &Path,
|
| - |
) -> anyhow::Result<()> {
|
| - |
let mut log = Rc::new(std::fs::File::create(log)?);
|
| + |
log_filename: &Path,
|
| + |
) -> Result<(), NativeError> {
|
| + |
let mut log = Rc::new(
|
| + |
std::fs::File::create(log_filename)
|
| + |
.map_err(|e| NativeError::CreateLog(log_filename.into(), e))?,
|
| + |
);
|
| |
|
| - |
logmsg(&mut log, "=== Radicle native CI\n".into())?;
|
| - |
logmsg(&mut log, format!("=== Repository id: {}\n", repo))?;
|
| - |
logmsg(&mut log, format!("=== Commit: {}\n", commit))?;
|
| + |
logmsg(log_filename, &mut log, "=== Radicle native CI\n".into())?;
|
| + |
logmsg(
|
| + |
log_filename,
|
| + |
&mut log,
|
| + |
format!("=== Repository id: {}\n", repo),
|
| + |
)?;
|
| + |
logmsg(log_filename, &mut log, format!("=== Commit: {}\n", commit))?;
|
| |
|
| |
let first = Response::triggered(RunId::from(run_id.to_string().as_str()));
|
| - |
first.to_writer(std::io::stdout())?;
|
| + |
first
|
| + |
.to_writer(std::io::stdout())
|
| + |
.map_err(|e| NativeError::WriteResponse(first.clone(), e))?;
|
| |
|
| |
let repo = storage.join(repo.canonical());
|
| |
debug!("repo path: {}", repo.display());
|
| |
|
| |
debug!("cloning repository to {}", src.display());
|
| |
runcmd(
|
| + |
log_filename,
|
| |
&mut log,
|
| |
&[
|
| |
"git",
|
| |
|
| |
debug!("running CI in cloned repository");
|
| |
let snippet = format!("set -xeuo pipefail\n{}", &runspec.shell);
|
| - |
runcmd(&mut log, &["bash", "-c", &snippet], src)?;
|
| + |
runcmd(log_filename, &mut log, &["bash", "-c", &snippet], src)?;
|
| |
|
| |
let second = Response::finished(RunResult::Success);
|
| - |
second.to_writer(std::io::stdout())?;
|
| + |
second
|
| + |
.to_writer(std::io::stdout())
|
| + |
.map_err(|e| NativeError::WriteResponse(second.clone(), e))?;
|
| |
|
| - |
logmsg(&mut log, "CI run finished successfully".into())?;
|
| + |
logmsg(
|
| + |
log_filename,
|
| + |
&mut log,
|
| + |
"CI run finished successfully".into(),
|
| + |
)?;
|
| |
|
| |
Ok(())
|
| |
}
|
| |
|
| |
/// Run a command in a directory.
|
| - |
fn runcmd(log: &mut Rc<File>, argv: &[&str], cwd: &Path) -> anyhow::Result<()> {
|
| - |
logmsg(log, format!("=== Run: {:?}\n---\n", argv))?;
|
| + |
fn runcmd(
|
| + |
log_filename: &Path,
|
| + |
log: &mut Rc<File>,
|
| + |
argv: &[&str],
|
| + |
cwd: &Path,
|
| + |
) -> Result<(), NativeError> {
|
| + |
logmsg(log_filename, log, format!("=== Run: {:?}\n---\n", argv))?;
|
| |
let mut p = Popen::create(
|
| |
argv,
|
| |
PopenConfig {
|
| |
stderr: Redirection::Merge,
|
| |
..Default::default()
|
| |
},
|
| - |
)?;
|
| - |
p.communicate(None)?;
|
| - |
let exit = p.wait()?;
|
| + |
)
|
| + |
.map_err(|e| NativeError::PopenCreate(format!("{:?}", argv), e))?;
|
| + |
p.communicate(None)
|
| + |
.map_err(|e| NativeError::ChildComms(format!("{:?}", argv), e))?;
|
| + |
let exit = p
|
| + |
.wait()
|
| + |
.map_err(|e| NativeError::PopenFailed(format!("{:?}", argv), e))?;
|
| |
debug!("exit: {:?}", exit);
|
| - |
logmsg(log, "...\n".into())?;
|
| + |
logmsg(log_filename, log, "...\n".into())?;
|
| |
if !exit.success() {
|
| |
let error = Response::error(&format!("command failed: {:?}", argv));
|
| - |
error.to_writer(std::io::stdout())?;
|
| + |
error
|
| + |
.to_writer(std::io::stdout())
|
| + |
.map_err(|e| NativeError::WriteResponse(error.clone(), e))?;
|
| |
return Ok(());
|
| |
}
|
| |
Ok(())
|
| |
}
|
| |
|
| |
/// Write a message to the run log.
|
| - |
fn logmsg(log: &mut Rc<File>, msg: String) -> anyhow::Result<()> {
|
| - |
let x = Rc::get_mut(log).unwrap();
|
| - |
x.write_all(msg.as_bytes())?;
|
| + |
fn logmsg(filename: &Path, log: &mut Rc<File>, msg: String) -> Result<(), NativeError> {
|
| + |
let x = Rc::get_mut(log).ok_or(NativeError::Rc("log file handle"))?;
|
| + |
x.write_all(msg.as_bytes())
|
| + |
.map_err(|e| NativeError::WriteLog(filename.into(), e))?;
|
| |
Ok(())
|
| |
}
|
| |
|
| |
|
| |
impl RunSpec {
|
| |
/// Read run specification from a file.
|
| - |
fn from_file(filename: &Path) -> anyhow::Result<Self> {
|
| + |
fn from_file(filename: &Path) -> Result<Self, NativeError> {
|
| |
debug!("loading CI run spec from {}", filename.display());
|
| - |
let file = std::fs::File::open(filename)?;
|
| - |
let runspec: RunSpec = serde_yaml::from_reader(&file)?;
|
| + |
let file = std::fs::File::open(filename)
|
| + |
.map_err(|e| NativeError::ReadRunSpec(filename.into(), e))?;
|
| + |
let runspec: RunSpec = serde_yaml::from_reader(&file)
|
| + |
.map_err(|e| NativeError::ParseRunSpec(filename.into(), e))?;
|
| |
debug!("runspec: {:#?}", runspec);
|
| |
Ok(runspec)
|
| |
}
|
| |
}
|
| + |
|
| + |
#[derive(Debug, thiserror::Error)]
|
| + |
enum NativeError {
|
| + |
#[error("failed to create log file {0}")]
|
| + |
CreateLog(PathBuf, #[source] std::io::Error),
|
| + |
|
| + |
#[error("failed to write to log file {0}")]
|
| + |
WriteLog(PathBuf, #[source] std::io::Error),
|
| + |
|
| + |
#[error("failed to read configuration file {0}")]
|
| + |
ReadConfig(PathBuf, #[source] std::io::Error),
|
| + |
|
| + |
#[error("failed to read run specification file {0}")]
|
| + |
ReadRunSpec(PathBuf, #[source] std::io::Error),
|
| + |
|
| + |
#[error("failed to communicate with child process {0}")]
|
| + |
ChildComms(String, #[source] std::io::Error),
|
| + |
|
| + |
#[error("failed to run program {0}")]
|
| + |
PopenCreate(String, #[source] subprocess::PopenError),
|
| + |
|
| + |
#[error("program failed: {0}")]
|
| + |
PopenFailed(String, #[source] subprocess::PopenError),
|
| + |
|
| + |
#[error("failed to parse configuration file as YAML: {0}")]
|
| + |
ParseConfig(PathBuf, #[source] serde_yaml::Error),
|
| + |
|
| + |
#[error("failed to parse run spec as YAML: {0}")]
|
| + |
ParseRunSpec(PathBuf, #[source] serde_yaml::Error),
|
| + |
|
| + |
#[error("failed to read request from stdin: {0:?}")]
|
| + |
ReadRequest(#[source] MessageError),
|
| + |
|
| + |
#[error("failed to write response to stdout: {0:?}")]
|
| + |
WriteResponse(Response, #[source] MessageError),
|
| + |
|
| + |
#[error("failed to get resource counted value: {0}")]
|
| + |
Rc(&'static str),
|
| + |
|
| + |
#[error("failed to get environment variable {0}")]
|
| + |
GetEnv(&'static str, #[source] std::env::VarError),
|
| + |
|
| + |
#[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),
|
| + |
}
|