pub fn generate_issue_templates(
report: &CategorizationReport,
labels: Option<Vec<String>>,
) -> Vec<TestIssueTemplate> {
let default_labels = vec!["test-quality".to_string(), "automated".to_string()];
let extra_labels = labels.unwrap_or_default();
report
.groups
.iter()
.map(|group| {
let test_list = group
.tests
.iter()
.take(20) .map(|t| format!("- `{}`", t.name))
.collect::<Vec<_>>()
.join("\n");
let more_tests = if group.tests.len() > 20 {
format!("\n... and {} more tests", group.tests.len() - 20)
} else {
String::new()
};
let body = format!(
r#"## Root Cause
{}
## Affected Tests ({} total)
{}{}
## Suggested Fix
{}
## Priority
{} (1=fix now, 5=low priority)
---
*Generated by `pmat test-discovery create-tickets`*
"#,
group.root_cause,
group.tests.len(),
test_list,
more_tests,
group.ignore_reason,
group.priority
);
let mut all_labels = default_labels.clone();
all_labels.extend(extra_labels.clone());
all_labels.push(format!("priority-{}", group.priority));
TestIssueTemplate {
title: format!("[Test Failures] {}", group.root_cause),
body,
labels: all_labels,
category: group.root_cause.clone(),
test_count: group.tests.len(),
}
})
.collect()
}
async fn handle_create_tickets(
input: &Path,
create: bool,
output: Option<&Path>,
repo: Option<&str>,
labels: Option<Vec<String>>,
) -> Result<()> {
println!("🎫 Creating tickets from categorized failures");
if !create {
println!(" (DRY RUN - use --create to create GitHub issues)");
}
println!();
let content = std::fs::read_to_string(input).context("Failed to read categorization report")?;
let report: CategorizationReport =
serde_json::from_str(&content).context("Failed to parse categorization report")?;
println!(" Found {} failure groups", report.groups.len());
let tickets = generate_issue_templates(&report, labels);
let tickets_report = TicketsReport {
tickets: tickets.clone(),
total_tests: report.total_failures,
timestamp: chrono::Utc::now().to_rfc3339(),
};
if let Some(output_path) = output {
let json = serde_json::to_string_pretty(&tickets_report)?;
std::fs::write(output_path, json)?;
println!(" Wrote tickets to: {}", output_path.display());
}
println!("\n📋 Generated {} issues:", tickets.len());
for ticket in &tickets {
println!(" 📝 {} ({} tests)", ticket.title, ticket.test_count);
}
if create {
if let Some(repo_name) = repo {
println!("\n🚀 Creating GitHub issues in {}...", repo_name);
for ticket in &tickets {
match create_github_issue(repo_name, ticket).await {
Ok(url) => println!(" ✅ Created: {}", url),
Err(e) => println!(" ❌ Failed: {}", e),
}
}
} else {
println!("\n⚠️ No --repo specified. Use --repo owner/repo to create issues.");
}
} else {
println!("\n Run with --create --repo owner/repo to create GitHub issues");
}
Ok(())
}
async fn create_github_issue(repo: &str, ticket: &TestIssueTemplate) -> Result<String> {
let labels_arg = ticket.labels.join(",");
let output = Command::new("gh")
.arg("issue")
.arg("create")
.arg("--repo")
.arg(repo)
.arg("--title")
.arg(&ticket.title)
.arg("--body")
.arg(&ticket.body)
.arg("--label")
.arg(&labels_arg)
.output()
.context("Failed to run gh CLI")?;
if output.status.success() {
let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(url)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("gh issue create failed: {}", stderr)
}
}