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 from_plan: bool,
14 plan_path: Option<&Path>,
15) -> Result<String> {
16 let repair_plan_path = plan_path
17 .map(Path::to_path_buf)
18 .unwrap_or_else(|| output_dir.join("repair-plan.md"));
19 if from_plan && repair_plan_path.exists() {
20 let repair_plan = std::fs::read_to_string(&repair_plan_path).into_diagnostic()?;
21 return Ok(with_marker(repair_plan.trim(), sticky_marker));
22 }
23
24 let comment_path = output_dir.join("pr-comment.md");
25 if comment_path.exists() {
26 let comment = std::fs::read_to_string(&comment_path).into_diagnostic()?;
27 return Ok(with_marker(comment.trim(), sticky_marker));
28 }
29
30 let doctor_path = output_dir.join("doctor.json");
31 let agent_pack_path = output_dir.join(".verifyos-agent").join("agent-pack.json");
32 let findings = load_agent_pack_findings(&agent_pack_path);
33 let doctor_summary = load_doctor_summary(&doctor_path);
34
35 let body = [
36 "## voc analysis",
37 "",
38 &format!("- Findings: **{}**", findings),
39 &format!("- Scan exit code: `{}`", scan_exit),
40 &format!("- Doctor exit code: `{}`", doctor_exit),
41 &format!("- Assets uploaded from: `{}`", output_dir.display()),
42 "- Includes: `report.sarif`, `AGENTS.md`, `fix-prompt.md`, `repair-plan.md`, `pr-brief.md`, `pr-comment.md`, `.verifyos-agent/`",
43 &format!("- Doctor summary: {}", doctor_summary),
44 ]
45 .join("\n");
46
47 Ok(with_marker(&body, sticky_marker))
48}
49
50fn with_marker(body: &str, sticky_marker: bool) -> String {
51 if sticky_marker {
52 format!("{STICKY_MARKER}\n{body}")
53 } else {
54 body.to_string()
55 }
56}
57
58fn load_agent_pack_findings(path: &Path) -> usize {
59 std::fs::read_to_string(path)
60 .ok()
61 .and_then(|raw| serde_json::from_str::<AgentPack>(&raw).ok())
62 .map(|pack| pack.total_findings)
63 .unwrap_or(0)
64}
65
66fn load_doctor_summary(path: &Path) -> String {
67 std::fs::read_to_string(path)
68 .ok()
69 .and_then(|raw| serde_json::from_str::<DoctorReport>(&raw).ok())
70 .map(|report| {
71 report
72 .checks
73 .into_iter()
74 .map(|item| format!("{}: {:?}", item.name, item.status).to_uppercase())
75 .collect::<Vec<_>>()
76 .join(", ")
77 })
78 .filter(|summary| !summary.is_empty())
79 .unwrap_or_else(|| "doctor report missing".to_string())
80}
81
82#[cfg(test)]
83mod tests {
84 use super::render_workflow_pr_comment;
85 use crate::doctor::{DoctorCheck, DoctorReport, DoctorStatus};
86 use crate::report::AgentPack;
87 use tempfile::tempdir;
88
89 #[test]
90 fn workflow_pr_comment_prefers_existing_file() {
91 let dir = tempdir().expect("temp dir");
92 std::fs::write(
93 dir.path().join("pr-comment.md"),
94 "## verifyOS review summary",
95 )
96 .expect("write comment");
97
98 let body = render_workflow_pr_comment(dir.path(), 1, 0, true, false, None)
99 .expect("render comment");
100
101 assert!(body.contains("<!-- voc-analysis-comment -->"));
102 assert!(body.contains("## verifyOS review summary"));
103 assert!(!body.contains("## voc analysis"));
104 }
105
106 #[test]
107 fn workflow_pr_comment_falls_back_to_doctor_and_agent_pack() {
108 let dir = tempdir().expect("temp dir");
109 let agent_dir = dir.path().join(".verifyos-agent");
110 std::fs::create_dir_all(&agent_dir).expect("create agent dir");
111 std::fs::write(
112 agent_dir.join("agent-pack.json"),
113 serde_json::to_string(&AgentPack {
114 generated_at_unix: 1,
115 total_findings: 3,
116 findings: Vec::new(),
117 })
118 .expect("json"),
119 )
120 .expect("write agent pack");
121 std::fs::write(
122 dir.path().join("doctor.json"),
123 serde_json::to_string(&DoctorReport {
124 checks: vec![DoctorCheck {
125 name: "Config".to_string(),
126 status: DoctorStatus::Pass,
127 detail: "ok".to_string(),
128 }],
129 repair_plan: Vec::new(),
130 plan_context: None,
131 })
132 .expect("json"),
133 )
134 .expect("write doctor report");
135
136 let body = render_workflow_pr_comment(dir.path(), 1, 0, false, false, None)
137 .expect("render comment");
138
139 assert!(body.contains("## voc analysis"));
140 assert!(body.contains("Findings: **3**"));
141 assert!(body.contains("Scan exit code: `1`"));
142 assert!(body.contains("Doctor exit code: `0`"));
143 assert!(body.contains("CONFIG: PASS"));
144 }
145
146 #[test]
147 fn workflow_pr_comment_can_reuse_repair_plan() {
148 let dir = tempdir().expect("temp dir");
149 std::fs::write(
150 dir.path().join("repair-plan.md"),
151 "# verifyOS Repair Plan\n\n## Context\n\n- Source: `fresh-scan`\n",
152 )
153 .expect("write repair plan");
154 std::fs::write(
155 dir.path().join("pr-comment.md"),
156 "## verifyOS review summary\n\n- stale\n",
157 )
158 .expect("write comment");
159
160 let body = render_workflow_pr_comment(dir.path(), 1, 0, false, true, None)
161 .expect("render comment");
162
163 assert!(body.contains("# verifyOS Repair Plan"));
164 assert!(!body.contains("## verifyOS review summary"));
165 }
166
167 #[test]
168 fn workflow_pr_comment_from_plan_matches_snapshot() {
169 let dir = tempdir().expect("temp dir");
170 std::fs::write(
171 dir.path().join("repair-plan.md"),
172 "# verifyOS Repair Plan\n\n## Context\n\n- Source: `fresh-scan`\n- Scan artifact: `examples/bad_app.ipa`\n\n## Planned Outputs\n\n- **pr-comment**\n - Path: `.verifyos/pr-comment.md`\n - Reason: refresh sticky PR comment draft\n",
173 )
174 .expect("write repair plan");
175
176 let body =
177 render_workflow_pr_comment(dir.path(), 1, 0, true, true, None).expect("render body");
178 let expected = "<!-- voc-analysis-comment -->\n# verifyOS Repair Plan\n\n## Context\n\n- Source: `fresh-scan`\n- Scan artifact: `examples/bad_app.ipa`\n\n## Planned Outputs\n\n- **pr-comment**\n - Path: `.verifyos/pr-comment.md`\n - Reason: refresh sticky PR comment draft";
179
180 assert_eq!(body, expected);
181 }
182
183 #[test]
184 fn workflow_pr_comment_can_use_explicit_plan_path() {
185 let dir = tempdir().expect("temp dir");
186 let nested = dir.path().join("plans");
187 std::fs::create_dir_all(&nested).expect("create plan dir");
188 let plan_path = nested.join("custom-plan.md");
189 std::fs::write(
190 &plan_path,
191 "# verifyOS Repair Plan\n\n## Context\n\n- Source: `existing-assets`\n",
192 )
193 .expect("write repair plan");
194
195 let body = render_workflow_pr_comment(dir.path(), 0, 0, false, true, Some(&plan_path))
196 .expect("render comment");
197
198 assert!(body.contains("# verifyOS Repair Plan"));
199 assert!(body.contains("existing-assets"));
200 }
201}