Skip to main content

verifyos_cli/
ci_comment.rs

1use crate::doctor::DoctorReport;
2use crate::report::AgentPack;
3use miette::{IntoDiagnostic, Result};
4use std::path::Path;
5
6const STICKY_MARKER: &str = "<!-- voc-analysis-comment -->";
7
8pub fn render_workflow_pr_comment(
9    output_dir: &Path,
10    scan_exit: i32,
11    doctor_exit: i32,
12    sticky_marker: bool,
13) -> Result<String> {
14    let comment_path = output_dir.join("pr-comment.md");
15    if comment_path.exists() {
16        let comment = std::fs::read_to_string(&comment_path).into_diagnostic()?;
17        return Ok(with_marker(comment.trim(), sticky_marker));
18    }
19
20    let doctor_path = output_dir.join("doctor.json");
21    let agent_pack_path = output_dir.join(".verifyos-agent").join("agent-pack.json");
22    let findings = load_agent_pack_findings(&agent_pack_path);
23    let doctor_summary = load_doctor_summary(&doctor_path);
24
25    let body = [
26        "## voc analysis",
27        "",
28        &format!("- Findings: **{}**", findings),
29        &format!("- Scan exit code: `{}`", scan_exit),
30        &format!("- Doctor exit code: `{}`", doctor_exit),
31        &format!("- Assets uploaded from: `{}`", output_dir.display()),
32        "- Includes: `report.sarif`, `AGENTS.md`, `fix-prompt.md`, `pr-brief.md`, `pr-comment.md`, `.verifyos-agent/`",
33        &format!("- Doctor summary: {}", doctor_summary),
34    ]
35    .join("\n");
36
37    Ok(with_marker(&body, sticky_marker))
38}
39
40fn with_marker(body: &str, sticky_marker: bool) -> String {
41    if sticky_marker {
42        format!("{STICKY_MARKER}\n{body}")
43    } else {
44        body.to_string()
45    }
46}
47
48fn load_agent_pack_findings(path: &Path) -> usize {
49    std::fs::read_to_string(path)
50        .ok()
51        .and_then(|raw| serde_json::from_str::<AgentPack>(&raw).ok())
52        .map(|pack| pack.total_findings)
53        .unwrap_or(0)
54}
55
56fn load_doctor_summary(path: &Path) -> String {
57    std::fs::read_to_string(path)
58        .ok()
59        .and_then(|raw| serde_json::from_str::<DoctorReport>(&raw).ok())
60        .map(|report| {
61            report
62                .checks
63                .into_iter()
64                .map(|item| format!("{}: {:?}", item.name, item.status).to_uppercase())
65                .collect::<Vec<_>>()
66                .join(", ")
67        })
68        .filter(|summary| !summary.is_empty())
69        .unwrap_or_else(|| "doctor report missing".to_string())
70}
71
72#[cfg(test)]
73mod tests {
74    use super::render_workflow_pr_comment;
75    use crate::doctor::{DoctorCheck, DoctorReport, DoctorStatus};
76    use crate::report::AgentPack;
77    use tempfile::tempdir;
78
79    #[test]
80    fn workflow_pr_comment_prefers_existing_file() {
81        let dir = tempdir().expect("temp dir");
82        std::fs::write(
83            dir.path().join("pr-comment.md"),
84            "## verifyOS review summary",
85        )
86        .expect("write comment");
87
88        let body = render_workflow_pr_comment(dir.path(), 1, 0, true).expect("render comment");
89
90        assert!(body.contains("<!-- voc-analysis-comment -->"));
91        assert!(body.contains("## verifyOS review summary"));
92        assert!(!body.contains("## voc analysis"));
93    }
94
95    #[test]
96    fn workflow_pr_comment_falls_back_to_doctor_and_agent_pack() {
97        let dir = tempdir().expect("temp dir");
98        let agent_dir = dir.path().join(".verifyos-agent");
99        std::fs::create_dir_all(&agent_dir).expect("create agent dir");
100        std::fs::write(
101            agent_dir.join("agent-pack.json"),
102            serde_json::to_string(&AgentPack {
103                generated_at_unix: 1,
104                total_findings: 3,
105                findings: Vec::new(),
106            })
107            .expect("json"),
108        )
109        .expect("write agent pack");
110        std::fs::write(
111            dir.path().join("doctor.json"),
112            serde_json::to_string(&DoctorReport {
113                checks: vec![DoctorCheck {
114                    name: "Config".to_string(),
115                    status: DoctorStatus::Pass,
116                    detail: "ok".to_string(),
117                }],
118                repair_plan: Vec::new(),
119            })
120            .expect("json"),
121        )
122        .expect("write doctor report");
123
124        let body = render_workflow_pr_comment(dir.path(), 1, 0, false).expect("render comment");
125
126        assert!(body.contains("## voc analysis"));
127        assert!(body.contains("Findings: **3**"));
128        assert!(body.contains("Scan exit code: `1`"));
129        assert!(body.contains("Doctor exit code: `0`"));
130        assert!(body.contains("CONFIG: PASS"));
131    }
132}