Files
radicle-native-ci/src/report.rs
T
2024-01-10 17:38:05 +02:00

135 lines
4.2 KiB
Rust

use std::{
collections::HashSet,
path::{Path, PathBuf},
};
use html_page::{Document, Element, Tag};
use radicle_ci_broker::msg::RunResult;
use walkdir::WalkDir;
use crate::{LogFile, RunInfo};
const CSS: &str = include_str!("native-ci.css");
pub fn build_report(_log: &mut LogFile, state: &Path) -> Result<(), ReportError> {
let mut run_infos = collect_run_infos(state)?;
run_infos.sort_by(|a, b| a.timestamp.partial_cmp(&b.timestamp).unwrap());
let repos = repositories(&run_infos);
let doc = list_runs(&repos, &run_infos);
let index = state.join("index.html");
std::fs::write(&index, doc.to_string().as_bytes())
.map_err(|e| ReportError::WriteHtml(index.clone(), e))?;
Ok(())
}
fn collect_run_infos(state: &Path) -> Result<Vec<RunInfo>, ReportError> {
let mut infos = vec![];
for entry in WalkDir::new(state) {
let entry = entry?;
if entry.path().ends_with("run.yaml") {
infos.push(read_file_info(entry.path())?);
}
}
Ok(infos)
}
fn repositories(infos: &[RunInfo]) -> Vec<&str> {
let mut ids: Vec<&str> = infos
.iter()
.map(|ri| ri.repo.as_str())
.collect::<HashSet<&str>>()
.iter()
.copied()
.collect();
ids.sort_by(|a, b| a.partial_cmp(b).unwrap());
ids
}
fn runs_for_repo<'a>(repo: &str, infos: &'a [RunInfo]) -> Vec<&'a RunInfo> {
infos.iter().filter(|ri| ri.repo == repo).collect()
}
fn read_file_info(filename: &Path) -> Result<RunInfo, ReportError> {
let yaml = std::fs::read(filename).map_err(|e| ReportError::ReadRunInfo(filename.into(), e))?;
serde_yaml::from_slice(&yaml).map_err(|e| ReportError::DeserializeRunInfo(filename.into(), e))
}
fn list_runs(repos: &[&str], run_infos: &[RunInfo]) -> Document {
let mut doc = Document::default();
doc.push_to_head(&Element::new(Tag::Title).with_text("CI run logs"));
doc.push_to_head(&Element::new(Tag::Style).with_text(CSS));
for repo in repos {
let section = Element::new(Tag::H1)
.with_text("Repository ")
.with_child(Element::new(Tag::Code).with_text(repo));
doc.push_to_body(&section);
let mut list = Element::new(Tag::Ol).with_class("runlist");
for ri in runs_for_repo(repo, run_infos) {
let commit = Element::new(Tag::Code)
.with_class("commit")
.with_text(&ri.commit);
let run_id = Element::new(Tag::Code)
.with_class("run_id")
.with_text(&ri.id);
let timestamp = Element::new(Tag::Span)
.with_class("timestamp")
.with_text(&ri.timestamp);
let (result_text, class) = match &ri.result {
RunResult::Success => ("success".into(), "success"),
RunResult::Failure => ("failure".into(), "failure"),
RunResult::Error(msg) => (format!("error: {}", msg), "failure"),
_ => (format!("unknown: {:?}", ri.result), "unknown"),
};
let result = Element::new(Tag::Span)
.with_class(class)
.with_text(&result_text);
let br = Element::new(Tag::Br);
let href = format!("{}", ri.log.display());
let link = Element::new(Tag::A)
.with_attribute("href", &href)
.with_text("CI run: ")
.with_child(run_id);
let item = Element::new(Tag::Li)
.with_child(timestamp)
.with_text(" ")
.with_child(result)
.with_child(br.clone())
.with_text("Commit: ")
.with_child(commit)
.with_child(br)
.with_child(link);
list.push_child(&item);
}
doc.push_to_body(&list);
}
doc
}
#[derive(Debug, thiserror::Error)]
pub enum ReportError {
#[error(transparent)]
WalkDir(#[from] walkdir::Error),
#[error("failed to read file for run metadata: {0}")]
ReadRunInfo(PathBuf, #[source] std::io::Error),
#[error("failed to parse run metadata as YAML: {0}")]
DeserializeRunInfo(PathBuf, #[source] serde_yaml::Error),
#[error("failed to write HTML file {0}")]
WriteHtml(PathBuf, #[source] std::io::Error),
}