Skip to main content

xbp_analysis/adapters/output/
github_actions.rs

1//! GitHub Actions workflow command annotations.
2
3use 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            // Escape for workflow commands
19            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}