1use crate::stack::releaser::ReleaseOrchestrator;
6use crate::stack::types::PreflightCheck;
7use std::path::Path;
8use std::process::Command;
9
10use super::helpers::{
11 parse_count_from_json_multi, parse_value_from_json, run_check_command, score_check_result,
12};
13
14impl ReleaseOrchestrator {
15 pub(in crate::stack) fn check_git_clean(&self, crate_path: &Path) -> PreflightCheck {
17 let output =
18 Command::new("git").args(["status", "--porcelain"]).current_dir(crate_path).output();
19
20 match output {
21 Ok(out) => {
22 if out.stdout.is_empty() {
23 PreflightCheck::pass("git_clean", "Working directory is clean")
24 } else {
25 let files = String::from_utf8_lossy(&out.stdout);
26 PreflightCheck::fail(
27 "git_clean",
28 format!("Uncommitted changes:\n{}", files.trim()),
29 )
30 }
31 }
32 Err(e) => {
33 PreflightCheck::fail("git_clean", format!("Failed to check git status: {}", e))
34 }
35 }
36 }
37
38 pub(in crate::stack) fn check_lint(&self, crate_path: &Path) -> PreflightCheck {
40 run_check_command(
41 &self.config.lint_command,
42 "lint",
43 "No lint command configured",
44 crate_path,
45 |output, _stdout, stderr| {
46 if output.status.success() {
47 PreflightCheck::pass("lint", "Lint passed")
48 } else {
49 PreflightCheck::fail("lint", format!("Lint failed: {}", stderr.trim()))
50 }
51 },
52 )
53 }
54
55 pub(in crate::stack) fn check_coverage(&self, crate_path: &Path) -> PreflightCheck {
57 let min_coverage = self.config.min_coverage;
58 run_check_command(
59 &self.config.coverage_command,
60 "coverage",
61 "No coverage command configured",
62 crate_path,
63 move |output, _stdout, _stderr| {
64 if output.status.success() {
65 PreflightCheck::pass(
66 "coverage",
67 format!("Coverage check passed (min: {}%)", min_coverage),
68 )
69 } else {
70 PreflightCheck::fail("coverage", format!("Coverage below {}%", min_coverage))
71 }
72 },
73 )
74 }
75
76 pub(in crate::stack) fn check_pmat_comply(&self, crate_path: &Path) -> PreflightCheck {
84 let fail_on_violations = self.config.fail_on_comply_violations;
85 run_check_command(
86 &self.config.comply_command,
87 "pmat_comply",
88 "No comply command configured (skipped)",
89 crate_path,
90 move |output, stdout, stderr| {
91 let has_violations = stdout.contains("CB-")
92 || stderr.contains("CB-")
93 || stdout.contains("violation")
94 || stderr.contains("violation");
95
96 if output.status.success() && !has_violations {
97 PreflightCheck::pass("pmat_comply", "PMAT comply passed (0 violations)")
98 } else if has_violations && fail_on_violations {
99 let violation_hint = if stdout.contains("CB-") {
100 stdout
101 .lines()
102 .filter(|l| l.contains("CB-"))
103 .take(3)
104 .collect::<Vec<_>>()
105 .join("; ")
106 } else {
107 "violations detected".to_string()
108 };
109 PreflightCheck::fail(
110 "pmat_comply",
111 format!("PMAT comply failed: {}", violation_hint),
112 )
113 } else if has_violations {
114 PreflightCheck::pass("pmat_comply", "PMAT comply has warnings (not blocking)")
115 } else {
116 PreflightCheck::fail(
117 "pmat_comply",
118 format!("PMAT comply error: {}", stderr.trim()),
119 )
120 }
121 },
122 )
123 }
124
125 pub(in crate::stack) fn check_no_path_deps(&self, _crate_name: &str) -> PreflightCheck {
127 PreflightCheck::pass("no_path_deps", "No path dependencies found")
130 }
131
132 pub(in crate::stack) fn check_version_bumped(&self, _crate_name: &str) -> PreflightCheck {
134 PreflightCheck::pass("version_bumped", "Version is ahead of crates.io")
137 }
138
139 pub(in crate::stack) fn check_pmat_quality_gate(&self, crate_path: &Path) -> PreflightCheck {
152 let fail_on_gate = self.config.fail_on_quality_gate;
153 run_check_command(
154 &self.config.quality_gate_command,
155 "quality_gate",
156 "No quality-gate command configured (skipped)",
157 crate_path,
158 move |output, _stdout, stderr| {
159 if output.status.success() {
160 PreflightCheck::pass("quality_gate", "PMAT quality-gate passed")
161 } else if fail_on_gate {
162 PreflightCheck::fail(
163 "quality_gate",
164 format!("Quality gate failed: {}", stderr.trim()),
165 )
166 } else {
167 PreflightCheck::pass("quality_gate", "Quality gate has warnings (not blocking)")
168 }
169 },
170 )
171 }
172
173 pub(in crate::stack) fn check_pmat_tdg(&self, crate_path: &Path) -> PreflightCheck {
178 let min_score = self.config.min_tdg_score;
179 let fail_on = self.config.fail_on_tdg;
180 run_check_command(
181 &self.config.tdg_command,
182 "tdg",
183 "No TDG command configured (skipped)",
184 crate_path,
185 move |output, stdout, _stderr| {
186 let score = parse_value_from_json(stdout, &["score", "tdg_score", "total"]);
187 score_check_result(
188 "tdg",
189 "TDG score",
190 score,
191 min_score,
192 fail_on,
193 output.status.success(),
194 )
195 },
196 )
197 }
198
199 pub(in crate::stack) fn check_pmat_dead_code(&self, crate_path: &Path) -> PreflightCheck {
203 let fail_on = self.config.fail_on_dead_code;
204 run_check_command(
205 &self.config.dead_code_command,
206 "dead_code",
207 "No dead-code command configured (skipped)",
208 crate_path,
209 move |_output, stdout, _stderr| {
210 let has_dead_code = stdout.contains("dead_code") || stdout.contains("unused");
211 let count = parse_count_from_json_multi(stdout, &["count", "dead_code_count"]);
212
213 match (has_dead_code, count) {
214 (_, Some(0)) | (false, None) => {
215 PreflightCheck::pass("dead_code", "No dead code detected")
216 }
217 (_, Some(n)) if fail_on => {
218 PreflightCheck::fail("dead_code", format!("{} dead code items found", n))
219 }
220 (_, Some(n)) => PreflightCheck::pass(
221 "dead_code",
222 format!("{} dead code items (warning)", n),
223 ),
224 (true, None) if fail_on => {
225 PreflightCheck::fail("dead_code", "Dead code detected")
226 }
227 (true, None) => {
228 PreflightCheck::pass("dead_code", "Dead code detected (warning)")
229 }
230 }
231 },
232 )
233 }
234
235 pub(in crate::stack) fn check_pmat_complexity(&self, crate_path: &Path) -> PreflightCheck {
240 let max_complexity = self.config.max_complexity;
241 let fail_on = self.config.fail_on_complexity;
242 run_check_command(
243 &self.config.complexity_command,
244 "complexity",
245 "No complexity command configured (skipped)",
246 crate_path,
247 move |output, stdout, _stderr| {
248 let max_found = parse_count_from_json_multi(stdout, &["max_complexity", "highest"]);
249 let violations =
250 parse_count_from_json_multi(stdout, &["violations", "violation_count"]);
251
252 match (max_found, violations) {
253 (Some(m), _) if m <= max_complexity => PreflightCheck::pass(
254 "complexity",
255 format!("Max complexity: {} (limit: {})", m, max_complexity),
256 ),
257 (Some(m), _) if fail_on => PreflightCheck::fail(
258 "complexity",
259 format!("Complexity {} exceeds limit {}", m, max_complexity),
260 ),
261 (_, Some(0)) => PreflightCheck::pass("complexity", "No complexity violations"),
262 (_, Some(v)) if fail_on => {
263 PreflightCheck::fail("complexity", format!("{} complexity violations", v))
264 }
265 _ if output.status.success() => {
266 PreflightCheck::pass("complexity", "Complexity check passed")
267 }
268 _ => PreflightCheck::pass("complexity", "Complexity check completed (warning)"),
269 }
270 },
271 )
272 }
273
274 pub(in crate::stack) fn check_pmat_satd(&self, crate_path: &Path) -> PreflightCheck {
279 let max_items = self.config.max_satd_items;
280 let fail_on = self.config.fail_on_satd;
281 run_check_command(
282 &self.config.satd_command,
283 "satd",
284 "No SATD command configured (skipped)",
285 crate_path,
286 move |output, stdout, _stderr| {
287 let count = parse_count_from_json_multi(stdout, &["total", "count", "satd_count"]);
288
289 match count {
290 Some(c) if c <= max_items => PreflightCheck::pass(
291 "satd",
292 format!("{} SATD items (limit: {})", c, max_items),
293 ),
294 Some(c) if fail_on => PreflightCheck::fail(
295 "satd",
296 format!("{} SATD items exceed limit {}", c, max_items),
297 ),
298 Some(c) => PreflightCheck::pass(
299 "satd",
300 format!("{} SATD items (warning: exceeds {})", c, max_items),
301 ),
302 None if output.status.success() => {
303 PreflightCheck::pass("satd", "SATD check passed")
304 }
305 None => PreflightCheck::pass("satd", "SATD check completed"),
306 }
307 },
308 )
309 }
310
311 pub(in crate::stack) fn check_pmat_popper(&self, crate_path: &Path) -> PreflightCheck {
317 let min_score = self.config.min_popper_score;
318 let fail_on = self.config.fail_on_popper;
319 run_check_command(
320 &self.config.popper_command,
321 "popper",
322 "No Popper command configured (skipped)",
323 crate_path,
324 move |output, stdout, _stderr| {
325 let score = parse_value_from_json(stdout, &["score", "popper_score", "total"]);
326 score_check_result(
327 "popper",
328 "Popper score",
329 score,
330 min_score,
331 fail_on,
332 output.status.success(),
333 )
334 },
335 )
336 }
337
338 pub(in crate::stack) fn check_book_build(&self, crate_path: &Path) -> PreflightCheck {
347 let book_dir = crate_path.join("book");
349 if !book_dir.exists() {
350 return PreflightCheck::pass("book", "No book directory found (skipped)");
351 }
352
353 let fail_on = self.config.fail_on_book;
354
355 run_check_command(
356 &self.config.book_command,
357 "book",
358 "No book command configured (skipped)",
359 crate_path,
360 move |output, _stdout, stderr| {
361 if output.status.success() {
362 PreflightCheck::pass("book", "Book built successfully")
363 } else if fail_on {
364 PreflightCheck::fail("book", format!("Book build failed: {}", stderr.trim()))
365 } else {
366 PreflightCheck::pass("book", "Book build has warnings (not blocking)")
367 }
368 },
369 )
370 }
371
372 pub(in crate::stack) fn check_examples_run(&self, crate_path: &Path) -> PreflightCheck {
377 let parts: Vec<&str> = self.config.examples_command.split_whitespace().collect();
378 if parts.is_empty() {
379 return PreflightCheck::pass("examples", "No examples command configured (skipped)");
380 }
381
382 let examples_dir = crate_path.join("examples");
384 if !examples_dir.exists() {
385 return PreflightCheck::pass("examples", "No examples directory found (skipped)");
386 }
387
388 let examples = self.discover_examples(crate_path);
390 if examples.is_empty() {
391 return PreflightCheck::pass("examples", "No examples found (skipped)");
392 }
393
394 let mut failed = Vec::new();
395 let mut succeeded = 0;
396
397 for example in &examples {
398 let output = Command::new("cargo")
400 .args(["run", "--example", example, "--", "--help"])
401 .current_dir(crate_path)
402 .output();
403
404 match output {
405 Ok(out) => {
406 if out.status.success() || out.status.code() == Some(0) {
409 succeeded += 1;
410 } else {
411 let stderr = String::from_utf8_lossy(&out.stderr);
413 if stderr.contains("error[E") || stderr.contains("could not compile") {
414 failed.push(example.clone());
415 } else {
416 succeeded += 1;
418 }
419 }
420 }
421 Err(_) => {
422 failed.push(example.clone());
423 }
424 }
425 }
426
427 if failed.is_empty() {
428 PreflightCheck::pass(
429 "examples",
430 format!("{}/{} examples verified", succeeded, examples.len()),
431 )
432 } else if self.config.fail_on_examples {
433 PreflightCheck::fail(
434 "examples",
435 format!(
436 "{}/{} examples failed: {}",
437 failed.len(),
438 examples.len(),
439 failed.join(", ")
440 ),
441 )
442 } else {
443 PreflightCheck::pass(
444 "examples",
445 format!(
446 "{}/{} examples verified ({} failed, not blocking)",
447 succeeded,
448 examples.len(),
449 failed.len()
450 ),
451 )
452 }
453 }
454
455 pub(in crate::stack) fn discover_examples(&self, crate_path: &Path) -> Vec<String> {
457 let mut examples = Vec::new();
458
459 let cargo_toml = crate_path.join("Cargo.toml");
461 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
462 for line in content.lines() {
464 if line.trim().starts_with("name = \"") {
465 if let Some(name) = line.split('"').nth(1) {
468 let example_file = crate_path.join("examples").join(format!("{}.rs", name));
470 if example_file.exists() {
471 examples.push(name.to_string());
472 }
473 }
474 }
475 }
476 }
477
478 let examples_dir = crate_path.join("examples");
480 if let Ok(entries) = std::fs::read_dir(&examples_dir) {
481 for entry in entries.flatten() {
482 let path = entry.path();
483 if path.extension().is_some_and(|e| e == "rs") {
484 if let Some(stem) = path.file_stem() {
485 let name = stem.to_string_lossy().to_string();
486 if !examples.contains(&name) {
487 examples.push(name);
488 }
489 }
490 }
491 }
492 }
493
494 examples
495 }
496
497 pub(in crate::stack) fn parse_score_from_json(json: &str, key: &str) -> Option<f64> {
503 let pattern = format!("\"{}\":", key);
505 if let Some(pos) = json.find(&pattern) {
506 let after_key = &json[pos + pattern.len()..];
507 let value_str: String = after_key
508 .chars()
509 .skip_while(|c| c.is_whitespace())
510 .take_while(|c| c.is_numeric() || *c == '.' || *c == '-')
511 .collect();
512 value_str.parse().ok()
513 } else {
514 None
515 }
516 }
517
518 pub(in crate::stack) fn parse_count_from_json(json: &str, key: &str) -> Option<u32> {
520 Self::parse_score_from_json(json, key).map(|f| f as u32)
521 }
522}