Radish alpha
r
rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE
Radicle CI adapter for native CI
Radicle
Git
refactor: use engine in main
Lars Wirzenius committed 2 years ago
commit 211bb3a7d18f5e2e1ad98ef661f036eddd4e652f
parent 52ba6a9
6 files changed +43 -420
modified src/bin/radicle-native-ci.rs
@@ -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(())
-
}
-

-
/// 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))
+
    exit(code);
}

-
#[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()
}
modified src/engine.rs
@@ -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
@@ -219,9 +223,6 @@ pub enum EngineError {
    Report(#[from] report::ReportError),

    #[error(transparent)]
-
    RunCmd(#[from] RunCmdError),
-

-
    #[error(transparent)]
    RunInfo(#[from] RunInfoError),

    #[error(transparent)]
modified src/lib.rs
@@ -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;
modified src/msg.rs
@@ -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.
@@ -82,9 +81,6 @@ pub enum NativeMessageError {
    Report(#[from] report::ReportError),

    #[error(transparent)]
-
    RunCmd(#[from] RunCmdError),
-

-
    #[error(transparent)]
    RunInfo(#[from] RunInfoError),

    #[error(transparent)]
modified src/run.rs
@@ -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,
@@ -222,9 +221,6 @@ pub enum RunError {
    Report(#[from] report::ReportError),

    #[error(transparent)]
-
    RunCmd(#[from] RunCmdError),
-

-
    #[error(transparent)]
    RunInfo(#[from] RunInfoError),

    #[error(transparent)]
deleted src/runcmd.rs
@@ -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),
-
}