use crate::domain::report::AnalysisReport;
use crate::domain::types::Severity;
use crate::ports::outbound::DiagnosticRenderer;
pub struct GithubActionsRenderer;
impl DiagnosticRenderer for GithubActionsRenderer {
fn render(&self, report: &AnalysisReport) -> Result<String, String> {
let mut out = String::new();
for f in &report.findings {
let level = match f.severity {
Severity::Info | Severity::Low => "notice",
Severity::Medium => "warning",
Severity::High | Severity::Critical => "error",
};
let msg = f
.explanation
.replace('%', "%25")
.replace('\r', "%0D")
.replace('\n', "%0A");
out.push_str(&format!(
"::{level} file={},line={},col={},title={}::{} [{}]\n",
f.location.file.display(),
f.location.start_line,
f.location.start_column,
f.rule_id,
msg,
f.remediation.replace('\n', " "),
));
}
if report.findings.is_empty() {
out.push_str("::notice::xbp audit errors found no issues\n");
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::types::{
Confidence, Finding, LanguageId, SourceLocation,
};
#[test]
fn annotation_format() {
let mut r = AnalysisReport::default();
r.findings.push(
Finding::builder("ignored-failure", LanguageId::rust())
.severity(Severity::High)
.confidence(Confidence::High)
.location(SourceLocation::single_line("src/main.rs", 12, 4))
.explanation("let _ = fallible()")
.failure_scenario("x")
.remediation("handle it")
.build(),
);
let s = GithubActionsRenderer.render(&r).unwrap();
assert!(s.starts_with("::error file="));
assert!(s.contains("line=12"));
assert!(s.contains("ignored-failure"));
}
}