xbp_analysis/adapters/output/
github_actions.rs1use crate::domain::report::AnalysisReport;
4use crate::domain::types::Severity;
5use crate::ports::outbound::DiagnosticRenderer;
6
7pub struct GithubActionsRenderer;
8
9impl DiagnosticRenderer for GithubActionsRenderer {
10 fn render(&self, report: &AnalysisReport) -> Result<String, String> {
11 let mut out = String::new();
12 for f in &report.findings {
13 let level = match f.severity {
14 Severity::Info | Severity::Low => "notice",
15 Severity::Medium => "warning",
16 Severity::High | Severity::Critical => "error",
17 };
18 let msg = f
20 .explanation
21 .replace('%', "%25")
22 .replace('\r', "%0D")
23 .replace('\n', "%0A");
24 out.push_str(&format!(
25 "::{level} file={},line={},col={},title={}::{} [{}]\n",
26 f.location.file.display(),
27 f.location.start_line,
28 f.location.start_column,
29 f.rule_id,
30 msg,
31 f.remediation.replace('\n', " "),
32 ));
33 }
34 if report.findings.is_empty() {
35 out.push_str("::notice::xbp audit errors found no issues\n");
36 }
37 Ok(out)
38 }
39}
40
41#[cfg(test)]
42mod tests {
43 use super::*;
44 use crate::domain::types::{
45 Confidence, Finding, LanguageId, SourceLocation,
46 };
47
48 #[test]
49 fn annotation_format() {
50 let mut r = AnalysisReport::default();
51 r.findings.push(
52 Finding::builder("ignored-failure", LanguageId::rust())
53 .severity(Severity::High)
54 .confidence(Confidence::High)
55 .location(SourceLocation::single_line("src/main.rs", 12, 4))
56 .explanation("let _ = fallible()")
57 .failure_scenario("x")
58 .remediation("handle it")
59 .build(),
60 );
61 let s = GithubActionsRenderer.render(&r).unwrap();
62 assert!(s.starts_with("::error file="));
63 assert!(s.contains("line=12"));
64 assert!(s.contains("ignored-failure"));
65 }
66}