Radish alpha
r
rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE
Radicle CI adapter for native CI
Radicle
Git
feat: add a module to construct HTML run logs
Lars Wirzenius committed 2 years ago
commit aafb25125403dfafce69c4db699f38d244d8cf9e
parent cced880
5 files changed +201 -19
modified src/bin/radicle-native-ci.rs
@@ -30,6 +30,7 @@ use radicle_native_ci::{
    report,
    runcmd::{runcmd, RunCmdError},
    runinfo::{RunInfo, RunInfoBuilder, RunInfoError},
+
    runlog::{RunLog, RunLogError},
    runspec::{RunSpec, RunSpecError},
};

@@ -155,7 +156,7 @@ struct Runner<'a> {
    commit: Oid,
    src: PathBuf,
    log: &'a mut LogFile,
-
    run_log: LogFile,
+
    run_log: RunLog,
    timeout: Option<usize>,
    builder: &'a mut RunInfoBuilder,
}
@@ -193,11 +194,9 @@ impl<'a> Runner<'a> {
        self.log
            .writeln(&format!("CI run on {}, {}", self.repo, self.commit))?;

-
        self.run_log.h1("Log from Radicle native CI")?;
-
        self.run_log
-
            .bullet_point(format!("* Repository id: `{}`", self.repo))?;
-
        self.run_log
-
            .bullet_point(format!("* Commit: `{}", 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)?;

@@ -228,8 +227,6 @@ impl<'a> Runner<'a> {

        write_response(&Response::finished(result.clone()))?;

-
        self.run_log.writeln("CI run finished successfully")?;
-

        std::fs::remove_dir_all(&self.src)
            .map_err(|e| NativeError::RemoveDir(self.src.clone(), e))?;

@@ -300,7 +297,7 @@ impl<'a> RunnerBuilder<'a> {

    fn build(self) -> Result<Runner<'a>, NativeError> {
        let run_log = self.run_log.ok_or(NativeError::Unset("run_log"))?;
-
        let run_log = LogFile::open(&run_log)?;
+
        let run_log = RunLog::new(&run_log);
        Ok(Runner {
            run_id: self.run_id.ok_or(NativeError::Unset("run_id"))?,
            storage: self.storage.ok_or(NativeError::Unset("storage"))?,
@@ -351,5 +348,8 @@ enum NativeError {
    RunInfo(#[from] RunInfoError),

    #[error(transparent)]
+
    RunLog(#[from] RunLogError),
+

+
    #[error(transparent)]
    RunSpec(#[from] RunSpecError),
}
added src/bin/run_log.rs
@@ -0,0 +1,22 @@
+
use std::path::Path;
+

+
use radicle_ci_broker::msg::{Id, Oid};
+

+
use radicle_native_ci::runlog::RunLog;
+

+
/// The main program.
+
fn main() {
+
    let mut run_log = RunLog::new(Path::new("testlog.html"));
+
    run_log.title("Some Title");
+
    run_log.rid(Id::from_urn("rad:z3qg5TKmN83afz2fj9z3fQjU8vaYE").expect("rid"));
+
    run_log.commit(Oid::try_from("b788f7ffd38572614457adb1656c0b4575b941dd").expect("commit"));
+
    run_log.runcmd(
+
        &["git", "pull"],
+
        Path::new("/tmp"),
+
        0,
+
        "This is stdout".as_bytes(),
+
        "Error messages go here".as_bytes(),
+
    );
+

+
    run_log.write().expect("write html log");
+
}
modified src/lib.rs
@@ -4,4 +4,5 @@ pub mod msg;
pub mod report;
pub mod runcmd;
pub mod runinfo;
+
pub mod runlog;
pub mod runspec;
modified src/runcmd.rs
@@ -4,17 +4,13 @@ use log::{debug, error};

use radicle_ci_broker::msg::{MessageError, Response};

-
use crate::logfile::{LogError, LogFile};
+
use crate::runlog::{RunLog, RunLogError};

/// Run a command in a directory.
-
pub fn runcmd(log: &mut LogFile, argv: &[&str], cwd: &Path) -> Result<(), RunCmdError> {
+
pub fn runcmd(run_log: &mut RunLog, argv: &[&str], cwd: &Path) -> Result<(), RunCmdError> {
    debug!("runcmd: argv={:?}", argv);
    debug!("runcmd: cwd={:?}", cwd);

-
    log.h2("Run command")?;
-
    log.fenced("argv", format!("{:?}", argv).as_bytes())?;
-
    log.writeln(&format!("in directory: {}\n", cwd.display()))?;
-

    assert!(!argv.is_empty());
    let argv0 = argv[0];
    let output = Command::new(argv0)
@@ -26,10 +22,14 @@ pub fn runcmd(log: &mut LogFile, argv: &[&str], cwd: &Path) -> Result<(), RunCmd

    let exit = output.status;
    debug!("exit: {:?}", exit);
-
    log.writeln(&format!("Exit: {}\n\n", exit.code().unwrap()))?;

-
    log.fenced("Standard output", &output.stdout)?;
-
    log.fenced("Standard error", &output.stderr)?;
+
    run_log.runcmd(
+
        argv,
+
        cwd,
+
        exit.code().unwrap(),
+
        &output.stdout,
+
        &output.stderr,
+
    );

    if !exit.success() {
        let error = Response::error(&format!("command failed: {:?}", argv));
@@ -50,7 +50,7 @@ pub enum RunCmdError {
    WriteResponse(Response, #[source] MessageError),

    #[error(transparent)]
-
    Log(#[from] LogError),
+
    RunLog(#[from] RunLogError),

    #[error("failed to run command {0:?}")]
    Command(Vec<String>, #[source] std::io::Error),
added src/runlog.rs
@@ -0,0 +1,159 @@
+
use std::path::{Path, PathBuf};
+

+
use html_page::{Document, Element, Tag};
+

+
use radicle_ci_broker::msg::{Id, Oid};
+

+
#[derive(Debug, Default)]
+
pub struct RunLog {
+
    filename: PathBuf,
+
    title: Option<String>,
+
    rid: Option<Id>,
+
    commit: Option<Oid>,
+
    commands: Vec<Command>,
+
}
+

+
impl RunLog {
+
    pub fn new(filename: &Path) -> Self {
+
        Self {
+
            filename: filename.into(),
+
            ..Default::default()
+
        }
+
    }
+

+
    pub fn title(&mut self, title: &str) {
+
        self.title = Some(title.into());
+
    }
+

+
    pub fn rid(&mut self, rid: Id) {
+
        self.rid = Some(rid);
+
    }
+

+
    pub fn commit(&mut self, commit: Oid) {
+
        self.commit = Some(commit);
+
    }
+

+
    pub fn runcmd(&mut self, argv: &[&str], cwd: &Path, exit: i32, stdout: &[u8], stderr: &[u8]) {
+
        self.commands.push(Command {
+
            argv: argv.iter().map(|a| a.to_string()).collect(),
+
            cwd: cwd.into(),
+
            exit,
+
            stdout: stdout.to_vec(),
+
            stderr: stderr.to_vec(),
+
        });
+
    }
+

+
    pub fn write(&self) -> Result<(), RunLogError> {
+
        let title = self.title.as_ref().ok_or(RunLogError::Missing("title"))?;
+
        let rid = self.rid.as_ref().ok_or(RunLogError::Missing("rid"))?;
+
        let commit = self.commit.as_ref().ok_or(RunLogError::Missing("commit"))?;
+

+
        let mut doc = Document::default();
+

+
        doc.push_to_head(&Element::new(Tag::Title).with_text(title));
+

+
        doc.push_to_body(&Element::new(Tag::H1).with_text(title));
+

+
        let mut ul = Element::new(Tag::Ul);
+
        ul.push_child(
+
            &Element::new(Tag::Li)
+
                .with_text("Repository id: ")
+
                .with_child(Element::new(Tag::Code).with_text(&rid.to_string())),
+
        );
+
        ul.push_child(
+
            &Element::new(Tag::Li)
+
                .with_text("Commit: ")
+
                .with_child(Element::new(Tag::Code).with_text(&commit.to_string())),
+
        );
+
        ul.push_child(
+
            &Element::new(Tag::Li)
+
                .with_text("Result: ")
+
                .with_child(Element::new(Tag::B).with_text(self.result())),
+
        );
+
        doc.push_to_body(&ul);
+

+
        for cmd in self.commands.iter() {
+
            cmd.format(&mut doc);
+
        }
+

+
        let html = format!("{}", doc);
+
        std::fs::write(&self.filename, html.as_bytes())
+
            .map_err(|e| RunLogError::Write(self.filename.clone(), e))?;
+

+
        Ok(())
+
    }
+

+
    fn result(&self) -> &'static str {
+
        if self.commands.iter().any(|c| c.exit != 0) {
+
            "FAILURE"
+
        } else {
+
            "SUCCESS"
+
        }
+
    }
+
}
+

+
#[derive(Debug)]
+
struct Command {
+
    argv: Vec<String>,
+
    cwd: PathBuf,
+
    exit: i32,
+
    stdout: Vec<u8>,
+
    stderr: Vec<u8>,
+
}
+

+
impl Command {
+
    fn command(&self) -> String {
+
        self.argv.as_slice().join(" ")
+
    }
+

+
    fn format(&self, doc: &mut Document) {
+
        doc.push_to_body(
+
            &Element::new(Tag::H2)
+
                .with_text("Run: ")
+
                .with_child(Element::new(Tag::Code).with_text(&self.command())),
+
        );
+

+
        doc.push_to_body(&Element::new(Tag::P).with_text("Command arguments:"));
+
        let mut ul = Element::new(Tag::Ul);
+
        for arg in self.argv.iter() {
+
            ul.push_child(
+
                &Element::new(Tag::Li)
+
                    .with_child(Element::new(Tag::Code).with_text(&format!("{:?}", arg))),
+
            );
+
        }
+
        doc.push_to_body(&ul);
+

+
        doc.push_to_body(
+
            &Element::new(Tag::P)
+
                .with_text("In directory: ")
+
                .with_child(Element::new(Tag::Code).with_text(&format!("{}", self.cwd.display()))),
+
        );
+

+
        doc.push_to_body(&Element::new(Tag::P).with_text(&format!("Exit code: {}", self.exit)));
+

+
        self.output(doc, "Stdout:", &self.stdout);
+
        self.output(doc, "Stderr:", &self.stderr);
+
    }
+

+
    fn output(&self, doc: &mut Document, stream: &str, data: &[u8]) {
+
        if !data.is_empty() {
+
            doc.push_to_body(&Element::new(Tag::P).with_text(stream));
+
            doc.push_to_body(
+
                &Element::new(Tag::P).with_child(
+
                    Element::new(Tag::Blockquote).with_child(
+
                        Element::new(Tag::Pre).with_text(&String::from_utf8_lossy(data)),
+
                    ),
+
                ),
+
            );
+
        }
+
    }
+
}
+

+
#[derive(Debug, thiserror::Error)]
+
pub enum RunLogError {
+
    #[error("programming error: missing field for run log {0}")]
+
    Missing(&'static str),
+

+
    #[error("failed to write HTML run log to {0}")]
+
    Write(PathBuf, #[source] std::io::Error),
+
}