Radish alpha
r
rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE
Radicle CI adapter for native CI
Radicle
Git
add NativeError
Lars Wirzenius committed 2 years ago
commit 8bf63eb6fa6397942df0bdc678e79185c37ffaa1
parent b6ff350
2 files changed +133 -35
modified Cargo.lock
@@ -983,7 +983,6 @@ dependencies = [
name = "radicle-ci-broker"
version = "0.1.0"
dependencies = [
-
 "anyhow",
 "log",
 "pretty_env_logger",
 "radicle",
modified src/main.rs
@@ -13,6 +13,7 @@
//! ```

use std::{
+
    error::Error,
    fs::File,
    io::Write,
    path::{Path, PathBuf},
@@ -25,38 +26,51 @@ use subprocess::{Popen, PopenConfig, Redirection};
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 {
@@ -64,7 +78,9 @@ fn main() -> anyhow::Result<()> {
        run(run_id, storage, repo, commit, &src, &log)?;
    } else {
        let error = Response::error("first request was not Trigger");
-
        error.to_writer(std::io::stdout())?;
+
        error
+
            .to_writer(std::io::stdout())
+
            .map_err(|e| NativeError::WriteResponse(error.clone(), e))?;
    }

    info!("radicle-native-ci ends");
@@ -78,22 +94,32 @@ fn run(
    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",
@@ -108,19 +134,30 @@ fn run(

    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 {
@@ -129,23 +166,30 @@ fn runcmd(log: &mut Rc<File>, argv: &[&str], cwd: &Path) -> anyhow::Result<()> {
            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(())
}

@@ -160,9 +204,11 @@ struct Config {

impl Config {
    /// Read configuration specification from a file.
-
    fn read(filename: &Path) -> anyhow::Result<Self> {
-
        let file = std::fs::File::open(filename)?;
-
        let config = serde_yaml::from_reader(&file)?;
+
    fn read(filename: &Path) -> Result<Self, NativeError> {
+
        let file = std::fs::File::open(filename)
+
            .map_err(|e| NativeError::ReadConfig(filename.into(), e))?;
+
        let config = serde_yaml::from_reader(&file)
+
            .map_err(|e| NativeError::ParseConfig(filename.into(), e))?;
        Ok(config)
    }
}
@@ -179,11 +225,64 @@ pub struct RunSpec {

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),
+
}