verifyos_cli/
ci_comment.rs1use 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}